This commit is contained in:
Teo
2025-09-06 15:27:22 +08:00
71 changed files with 7093 additions and 1831 deletions

View File

@ -5,14 +5,16 @@ VITE_APP_TITLE = 煤科建管平台
VITE_APP_ENV = 'development'
# 开发环境
VITE_APP_BASE_API = 'http://192.168.110.149:8899'
# 李陈杰 209
# VITE_APP_BASE_API = 'http://192.168.110.209:8899'
# 曾涛
# VITE_APP_BASE_API = 'http://192.168.110.180:8899'
# 罗成
# VITE_APP_BASE_API = 'http://192.168.110.188:8899'
# 朱银
VITE_APP_BASE_API = 'http://192.168.110.149:8899'
# VITE_APP_BASE_API = 'http://192.168.110.180:8899'
#曾涛
# VITE_APP_BASE_API = 'http://192.168.110.171:8899'

View File

@ -0,0 +1 @@

111
src/api/equipment/index.ts Normal file
View File

@ -0,0 +1,111 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { EquipmentVO, EquipmentForm, EquipmentQuery } from '@/api/equipment/types';
/**
* 查询GPS设备详细列表
* @param query
* @returns {*}
*/
export const listEquipment = (query?: EquipmentQuery): AxiosPromise<EquipmentVO[]> => {
return request({
url: '/gps/equipment/list',
method: 'get',
params: query
});
};
/**
* 查询GPS设备详细详细
* @param id
*/
export const getEquipment = (id: string | number): AxiosPromise<EquipmentVO> => {
return request({
url: '/gps/equipment/' + id,
method: 'get'
});
};
/**
* 新增GPS设备详细
* @param data
*/
export const addEquipment = (data: EquipmentForm) => {
return request({
url: '/gps/equipment',
method: 'post',
data: data
});
};
/**
* 修改GPS设备详细
* @param data
*/
export const updateEquipment = (data: EquipmentForm) => {
return request({
url: '/gps/equipment',
method: 'put',
data: data
});
};
/**
* 删除GPS设备详细
* @param id
*/
export const delEquipment = (id: string | number | Array<string | number>) => {
return request({
url: '/gps/equipment/' + id,
method: 'delete'
});
};
export const bindUser = (data) => {
return request({
url: '/gps/equipment/bindManmachine',
method: 'post',
data: data
});
};
export const getUserId = (projectId) => {
return request({
url: '/gps/equipment/userList',
method: 'get',
params: { projectId }
});
};
export const gethistroyUser = (data) => {
return request({
url: '/gps/equipment/getUserList',
method: 'get',
params: data
});
};
// 解除绑定接口定义确保使用POST方法并正确传递data
export function getRemoveBind(data: { id: number; clientId: string }) {
return request({
url: '/gps/equipment/unbindManmachine',
method: 'post',
data: data
});
}
export function getProjectId() {
return request({
url: 'gps/equipment/getProjectList',
method: 'get'
});
}
export const getFootNote = (data) => {
return request({
url: 'gps/equipmentSon/getList',
method: 'get',
data: data
});
};

169
src/api/equipment/types.ts Normal file
View File

@ -0,0 +1,169 @@
export interface EquipmentVO {
/**
*
*/
id: string | number;
/**
* 项目ID
*/
projectId: string | number;
/**
* 用户id
*/
userId: string | number;
/**
* 设备标识
*/
clientId: string | number;
/**
* 设备名称
*/
deviceName: string;
/**
* 是否使用UDP协议0=否1=是
*/
udp: number;
/**
* 远程连接地址IP:端口)
*/
remoteAddressStr: string;
/**
* 连接创建时间
*/
creationTime: number;
/**
* 最后活动时间
*/
lastAccessedTime: number;
/**
* 是否已注册0=未注册1=已注册
*/
registered: number;
/**
* 备注
*/
remark: string;
}
export interface EquipmentForm extends BaseEntity {
/**
*
*/
id?: string | number;
/**
* 项目ID
*/
projectId?: string | number;
/**
* 用户id
*/
userId?: string | number;
/**
* 设备标识
*/
clientId?: string | number;
/**
* 设备名称
*/
deviceName?: string;
/**
* 是否使用UDP协议0=否1=是
*/
udp?: number;
/**
* 远程连接地址IP:端口)
*/
remoteAddressStr?: string;
/**
* 连接创建时间
*/
creationTime?: number;
/**
* 最后活动时间
*/
lastAccessedTime?: number;
/**
* 是否已注册0=未注册1=已注册
*/
registered?: number;
/**
* 备注
*/
remark?: string;
}
export interface EquipmentQuery extends PageQuery {
/**
* 项目ID
*/
projectId?: string | number;
/**
* 是否绑定
*/
type?: string | number;
/**
* 用户id
*/
userId?: string | number;
/**
* 设备标识
*/
clientId?: string | number;
/**
* 设备名称
*/
deviceName?: string;
/**
* 是否使用UDP协议0=否1=是
*/
udp?: number;
/**
* 远程连接地址IP:端口)
*/
remoteAddressStr?: string;
/**
* 连接创建时间
*/
creationTime?: number;
/**
* 最后活动时间
*/
lastAccessedTime?: number;
/**
* 是否已注册0=未注册1=已注册
*/
registered?: number;
/**
* 日期范围参数
*/
params?: any;
}

View File

@ -14,6 +14,18 @@ export const listCompany = (query?: CompanyQuery): AxiosPromise<CompanyVO[]> =>
method: 'get',
params: query
});
}; /**
* 查询材料提供商
* @param query
* @returns {*}
*/
export const supplierInputGet = (query?) => {
return request({
url: '/supplierInput/supplierInput/getList',
method: 'get',
params: query
});
};
/**

View File

@ -0,0 +1,27 @@
import request from '@/utils/request';
//获取出库材料得列表
export const outboundMaterials = (query?: any) => {
return request({
url: '/materials/materials/listRelevancy',
method: 'get',
params: query
});
};
//新增出库
export const addOutbound = (data?: any) => {
return request({
url: '/materials/materialsInventory',
method: 'post',
data
});
};
//获取材料列表
export const getMaterialsList = (query?: any) => {
return request({
url: '/materials/materials/list',
method: 'get',
params: query
});
};

View File

@ -33,7 +33,6 @@ export interface MaterialsUseRecordVO {
* 备注
*/
remark: string;
}
export interface MaterialsUseRecordForm extends BaseEntity {
@ -71,11 +70,9 @@ export interface MaterialsUseRecordForm extends BaseEntity {
* 备注
*/
remark?: string;
}
export interface MaterialsUseRecordQuery extends PageQuery {
/**
* 项目ID
*/
@ -101,11 +98,9 @@ export interface MaterialsUseRecordQuery extends PageQuery {
*/
residueNumber?: string | number;
/**
* 日期范围参数
*/
params?: any;
/**
* 日期范围参数
*/
params?: any;
materialsId?: string | number;
}

View File

@ -8,7 +8,7 @@ import { MonthPlanVO, MonthPlanForm, MonthPlanQuery } from '@/api/out/monthPlan/
* @returns {*}
*/
export const listMonthPlan = (query?: MonthPlanQuery): AxiosPromise<MonthPlanVO[]> => {
export const listMonthPlan = (query?: any) => {
return request({
url: '/out/monthPlan/list',
method: 'get',

View File

@ -203,3 +203,31 @@ export const importConstructionUserInfo = (file: string) => {
data: { file }
});
};
// 获取项目列表
export const ProjectList = (query) => {
return request({
url: '/contractor/constructionUser/projectList',
method: 'get',
params: query
});
};
// 获取班组列表
export const TeamList = (query) => {
return request({
url: '/contractor/constructionUser/teamList',
method: 'get',
params: query
});
};
// 班组分配
export const TeamDistribution = (data) => {
return request({
url: '/contractor/constructionUser/addTeam',
method: 'post',
data: data
});
};

View File

@ -182,6 +182,8 @@ export interface ConstructionUserVO {
* 创建时间
*/
createTime: string;
sysUserId: string | number;
}
export interface skipType {
/**

View File

@ -227,3 +227,39 @@ export const byProjectIdDetail = (id) => {
method: 'get'
});
};
// 新增项目打卡范围
export const addAttendanceRange = (data) => {
return request({
url: '/project/projectPunchrange',
method: 'post',
data
});
};
// 删除项目打卡范围
export const delAttendanceRange = (id) => {
return request({
url: '/project/projectPunchrange/' + id,
method: 'delete'
});
};
// 修改项目打卡范围
export const updateAttendanceRange = (data) => {
return request({
url: '/project/projectPunchrange',
method: 'put',
data
});
};
// 查询项目打卡范围列表
export const getAttendanceRangeList = (data) => {
return request({
url: '/project/projectPunchrange/list',
method: 'get',
params: data
});
};

View File

@ -72,3 +72,12 @@ export const delProjectTeam = (id: string | number | Array<string | number>) =>
method: 'delete'
});
};
// 获取项目得打卡范围
export const getProjectTeamClockIn = (params) => {
return request({
url: '/project/projectTeam/rangeList',
method: 'get',
params
});
};

View File

@ -55,6 +55,11 @@ export interface ProjectTeamForm extends BaseEntity {
* 备注
*/
remark?: string;
/**
* 创建时间
*/
punchRangeList?: [];
}
export interface ProjectTeamQuery extends PageQuery {

View File

@ -0,0 +1,66 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
// 查询生项目天气
export const getScreenWeather = (projectId: number | string) => {
return request({
url: '/project/big/screen/weather/' + projectId,
method: 'get',
});
};
// 查询项目安全天数
export const getScreenSafetyDay = (projectId: number | string) => {
return request({
url: '/project/big/screen/safetyDay/' + projectId,
method: 'get',
});
};
// 查询项目公告
export const getScreenNews = (projectId: number | string) => {
return request({
url: '/project/big/screen/news/' + projectId,
method: 'get',
});
};
// 查询项目土地统计
export const getScreenLand = (projectId: number | string) => {
return request({
url: '/project/big/screen/' + projectId,
method: 'get',
});
};
// 查询项目形象进度
export const getScreenImgProcess = (projectId: number | string) => {
return request({
url: '/project/big/screen/imageProgress/' + projectId,
method: 'get',
});
};
// 查询项目人员情况
export const getScreenPeople = (projectId: number | string) => {
return request({
url: '/project/big/screen/people/' + projectId,
method: 'get',
});
};
// 查询项目AI安全巡检
export const getScreenSafetyInspection = (projectId: number | string) => {
return request({
url: '/project/big/screen/safetyInspection/' + projectId,
method: 'get',
});
};
// 查询项目概况
export const getScreenGeneralize = (projectId: number | string) => {
return request({
url: '/project/big/screen/generalize/' + projectId,
method: 'get',
});
};

View File

@ -0,0 +1,5 @@
export interface TableQuery extends PageQuery {
tableName: string;
tableComment: string;
dataName: string;
}

View File

@ -79,3 +79,26 @@ export const landTransferLedgerCount = (id: string | number | Array<string | num
method: 'get'
});
};
export const addSonLandTransferLedger = (data) => {
return request({
url: '/land/landTransferLedger/children/add',
method: 'post',
data: data
});
};
export const listSonLandTransferLedger = (query) => {
return request({
url: '/land/landTransferLedger/children/list',
method: 'get',
params: query
});
};
// api/yourApiModule.js
export const listallCountValue = (projectId) => {
return request({
url: '/land/landTransferLedger/allCountValue/' + projectId,
method: 'get'
});
};

View File

@ -4,6 +4,11 @@ export interface LandTransferLedgerVO {
*/
id: string | number;
/**
* 父级ID
*/
parentId: string | number;
/**
* 项目ID
*/
@ -83,7 +88,6 @@ export interface LandTransferLedgerVO {
* 下一步策略
*/
nextStrategy: string;
}
export interface LandTransferLedgerForm extends BaseEntity {
@ -92,6 +96,11 @@ export interface LandTransferLedgerForm extends BaseEntity {
*/
id?: string | number;
/**
* 父级ID
*/
parentId: string | number;
/**
* 项目ID
*/
@ -171,11 +180,9 @@ export interface LandTransferLedgerForm extends BaseEntity {
* 下一步策略
*/
nextStrategy?: string;
}
export interface LandTransferLedgerQuery extends PageQuery {
/**
* 项目ID
*/
@ -256,11 +263,8 @@ export interface LandTransferLedgerQuery extends PageQuery {
*/
nextStrategy?: string;
/**
* 日期范围参数
*/
params?: any;
/**
* 日期范围参数
*/
params?: any;
}

View File

@ -20,18 +20,20 @@ export const getMenu = (menuId: string | number): AxiosPromise<MenuVO> => {
};
// 查询菜单下拉树结构
export const treeselect = (): AxiosPromise<MenuTreeOption[]> => {
export const treeselect = (params?: any): AxiosPromise<MenuTreeOption[]> => {
return request({
url: '/system/menu/treeselect',
method: 'get'
method: 'get',
params
});
};
// 根据角色ID查询菜单下拉树结构
export const roleMenuTreeselect = (roleId: string | number): AxiosPromise<RoleMenuTree> => {
export const roleMenuTreeselect = (roleId: string | number, params?: any): AxiosPromise<RoleMenuTree> => {
return request({
url: '/system/menu/roleMenuTreeselect/' + roleId,
method: 'get'
method: 'get',
params
});
};

View File

@ -71,7 +71,7 @@ export function getRoleList(deptId?: number | string): AxiosPromise<any[]> {
url: '/system/role/listNoPage',
method: 'get',
params: {
deptId,
deptId
}
});
}

View File

@ -147,10 +147,11 @@ export const authUserSelectAll = (data: any) => {
});
};
// 根据角色ID查询部门树结构
export const deptTreeSelect = (roleId: string | number): AxiosPromise<RoleDeptTree> => {
export const deptTreeSelect = (roleId: string | number, params?) => {
return request({
url: '/system/role/deptTree/' + roleId,
method: 'get'
method: 'get',
params
});
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

View File

@ -1,26 +1,27 @@
<template>
<div class="centerPage">
<div class="topPage">
<!-- 暂无 -->
<div id="earth" style="width: 100%;height: 100%;"></div>
</div>
<div class="endPage">
<Title title="AI安全巡检" :prefix="true" />
<div class="swiper">
<div class="endPage" :class="{ 'slide-out-down': isHide }">
<Title title="AI安全巡检">
<img src="@/assets/projectLarge/robot.svg" alt="" height="20px" width="20px">
</Title>
<div class="swiper" v-if="inspectionList.length">
<div class="arrow" :class="{ 'canUse': canLeft }" @click="swiperClick('left')">
<el-icon size="16" color="skyblue">
<el-icon size="16" :color="canLeft ? 'rgba(29, 214, 255, 1)' : 'rgba(29, 214, 255, 0.3)'">
<ArrowLeft />
</el-icon>
</div>
<div class="swiper_content" ref="swiperContent">
<div class="swiper_item" v-for="(item, index) in swiperList" :key="index">
<img src="@/assets/projectLarge/swiper.png" alt="" class="swiper_img">
<div class="swiper_date">{{ item.date }}</div>
<div class="swiper_tip">{{ item.tip }}</div>
<div class="swiper_item" v-for="(item, i) in inspectionList" :key="i">
<img :src="item.picture" alt="安全巡检" class="swiper_img">
<div class="swiper_date">{{ item.createTime.slice(5) }}</div>
<div class="swiper_tip">{{ item.label }}</div>
</div>
</div>
<div class="arrow" :class="{ 'canUse': canRight }" @click="swiperClick('right')">
<el-icon size="16">
<el-icon size="16" :color="canRight ? 'rgba(29, 214, 255, 1)' : 'rgba(29, 214, 255, 0.3)'">
<ArrowRight />
</el-icon>
</div>
@ -29,31 +30,39 @@
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
<script setup>
import { ref, onMounted, toRefs, getCurrentInstance } from "vue"
import Title from './title.vue'
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
import { getScreenSafetyInspection } from '@/api/projectScreen'
const swiperList = ref([
{ date: '03-18 15:00', tip: '未佩戴安全帽1' },
{ date: '03-18 15:00', tip: '未佩戴安全帽2' },
{ date: '03-18 15:00', tip: '未佩戴安全帽3' },
{ date: '03-18 15:00', tip: '未佩戴安全帽4' },
{ date: '03-18 15:00', tip: '未佩戴安全帽5' },
{ date: '03-18 15:00', tip: '未佩戴安全帽6' },
{ date: '03-18 15:00', tip: '未佩戴安全帽7' },
{ date: '03-18 15:00', tip: '未佩戴安全帽8' },
{ date: '03-18 15:00', tip: '未佩戴安全帽9' },
{ date: '03-18 15:00', tip: '未佩戴安全帽10' },
{ date: '03-18 15:00', tip: '未佩戴安全帽11' },
{ date: '03-18 15:00', tip: '未佩戴安全帽12' },
])
const { proxy } = getCurrentInstance();
const { violation_level_type } = toRefs(proxy?.useDict('violation_level_type'));
const swiperContent = ref<HTMLDivElement>()
const props = defineProps({
isHide: {
type: Boolean,
default: false
},
projectId: {
type: String,
default: ""
}
})
const inspectionList = ref([{
id: "",
label: "",
picture: "",
createTime: ""
}])
const swiperContent = ref()
const swiperItemWidth = ref(100)
const canLeft = ref(false)
const canRight = ref(true)
const swiperClick = (direction: 'left' | 'right') => {
const swiperClick = (direction) => {
if (!swiperContent.value) return
if (direction === 'right') {
if (swiperContent.value.scrollLeft >= swiperContent.value.scrollWidth - swiperContent.value.clientWidth) {
@ -70,10 +79,79 @@ const swiperClick = (direction: 'left' | 'right') => {
}
swiperContent.value.scrollLeft -= swiperItemWidth.value
}
// 更新箭头状态
canLeft.value = swiperContent.value.scrollLeft > 0
canRight.value = swiperContent.value.scrollLeft < swiperContent.value.scrollWidth - swiperContent.value.clientWidth
}
const getInspectionList = async () => {
const res = await getScreenSafetyInspection(props.projectId)
const { code, data } = res
if (code === 200) {
data.map(item => {
item.label = violation_level_type.value.find((i) => i.value === item.violationType)?.label
})
inspectionList.value = data
}
}
// 创建地球
const createEarth = () => {
window.YJ.on({
ws: true,
// host: getIP(), //资源所在服务器地址
// username: this.loginForm.username, //用户名 可以不登录(不填写用户名),不登录时无法加载服务端的数据
// password: md5pass, //密码 生成方式md5(用户名_密码)
}).then((res) => {
let earth = new YJ.YJEarth("earth");
window.Earth1 = earth;
YJ.Global.openRightClick(window.Earth1);
YJ.Global.openLeftClick(window.Earth1);
let view = {
"position": {
"lng": 102.03643298211526,
"lat": 34.393586474501,
"alt": 11298179.51993155
},
"orientation": {
"heading": 360,
"pitch": -89.94481747201486,
"roll": 0
}
}
loadBaseMap(earth.viewer)
YJ.Global.CesiumContainer(window.Earth1, {
compass: false, //罗盘
});
// YJ.Global.flyTo(earth, view);
// YJ.Global.setDefaultView(earth.viewer, view)
})
}
// 加载底图
const loadBaseMap = (viewer) => {
// 创建瓦片提供器
const imageryProvider = new Cesium.UrlTemplateImageryProvider({
url: 'https://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
// 可选:设置瓦片的格式
fileExtension: 'png',
// 可选:设置瓦片的范围和级别
minimumLevel: 0,
maximumLevel: 18,
// 可选设置瓦片的投影默认为Web Mercator
projection: Cesium.WebMercatorProjection,
// 可选:如果瓦片服务需要跨域请求,设置请求头部
credit: new Cesium.Credit('卫星图数据来源')
});
// 添加图层到视图
const layer = viewer.imageryLayers.addImageryProvider(imageryProvider);
}
onMounted(() => {
swiperItemWidth.value = swiperContent.value.children[0].clientWidth + 20
getInspectionList()
createEarth()
if (swiperContent.value && swiperContent.value.children.length > 0) {
swiperItemWidth.value = swiperContent.value.children[0].clientWidth + 20
}
})
</script>
@ -82,24 +160,41 @@ onMounted(() => {
.centerPage {
display: flex;
flex-direction: column;
width: 50vw;
height: 100%;
}
.topPage,
.endPage {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
padding: 15px 0;
border: 1px solid rgba(29, 214, 255, 0.1);
box-sizing: border-box;
}
.topPage,
.endPage {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
padding: 15px 0;
border: 1px solid rgba(230, 247, 255, 0.1);
box-sizing: border-box;
}
.topPage {
flex: 1;
margin-bottom: 23px;
}
.topPage {
flex: 1;
margin-bottom: 23px;
transition: flex 0.5s ease;
}
.endPage {
max-height: 300px;
opacity: 1;
transition: all 0.5s ease;
}
/* 向下滑出动画 */
.slide-out-down {
transform: translateY(100%);
opacity: 0;
max-height: 0;
padding: 0;
margin: 0;
border: none;
}
.swiper {
@ -161,14 +256,20 @@ onMounted(() => {
.arrow {
display: grid;
place-items: center;
width: 20px;
height: 20px;
width: 24px;
height: 24px;
border-radius: 50%;
border: 1px solid skyblue;
border: 1px solid rgba(29, 214, 255, 0.3);
color: skyblue;
cursor: pointer;
transition: all 0.3s ease;
&:canUse {
color: #000 !important;
&.canUse {
border: 1px solid rgba(29, 214, 255, 1);
}
&:hover:not(.canUse) {
opacity: 0.7;
}
}
</style>

View File

@ -6,7 +6,7 @@
</div>
<div style="font-size: 12px; padding-left: 10px">安全生产天数</div>
<div class="header_left_text">
1,235
{{ safetyDay }}
<span style="font-size: 12px"></span>
</div>
</div>
@ -14,16 +14,18 @@
<div>XXX智慧工地管理平台</div>
<div>XXX Smart Construction Stic Management Dashboard</div>
</div>
<div class="right">
<div class="header_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 class="weather-list" @mouseenter="requestPause" @mouseleave="resumeScroll">
<div v-for="(item, i) in weatherList" :key="i" class="weather-item"
:style="{ transform: `translateY(-${offsetY}px)`, transition: transition }">
<img :src="`../../../src/assets/images/${item.icon}.png`" alt="" />
<div>{{ item.weather }}{{ item.tempMin }}°/{{ item.tempMax }}°</div>
<div>{{ item.week }}({{ item.date }})</div>
</div>
</div>
</div>
<!-- 分割线 -->
<div class="divider">
@ -35,48 +37,155 @@
<img src="@/assets/large/setting.png" alt="设置图标" />
<span>管理系统</span>
</div>
<!-- 分割线 -->
<div class="divider">
<div class="top-block"></div>
<div class="bottom-block"></div>
</div>
<!-- -->
<div class="change" @click="emit('changePage')">
<el-icon size="20" v-if="!isFull">
<Expand />
</el-icon>
<el-icon size="20" v-else>
<Fold />
</el-icon>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const week = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
const date = ref({
ymd: '',
hms: '',
week: 0
import { ref, onMounted, onUnmounted } from 'vue';
import { getScreenSafetyDay, getScreenWeather } from '@/api/projectScreen';
interface Weather {
week: string;
date: string;
icon: string;
weather: string;
tempMax: string;
tempMin: string;
}
const props = defineProps({
projectId: {
type: String,
default: ''
},
isFull: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['changePage'])
const safetyDay = ref<number>(0);
const weatherList = ref<Weather[]>([])
const timer = ref<number | null>(0)
const offsetY = ref<number>(0)
const curIndex = ref(0)
const transition = ref('transform 0.5s ease');
const pendingPause = ref(false);
/**
* 判断当前时间是白天/夜晚
*/
function judgeDayOrNight(sunRise: string, sunSet: string) {
// 将 "HH:MM" 格式转为分钟数(便于计算)
const timeToMinutes = (timeStr: any) => {
const [hours, minutes] = timeStr.split(':').map(Number);
return isNaN(hours) || isNaN(minutes) ? 0 : hours * 60 + minutes;
};
// 转换日出、日落时间为分钟数
const sunRiseMinutes = timeToMinutes(sunRise);
const sunSetMinutes = timeToMinutes(sunSet);
// 获取当前时间并转为分钟数
const now = new Date();
const currentMinutes = now.getHours() * 60 + now.getMinutes();
// true 白天 false 夜晚
return currentMinutes >= sunRiseMinutes && currentMinutes <= sunSetMinutes
? true
: false;
}
/**
* 设置天气周期滑动
*/
const setWeatherScroll = () => {
curIndex.value += 1
transition.value = 'transform 0.3s ease';
offsetY.value = curIndex.value * 60
if (curIndex.value === weatherList.value.length - 1) {
setTimeout(() => {
transition.value = 'none';
curIndex.value = 0;
offsetY.value = 0;
}, 350);
}
}
function startScroll() {
if (timer.value) clearInterval(timer.value);
timer.value = window.setInterval(setWeatherScroll, 5000);
}
function requestPause() {
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
pendingPause.value = true;
}
function resumeScroll() {
console.log('resumeScroll')
pendingPause.value = false;
startScroll();
}
onMounted(() => {
/**
* 获取安全生产天数
*/
getScreenSafetyDay(props.projectId).then(res => {
const { data, code } = res
if (code === 200) {
safetyDay.value = data.safetyDay;
}
})
/**
* 获取近三天天气
*/
getScreenWeather(props.projectId).then(res => {
const { data, code } = res
if (code === 200) {
data.forEach(item => {
if (judgeDayOrNight(item.sunRise, item.sunSet)) {
item.weather = item.dayStatus
item.icon = item.dayIcon
} else {
item.weather = item.nightStatus
item.icon = item.nightIcon
}
})
weatherList.value = data
// 多添加第一项 实现无缝衔接
weatherList.value = [...weatherList.value, weatherList.value[0]]
startScroll()
}
})
});
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);
});
if (timer.value) {
clearInterval(timer.value)
}
})
</script>
<style scoped lang="scss">
@ -107,6 +216,12 @@ onUnmounted(() => {
}
}
.header_right {
width: 100%;
height: 100%;
display: flex;
}
.title {
color: #fff;
font-family: 'AlimamaShuHeiTi', sans-serif;
@ -124,12 +239,6 @@ onUnmounted(() => {
font-size: 14px;
}
.right {
width: 100%;
height: 100%;
display: flex;
}
/* 顶部栏容器Flex 水平布局 + 垂直居中 */
.top-bar {
width: 100%;
@ -137,7 +246,6 @@ onUnmounted(() => {
display: flex;
align-items: center;
justify-content: flex-end;
// background-color: #1e2128;
color: #fff;
padding: 8px 16px;
font-size: 14px;
@ -145,24 +253,37 @@ onUnmounted(() => {
/* 左侧区域(天气 + 日期):自身也用 Flex 水平排列,确保元素在一行 */
.left-section {
height: 100%;
display: flex;
align-items: center;
// margin-right: auto; /* 让右侧元素(管理系统)居右 */
}
.left-section img {
width: 32px;
height: 32px;
margin-right: 8px;
/* 图标与文字间距 */
.weather-list {
height: 60px;
overflow: hidden;
.weather-item {
height: 60px;
line-height: 60px;
display: flex;
align-items: center;
&>div:last-child {
margin-left: 10px;
}
img {
width: 50px;
height: 50px;
}
}
}
}
/* 分割线(视觉分隔,可根据需求调整样式) */
.divider {
display: grid;
grid-template-rows: 1fr 1fr;
height: 100%;
/* 根据需要调整高度 */
gap: 2px;
padding: 14px 10px;
}
@ -195,4 +316,11 @@ onUnmounted(() => {
margin-right: 6px;
/* 图标与文字间距 */
}
.change {
display: grid;
place-items: center;
margin-right: 10px;
cursor: pointer;
}
</style>

View File

@ -2,30 +2,53 @@
<div class="leftPage">
<div class="topPage">
<Title title="项目公告" />
<div class="content">
<div class="content_item" v-for="item in 6" :key="item">
<div class="round">
<div class="sub_round"></div>
<div class="content_item" v-for="item in news" :key="item.id">
<img src="@/assets/projectLarge/round.svg" alt="">
<div class="ellipsis">
{{ item.title }}
<span @click="showNewsDetail(item)" style="color: rgba(138, 149, 165, 1);">{{ item.id === newId ? '关闭' :
'查看' }}</span>
</div>
<div class="ellipsis">2025年6月23日 重庆市两江新区广场前期准备与审批完毕区广场前期准备与审批完毕前期准备与审批完毕区广场前期准备与审批完毕</div>
</div>
</div>
</div>
<div class="detailBox" :class="{ 'show': newId }">
<!-- <div class="detail_title">{{ newDetail.title }}</div> -->
<div class="detail_content" v-html="newDetail.content"></div>
</div>
<div class="endPage">
<Title title="人员情况" />
<div class="map">
<img src="@/assets/projectLarge/map.svg" alt="">
<!-- <div ref="mapChartRef"></div> -->
</div>
<div class="attendance_tag">
<div class="tag_item" v-for="(item, index) in tagList" :key="index">
<div class="tag_item">
<img src="@/assets/projectLarge/people.svg" alt="">
<div class="tag_title">{{ item.title }}</div>
<div class="tag_title">出勤人</div>
<div class="tag_info">
{{ item.number }}
<span style="font-size: 14px;">{{ index === 2 ? '%' : '人' }}</span>
{{ attendanceCount }}
<span style="font-size: 14px;"></span>
</div>
</div>
<div class="tag_item">
<img src="@/assets/projectLarge/people.svg" alt="">
<div class="tag_title">在岗人</div>
<div class="tag_info">
{{ peopleCount }}
<span style="font-size: 14px;"></span>
</div>
</div>
<div class="tag_item">
<img src="@/assets/projectLarge/people.svg" alt="">
<div class="tag_title">出勤率</div>
<div class="tag_info">
{{ attendanceRate }}
<span style="font-size: 14px;">%</span>
</div>
</div>
</div>
@ -37,11 +60,12 @@
<div class="attendance_item_title">出勤率</div>
<div class="attendance_item_title">出勤时间</div>
</div>
<div v-for="item in list" :key="item.title" class="attendance_item">
<div class="attendance_item_title">{{ item.title }}</div>
<div class="attendance_item_number">{{ item.number }} <span class="subfont">/{{ item.number }}</span></div>
<div v-for="item in teamAttendanceList" :key="item.id" class="attendance_item">
<div class="attendance_item_title">{{ item.teamName }}</div>
<div class="attendance_item_number">{{ item.attendanceNumber }} <span class="subfont">/{{ item.allNumber
}}</span></div>
<div class="attendance_item_rate">{{ item.attendanceRate }} %</div>
<div class="attendance_item_date subfont">{{ item.date }}</div>
<div class="attendance_item_date subfont">{{ item.attendanceTime }}</div>
</div>
</div>
</div>
@ -51,29 +75,101 @@
<script setup lang="ts">
import { ref } from "vue"
import Title from './title.vue'
import { getScreenNews, getScreenPeople } from '@/api/projectScreen';
import { mapOption } from './optionList'
import * as echarts from 'echarts';
const list = ref([
{ title: '智慧系统运维', number: 30, attendanceRate: 100, date: '2025-08-05 08:10' },
{ title: '智慧系统运维', number: 30, attendanceRate: 100, date: '2025-08-05 08:10' },
{ title: '智慧系统运维', number: 30, attendanceRate: 100, date: '2025-08-05 08:10' },
{ title: '智慧系统运维', number: 30, attendanceRate: 100, date: '2025-08-05 08:10' },
{ title: '智慧系统运维', number: 30, attendanceRate: 100, date: '2025-08-05 08:10' },
{ title: '智慧系统运维', number: 30, attendanceRate: 100, date: '2025-08-05 08:10' },
const props = defineProps({
projectId: {
type: String,
default: ''
}
})
let mapChart = null
const mapChartRef = ref<HTMLDivElement | null>(null);
const news = ref([])
const newDetail = ref({
title: '',
content: ''
})
const newId = ref('')
const attendanceCount = ref(0)
const attendanceRate = ref(0)
const peopleCount = ref(0)
const teamAttendanceList = ref([
{ id: "", teamName: "", attendanceNumber: 0, allNumber: 0, attendanceRate: 0, attendanceTime: "" },
])
const tagList = ref([
{ title: '出勤人数', number: 259 },
{ title: '在岗人数', number: 100 },
{ title: '出勤率', number: 100 },
])
/**
* 显示新闻详情
*/
const showNewsDetail = (item: any) => {
if (newId.value === item.id) {
newId.value = ''
return
}
newDetail.value = item
newId.value = item.id
}
/**
* 获取项目人员出勤数据
*/
const getPeopleData = async () => {
const res = await getScreenPeople(props.projectId);
const { data, code } = res
if (code === 200) {
attendanceCount.value = data.attendanceCount
attendanceRate.value = data.attendanceRate
peopleCount.value = data.peopleCount
teamAttendanceList.value = data.teamAttendanceList
}
}
/**
* 获取项目新闻数据
*/
const getNewsData = async () => {
const res = await getScreenNews(props.projectId);
const { data, code } = res
if (code === 200) {
news.value = data
}
}
/**
* 初始化地图
*/
const initMapChart = () => {
if (!mapChartRef.value) {
return;
}
mapChart = echarts.init(mapChartRef.value);
mapChart.setOption(mapOption);
}
onMounted(() => {
// nextTick(() => {
// initMapChart();
// });
getPeopleData()
getNewsData()
})
onUnmounted(() => {
// if (mapChart) {
// mapChart.dispose();
// mapChart = null;
// }
});
</script>
<style scoped lang="scss">
.leftPage {
display: flex;
flex-direction: column;
width: calc(25vw - 30px);
margin: 0 15px;
height: 100%;
.topPage,
@ -115,11 +211,11 @@ const tagList = ref([
display: flex;
align-items: flex-start;
gap: 10px;
// position: relative;
margin-bottom: 20px;
font-size: 14px;
font-weight: 400;
color: rgba(230, 247, 255, 1);
cursor: pointer;
.ellipsis {
display: -webkit-box;
@ -134,21 +230,8 @@ const tagList = ref([
margin-bottom: 0;
}
.round {
display: grid;
place-items: center;
img {
margin-top: 3px;
width: 12px;
height: 12px;
border-radius: 50%;
background: rgba(29, 214, 255, 0.3);
.sub_round {
width: 6px;
height: 6px;
border-radius: 50%;
background: #1DD6FF;
}
}
}
}
@ -165,12 +248,13 @@ const tagList = ref([
margin-top: 15px;
.tag_item {
width: 28%;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
border: 1px dashed rgba(29, 214, 255, 0.3);
padding: 10px 25px;
padding: 10px;
.tag_info {
font-size: 20px;
@ -201,4 +285,45 @@ const tagList = ref([
.subfont {
color: rgba(138, 149, 165, 1);
}
.detailBox {
position: absolute;
left: 20vw;
top: 0;
width: 300px;
height: 300px;
max-height: 500px;
overflow-y: auto;
padding: 0 15px;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
transition: all 0.3s ease;
opacity: 0;
z-index: -1;
&.show {
left: 25vw;
opacity: 1;
z-index: 1;
}
}
.detailBox::before {
content: '';
/* 绝对定位相对于父元素 */
position: absolute;
/* 定位到左侧中间位置 */
left: -10px;
top: 50%;
/* 垂直居中 */
transform: translateY(-50%);
/* 利用边框创建三角形 */
border-width: 10px 10px 10px 0;
border-style: solid;
/* 三角形颜色与背景匹配,左侧边框透明 */
border-color: transparent rgba(255, 255, 255, 0.2) transparent transparent;
/* 确保三角形在内容下方 */
z-index: -1;
}
</style>

View File

@ -0,0 +1,153 @@
export let pieOption = {
// 定义中心文字
graphic: [
{
type: 'text',
left: 'center',
top: '40%',
style: {
// 需要从接口替换
text: '70%',
fontSize: 24,
fontWeight: 'bold',
fill: '#fff'
}
},
{
type: 'text',
left: 'center',
top: '50%',
style: {
text: '总进度',
fontSize: 14,
fill: '#fff'
}
},
],
legend: {
show: true,
type: 'plain',
bottom: 20,
itemWidth: 12,
itemHeight: 12,
textStyle: {
color: '#fff'
}
},
series: {
type: 'pie',
data: [],
radius: [50, 80],
center: ['50%', '45%'],
itemStyle: {
borderColor: '#fff',
borderWidth: 1
},
label: {
alignTo: 'edge',
formatter: '{name|{b}}\n{percent|{c} %}',
minMargin: 10,
edgeDistance: 20,
lineHeight: 15,
rich: {
name: {
fontSize: 12,
color: '#fff'
},
percent: {
fontSize: 12,
color: '#fff'
}
}
},
legend: {
top: 'bottom'
},
}
};
export let barOption = {
legend: {
icon: 'rect',
itemWidth: 12,
itemHeight: 12,
// 调整文字与图标间距
data: ['计划流转面积', '已流转面积'],
top: 0,
right: 20,
textStyle: {
color: '#fff',
}
},
xAxis: {
type: 'category',
data: ['地块1', '地块2', '地块3', '地块4', '地块5', '地块6'],
axisLabel: {
color: '#fff'
},
axisLine: {
show: false
},
splitLine: {
show: false
}
},
yAxis: {
name: '单位m²',
type: 'value',
axisLabel: {
formatter: '{value}'
}
},
grid: {
bottom: 0, // 距离容器底部的距离
containLabel: true // 确保坐标轴标签不被裁剪
},
series: [
{
name: '计划流转面积',
type: 'bar',
data: [],
barWidth: '20%',
itemStyle: {
color: 'rgb(29, 253, 253)'
},
},
{
name: '已流转面积',
type: 'bar',
data: [],
barWidth: '20%',
itemStyle: {
color: 'rgb(25, 181, 251)'
},
}
]
};
export let mapOption = {
geo: {
map: 'ch',
roam: true,
aspectScale: Math.cos((47 * Math.PI) / 180),
},
series: [
{
type: 'graph',
coordinateSystem: 'geo',
data: [
{ name: 'a', value: [7.667821250000001, 46.791734269956265] },
{ name: 'b', value: [7.404848750000001, 46.516308805996054] },
{ name: 'c', value: [7.376673125000001, 46.24728858538375] },
{ name: 'd', value: [8.015320625000001, 46.39460918238572] },
{ name: 'e', value: [8.616400625, 46.7020608630855] },
{ name: 'f', value: [8.869981250000002, 46.37539345234199] },
{ name: 'g', value: [9.546196250000001, 46.58676648282309] },
{ name: 'h', value: [9.311399375, 47.182454114178896] },
{ name: 'i', value: [9.085994375000002, 47.55395822835779] },
{ name: 'j', value: [8.653968125000002, 47.47709530818285] },
{ name: 'k', value: [8.203158125000002, 47.44506909144329] }
],
}
]
};

View File

@ -2,21 +2,14 @@
<div class="leftPage">
<div class="topPage">
<Title title="项目概况" />
<div class="content">
<div class="content_item">项目名称智慧生态工地社区开发项目</div>
<div class="content_item">项目位置贵州省贵阳市乌当区具体地块编号01-123-11</div>
<div class="content_item">占地面积约10000亩</div>
<div class="content_item"> 土地性质城镇住宅用地兼容商业用地容积率2.5</div>
</div>
<div class="content" v-html="generalize"></div>
</div>
<div class="endPage">
<!-- 饼图容器 -->
<Title title="形象进度" />
<div ref="pieChartRef" class="echart" />
<!-- 折线图容器 -->
<div ref="lineChartRef" class="echart" />
<div class="chart_container">
<div ref="pieChartRef" class="echart" />
<div ref="lineChartRef" class="echart" />
</div>
</div>
</div>
</template>
@ -25,149 +18,45 @@
import { ref, onMounted, onUnmounted, nextTick } from "vue"
import Title from './title.vue'
import * as echarts from 'echarts';
import { pieOption, barOption } from './optionList';
import { getScreenLand, getScreenImgProcess, getScreenGeneralize } from '@/api/projectScreen';
const props = defineProps({
projectId: {
type: String,
default: 0
}
})
const generalize = ref()
// 饼图相关
const pieChartRef = ref<HTMLDivElement | null>(null);
let pieChart: any = null;
const totalPercent = ref(0)
// 折线图相关
const lineChartRef = ref<HTMLDivElement | null>(null);
let lineChart: any = null;
// 土地数据 折线图
const designAreaData = ref([])
const transferAreaData = ref([])
// 饼图数据
const pieData = [
{ name: '桩点浇筑', value: 13 },
{ name: '水泥灌注', value: 7 },
{ name: '箱变安装', value: 40 },
{ name: '支架安装', value: 20 },
{ name: '组件安装', value: 20 },
{ label: 'areaPercentage', name: '厂区', value: 0 },
{ label: 'roadPercentage', name: '道路', value: 0 },
{ label: 'collectorLinePercentage', name: '集电线路', value: 0 },
{ label: 'exportLinePercentage', name: '送出线路', value: 0 },
{ label: 'substationPercentage', name: '升压站', value: 0 },
{ label: 'boxTransformerPercentage', name: '箱变', value: 0 },
]
// 折线图数据
const barData = {
xAxis: ['地块1', '地块2', '地块3', '地块4', '地块5', '地块6'],
series: [
{
name: '计划流转面积',
data: [70, 25, 45, 115, 70, 85]
},
{
name: '已流转面积',
data: [105, 30, 150, 65, 80, 200]
}
]
}
// 饼图配置
const pieOption = {
series: {
type: 'pie',
data: pieData,
radius: [50, 80],
itemStyle: {
borderColor: '#fff',
borderWidth: 1
},
label: {
alignTo: 'edge',
formatter: '{name|{b}}\n{percent|{c} %}',
minMargin: 10,
edgeDistance: 20,
lineHeight: 15,
rich: {
name: {
fontSize: 12,
color: '#fff'
},
percent: {
fontSize: 12,
color: '#fff'
}
}
},
legend: {
top: 'bottom'
},
}
};
// 柱状图配置
const barOption = {
legend: {
data: ['计划流转面积', '已流转面积'],
top: 0
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: barData.xAxis
},
yAxis: {
name: '单位m²',
type: 'value',
axisLabel: {
formatter: '{value}'
}
},
series: [
{
type: 'bar',
data: [], // 空数据仅用于承载markArea
markArea: {
silent: true, // 背景不响应交互
data: (() => {
const groupCount = 3; // 共3组6个月 ÷ 2
const groupWidth = 1.8; // 每组背景宽度覆盖2根柱子
const bgData = [];
for (let i = 0; i < groupCount; i++) {
const startX = i * 2 - 0.9; // 每组起始位置
const endX = startX + groupWidth; // 每组结束位置
bgData.push([
{ xAxis: startX, yAxis: 0 },
{ xAxis: endX, yAxis: 100 }
]);
}
return bgData;
})(),
itemStyle: {
color: 'rgba(255, 255, 255, 0.05)',
borderRadius: 4
}
}
},
{
name: '计划流转面积',
type: 'bar',
data: barData.series[0].data,
barWidth: 15, // 柱形宽度
itemStyle: {
color: 'rgb(29, 253, 253)'
},
},
{
name: '已流转面积',
type: 'bar',
data: barData.series[1].data,
barWidth: 15,
itemStyle: {
color: '#rgb(25, 181, 251)'
},
}
]
};
// 初始化饼图
const initPieChart = () => {
if (!pieChartRef.value) {
console.error('未找到饼图容器元素');
return;
}
pieOption.series.data = pieData
pieOption.graphic[0].style.text = totalPercent.value + '%'
pieChart = echarts.init(pieChartRef.value, null, {
renderer: 'canvas',
useDirtyRect: false
@ -181,6 +70,8 @@ const initLineChart = () => {
console.error('未找到折线图容器元素');
return;
}
barOption.series[0].data = designAreaData.value
barOption.series[1].data = transferAreaData.value
lineChart = echarts.init(lineChartRef.value, null, {
renderer: 'canvas',
useDirtyRect: false
@ -194,11 +85,52 @@ const handleResize = () => {
if (lineChart) lineChart.resize();
};
/**
* 获取项目土地统计数据
*/
const getScreenLandData = async () => {
const res = await getScreenLand(props.projectId);
const { data, code } = res
if (code === 200) {
designAreaData.value = data.map((item: any) => Number(item.designArea))
transferAreaData.value = data.map((item: any) => Number(item.transferArea))
initLineChart();
}
}
/**
* 获取项目形象进度数据
*/
const getScreenImgProcessData = async () => {
const res = await getScreenImgProcess(props.projectId);
const { data, code } = res
if (code === 200) {
totalPercent.value = data.totalPercentage
pieData.forEach((item: any) => {
item.value = data[item.label]
})
initPieChart()
}
}
/**
* 获取项目概况数据
*/
const getScreenGeneralizeData = async () => {
const res = await getScreenGeneralize(props.projectId);
const { data, code } = res
if (code === 200) {
generalize.value = data
}
}
// 组件挂载时初始化图表
onMounted(() => {
getScreenLandData()
getScreenImgProcessData()
getScreenGeneralizeData()
nextTick(() => {
initPieChart();
initLineChart();
window.addEventListener('resize', handleResize);
});
});
@ -221,8 +153,6 @@ onUnmounted(() => {
.leftPage {
display: flex;
flex-direction: column;
width: calc(25vw - 30px);
margin: 0 15px;
height: 100%;
.topPage,
@ -240,15 +170,38 @@ onUnmounted(() => {
flex: 1;
margin-top: 23px;
.echart {
.chart_container {
display: flex;
flex-direction: column;
gap: 5px;
width: 100%;
height: 100%;
}
.echart {
height: 48%;
width: 100%;
}
}
}
.content {
margin: 10px 35px;
height: 100px;
margin: 0 15px;
padding: 0 10px;
margin-top: 15px;
box-sizing: border-box;
overflow-y: auto;
&::-webkit-scrollbar-track {
background: rgba(204, 204, 204, 0.1);
border-radius: 10px;
}
&::-webkit-scrollbar-thumb {
background: rgba(29, 214, 255, 0.78);
border-radius: 10px;
}
.content_item {
font-size: 14px;
@ -262,12 +215,6 @@ onUnmounted(() => {
}
}
.ellipse {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.subfont {
color: rgba(138, 149, 165, 1);
}

View File

@ -4,8 +4,8 @@
<img src="@/assets/projectLarge/section.svg" alt="">
<img src="@/assets/projectLarge/border.svg" alt="">
</div>
<div v-if="prefix">
<img src="@/assets/projectLarge/robot.svg" alt="" style="width: 20px; height: 20px;margin-right: 5px;">
<div>
<slot></slot>
</div>
<div>{{ title }}</div>
</div>

View File

@ -1,45 +1,108 @@
<template>
<div class="large-screen">
<Header />
<div class="large_screen">
<Header :projectId="projectId" :isFull="isFull" @changePage="handleChangePage" />
<div class="nav">
<leftPage />
<centerPage />
<rightPage />
<div class="nav_left" :style="{ left: isHideOther ? '-25vw' : '0' }">
<leftPage :projectId="projectId" />
</div>
<div class="nav_center" :style="{ width: isFull ? '100%' : 'calc(50vw - 30px)' }">
<centerPage :projectId="projectId" :isHide="isFull" />
</div>
<div class="nav_right" :style="{ right: isHideOther ? '-25vw' : '0' }">
<rightPage :projectId="projectId" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
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 { useUserStoreHook } from '@/store/modules/user';
const userStore = useUserStoreHook();
const projectId = computed(() => userStore.selectedProject.id);
const isFull = ref(false)
const isHideOther = ref(false)
/**
* 切换中心页面全屏
*/
const handleChangePage = () => {
if (isFull.value) {
isFull.value = false;
setTimeout(() => {
isHideOther.value = false;
}, 500);
} else {
isFull.value = true;
isHideOther.value = true;
}
}
</script>
<style lang="scss" scoped>
.large-screen {
.large_screen {
width: 100vw;
height: 100vh;
background: url('@/assets/large/bg.png') no-repeat;
background-size: 100% 100%;
background-attachment: fixed;
background-color: rgba(4, 7, 17, 1);
overflow: hidden;
}
.nav {
display: flex;
gap: 15rpx;
width: 100%;
height: calc(100vh - 100px);
position: relative;
display: grid;
place-items: center;
width: calc(100vw - 30px);
height: calc(100vh - 90px);
margin: 0 auto;
box-sizing: border-box;
color: #fff;
}
.nav_left,
.nav_right {
margin: 0 15px 15px 15px;
position: absolute;
width: calc(25vw - 15px);
height: 100%;
transition: all 0.5s ease;
}
.nav_left {
top: 0;
left: 0;
}
.nav_right {
top: 0;
right: 0;
}
.nav_center {
margin-bottom: 15px;
height: 100%;
transition: all 0.5s ease;
}
/* 中间面板全屏动画 */
.full-width {
/* 取消flex增长使用固定宽度 */
width: calc(100vw - 30px);
flex: none;
}
.slide_left {
left: -25vw;
opacity: 0;
}
.slide_right {
right: -25vw;
opacity: 0;
}
</style>

View File

@ -58,9 +58,10 @@
<el-card shadow="never" class="mb8">
<el-table ref="tableRef" v-loading="loading" :data="tableData" row-key="id" border lazy default-expand-all>
<el-table-column prop="num" label="编号" />
<el-table-column prop="name" label="工程或费用名称" />
<el-table-column prop="unit" label="单位" />
<el-table-column prop="quantity" label="数量" />
<el-table-column prop="name" label="工程或费用名称" />
<el-table-column prop="unit" label="单位" align="center" />
<el-table-column prop="quantity" label="数量" align="center" />
<el-table-column prop="specification" label="规格" align="center" />
<el-table-column prop="remark" label="单价" align="center">
<template #default="scope">
<span>{{ scope.row.unitPrice }}</span>
@ -210,8 +211,6 @@ const tableRef = ref<any>();
const toggleExpandAll = () => {
isExpandAll.value = !isExpandAll.value;
console.log(isExpandAll.value);
tableData.value.forEach((row) => {
tableRef.value.toggleRowExpansion(row, isExpandAll.value);
});
@ -249,7 +248,7 @@ const handleExport = () => {
projectId: currentProject.value?.id,
sheet: queryForm.value.sheet
},
`限价一览表${queryForm.value.sheet}.xlsx`
`投标成本核算清单${queryForm.value.sheet}.xlsx`
);
};
// 审核

View File

@ -59,8 +59,9 @@
<el-table ref="tableRef" v-loading="loading" :data="tableData" row-key="id" border lazy default-expand-all>
<el-table-column prop="num" label="编号" />
<el-table-column prop="name" label="工程或费用名称" />
<el-table-column prop="unit" label="单位" />
<el-table-column prop="quantity" label="数量">
<el-table-column prop="unit" label="单位" align="center" />
<el-table-column prop="specification" label="规格" align="center"/>
<el-table-column prop="quantity" label="数量" align="center">
<template #default="scope">
{{ scope.row.children.length > 0 ? '' : scope.row.quantity }}
</template>

View File

@ -60,8 +60,10 @@
<el-table ref="tableRef" v-loading="loading" :data="tableData" row-key="id" border lazy default-expand-all>
<el-table-column prop="num" label="编号" />
<el-table-column prop="name" label="工程或费用名称" />
<el-table-column prop="unit" label="单位" />
<el-table-column prop="quantity" label="数量">
<el-table-column prop="unit" label="单位" align="center" />
<el-table-column prop="taxRate" label="税率" align="center" />
<el-table-column prop="specification" label="规格" align="center" />
<el-table-column prop="quantity" label="数量" align="center">
<template #default="scope">
{{ scope.row.children.length > 0 ? '' : scope.row.quantity }}
</template>
@ -84,7 +86,7 @@
/>
</template>
</el-table-column>
<el-table-column prop="price" label="总价" align="center">
<el-table-column prop="price" label="总价" align="center" >
<template #default="scope">
<!-- {{ scope.row.children.length > 0 ? scope.row.children.reduce((sum, child) => sum + child.price, 0) : scope.row.price }} -->
{{ scope.row.price != 0 ? Number(scope.row.price).toFixed(2) : null }}
@ -307,7 +309,7 @@ const handleExport = () => {
versions: queryForm.value.versions,
type: '1'
},
`限价一览${queryForm.value.sheet}.xlsx`
`限价一览${queryForm.value.sheet}.xlsx`
);
};
// 审批

View File

@ -93,9 +93,7 @@
</el-row>
<el-form-item label="附图">
<file-Upload v-model="form.attachmentsImg" :file-type="['pdf', 'png', 'jpg', 'jpeg', 'gif', 'bmp']">
<el-button type="primary">上传附件</el-button>
</file-Upload>
<file-Upload :fileSize="50" v-model="form.attachmentsImg" :file-type="['png', 'jpg', 'jpeg']"> </file-Upload>
</el-form-item>
<!-- 变更原因 -->
<el-form-item label="变更原因">
@ -114,7 +112,7 @@
<el-input v-model="form.content" type="textarea" :rows="6" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="附件" prop="attachments">
<file-upload v-model="form.attachments" :limit="1" :file-type="['pdf']"></file-upload>
<file-upload :fileSize="50" v-model="form.attachments" :limit="1" :file-type="['pdf']"></file-upload>
</el-form-item>
<el-form-item label="变更费用估算" prop="costEstimation">
<el-input v-model="form.costEstimation" :rows="6" type="number" placeholder="请输入变更费用估算" />

View File

@ -16,7 +16,7 @@
<!-- 表单区域 -->
<el-card class="rounded-lg shadow-sm bg-white border border-gray-100 transition-all hover:shadow-md overflow-hidden">
<div class="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-100">
<h3 class="text-lg font-semibold text-gray-800">标工程清单</h3>
<h3 class="text-lg font-semibold text-gray-800">标工程清单</h3>
</div>
<div class="p-6">
<el-form

View File

@ -113,7 +113,7 @@ const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
//按钮组件
const flowCodeOptions = [
{
value: currentProject.value?.id + '_materialsPlans',
value: currentProject.value?.id + '_equipmentList',
label: '物资设备清单审核'
}
];

View File

@ -0,0 +1,185 @@
<template>
<div class="p5" style="width: 100%; height: calc(100vh - 84px)" v-loading="loading">
<div id="TrajectoryEarth" style="width: 100%; height: 100%"></div>
</div>
</template>
<script setup name="equipmentGPS">
import { ref, onMounted, onUnmounted } from 'vue';
import { ElMessage } from 'element-plus';
import { useRoute } from 'vue-router';
import { getFootNote } from '@/api/equipment/index';
const route = useRoute();
const loading = ref(true);
let earthInstance = null;
let data = [
{
'locLongitude': 106.45637808828741,
'locLatitude': 29.5597535878972,
'locAltitude': 0
}
];
// 获取轨迹数据
const getTrajectoryData = async () => {
try {
// 从URL参数中获取clientId、projectId和userId
const { clientId, projectId, userId } = route.query;
if (!clientId || !projectId || !userId) {
ElMessage.warning('缺少必要参数,请检查传入的参数');
return;
}
loading.value = true;
const res = await getFootNote({ clientId, projectId, userId });
if (res && res.code === 200 && res.data && res.data.length > 0) {
data = res.data;
// 渲染轨迹
if (earthInstance && earthInstance.viewer) {
renderRange(data);
}
} else {
ElMessage.warning('暂无轨迹数据');
}
} catch (error) {
console.error('获取轨迹数据失败:', error);
ElMessage.error('获取轨迹数据失败,请稍后重试');
} finally {
loading.value = false;
}
};
// 创建地球
const createEarth = () => {
if (!window.YJ) {
ElMessage.error('YJ库未加载请检查依赖');
return;
}
window.YJ.on({
ws: true
// host: getIP(), // 资源所在服务器地址
// username: this.loginForm.username, // 用户名
// password: md5pass, // 密码
})
.then((res) => {
// 创建地球实例
earthInstance = new YJ.YJEarth('TrajectoryEarth');
window.Earth3 = earthInstance;
// 开启右键和左键点击事件
YJ.Global.openRightClick(window.Earth3);
YJ.Global.openLeftClick(window.Earth3);
// 设置初始视角
const view = {
position: {
lng: 102.03643298211526,
lat: 34.393586474501,
alt: 11298179.51993155
},
orientation: {
heading: 360,
pitch: -89.94481747201486,
roll: 0
}
};
YJ.Global.CesiumContainer(window.Earth3, {
compass: false //罗盘
});
// 加载底图
loadBaseMap(earthInstance.viewer);
// 可以取消注释以下代码来设置初始视角
// YJ.Global.flyTo(earthInstance, view);
// YJ.Global.setDefaultView(earthInstance.viewer, view)
// 地球创建完成后获取并渲染轨迹数据
getTrajectoryData();
})
.catch((err) => {
console.error('初始化地球失败:', err);
ElMessage.error('初始化地球失败,请稍后重试');
});
};
// 加载底图
const loadBaseMap = (viewer) => {
if (!viewer || !Cesium) {
ElMessage.error('Cesium库未加载请检查依赖');
return;
}
try {
// 创建瓦片提供器
const imageryProvider = new Cesium.UrlTemplateImageryProvider({
url: 'https://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
fileExtension: 'png',
minimumLevel: 0,
maximumLevel: 18,
projection: Cesium.WebMercatorProjection,
credit: new Cesium.Credit('卫星图数据来源')
});
// 添加图层到视图
viewer.imageryLayers.addImageryProvider(imageryProvider);
} catch (err) {
console.error('加载底图失败:', err);
ElMessage.error('加载底图失败');
}
};
// 渲染轨迹
const renderRange = (data) => {
if (!data || data.length === 0) {
ElMessage.warning('无轨迹数据可渲染');
return;
}
if (!earthInstance || !earthInstance.viewer) {
ElMessage.error('地球实例未初始化');
return;
}
try {
const positions = data.map((point) => {
return Cesium.Cartesian3.fromDegrees(point.locLongitude, point.locLatitude, point.locAltitude || 0);
});
const entity = earthInstance.viewer.entities.add({
polyline: {
positions: positions,
width: 5,
material: Cesium.Color.RED,
clampToGround: true
}
});
// 调整视角以适应轨迹
earthInstance.viewer.flyTo(entity);
} catch (err) {
console.error('渲染轨迹失败:', err);
ElMessage.error('渲染轨迹失败');
}
};
//
onMounted(() => {
createEarth();
});
//
onUnmounted(() => {
if (earthInstance) {
earthInstance.destroy();
earthInstance = null;
window.Earth3 = null;
}
});
</script>
<style></style>

View File

@ -0,0 +1,786 @@
<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="clientId">
<el-input v-model="queryParams.clientId" placeholder="请输入设备标识" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="设备名称" prop="deviceName">
<el-input v-model="queryParams.deviceName" 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="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['gps:equipment:edit']"
>修改</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['gps:equipment:remove']"
>删除</el-button
>
</el-col>
<el-col :span="1.8">
<el-button
type="primary"
plain
icon="User"
:disabled="single"
@click="handleBindUser()"
v-hasPermi="['gps:equipment:unbindManmachine', 'gps:equipment:bindManmachine']"
>绑定用户</el-button
>
</el-col>
<el-col :span="2">
<el-button type="primary" plain @click="handleViewAll">{{ viewAllButtonText }}</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="equipmentList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="项目名称" align="center" prop="projectName" />
<el-table-column label="用户名称" align="center" prop="userName" />
<el-table-column label="设备标识" align="center" prop="clientId" />
<!-- 绑定状态列 -->
<el-table-column label="绑定状态" align="center">
<template #default="scope">
<el-tag :type="scope.row.type === 1 ? 'success' : 'warning'" size="small">
{{ scope.row.type === 1 ? '已绑定' : '未绑定' }}
</el-tag>
</template>
</el-table-column>
<!-- 设备名称列 -->
<el-table-column label="设备名称" align="center" prop="deviceName" />
<el-table-column label="远程连接地址" align="center" prop="remoteAddressStr" />
<el-table-column label="连接创建时间" align="center">
<template #default="scope">
{{ formatDateTime(scope.row.creationTime) }}
</template>
</el-table-column>
<el-table-column label="最后活动时间" align="center">
<template #default="scope">
{{ formatDateTime(scope.row.lastAccessedTime) }}
</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="['gps:equipment:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['gps:equipment:remove']"></el-button>
</el-tooltip>
<el-tooltip :content="scope.row.type === 1 ? '取消绑定' : '绑定用户'" placement="top">
<el-button
link
type="primary"
icon="User"
@click="scope.row.type === 1 ? handleUnbindUser(scope.row) : handleBindUser(scope.row)"
v-hasPermi="['gps:equipment:unbindManmachine', 'gps:equipment:bindManmachine']"
>
</el-button>
</el-tooltip>
<!-- 新增跳转空页面按钮 -->
<el-tooltip content="足迹" placement="top">
<el-button
link
type="primary"
icon="Location"
v-hasPermi="['gps:equipmentSon:getList']"
@click="handleGoToEmptyPage(scope.row.userId, scope.row.projectId, scope.row.clientId)"
:disabled="!scope.row.userId || !scope.row.projectId"
></el-button>
</el-tooltip>
<el-tooltip content="历史记录" placement="top">
<el-button link type="primary" icon="Clock" @click="handleOpenHistoryUser(scope.row.clientId, scope.row.userId)"> </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>
<!-- 添加或修改GPS设备详细对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="equipmentFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="设备名称" prop="deviceName">
<el-input v-model="form.deviceName" 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="bindDialogVisible" width="500px" append-to-body>
<el-form ref="bindUserFormRef" :model="bindForm" :rules="bindRules" label-width="80px">
<!-- 绑定用户对话框 - 项目选择下拉框 -->
<el-form-item label="项目选择" prop="projectId" required>
<el-select v-model="bindForm.projectId" placeholder="请选择项目" clearable @change="handleProjectChange">
<el-option v-for="project in projectList" :key="project.projectId" :label="project.projectName" :value="project.projectId" />
</el-select>
</el-form-item>
<el-form-item label="设备名称" prop="deviceName">
<el-input v-model="bindForm.deviceName" disabled placeholder="设备名称" />
</el-form-item>
<el-form-item label="选择用户" prop="userId" required>
<el-select v-model="bindForm.userId" placeholder="请选择用户" clearable>
<el-option v-for="user in userList" :key="user.sysUserId" :label="user.userName" :value="user.sysUserId" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="bindButtonLoading" type="primary" @click="submitBindUser"> </el-button>
<el-button @click="cancelBindUser"> </el-button>
</div>
</template>
</el-dialog>
<!-- 历史用户弹窗 -->
<el-dialog title="设备历史用户" v-model="historyUserDialogVisible" width="600px" append-to-body @close="() => historyUserList.splice(0)">
<div v-loading="historyUserLoading" style="min-height: 200px; padding: 16px">
<el-table
:data="historyUserList"
stripe
size="small"
:show-header="historyUserList.length > 0"
v-if="!historyUserLoading || historyUserList.length > 0"
>
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="用户名" align="center" prop="userName" />
<el-table-column label="状态" align="center" width="120">
<template #default="scope">
<el-tag :type="scope.row.type === 0 ? 'success' : 'info'" size="small">
{{ scope.row.type === 0 ? '当前绑定' : '历史绑定' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<div v-if="!historyUserLoading && historyUserList.length === 0" style="text-align: center; padding: 50px 0">
<el-empty description="暂无该设备的历史用户数据"></el-empty>
</div>
</div>
<template #footer>
<el-button @click="historyUserDialogVisible = false">关 闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="Equipment" lang="ts">
import {
listEquipment,
getEquipment,
delEquipment,
addEquipment,
updateEquipment,
bindUser,
getUserId,
gethistroyUser,
getRemoveBind,
getProjectId
} from '@/api/equipment/index';
import { EquipmentVO, EquipmentQuery, EquipmentForm } from '@/api/equipment/types';
import { getCurrentInstance, ComponentInternalInstance, onMounted, ref, reactive, toRefs, watch, computed } from 'vue';
import { useUserStoreHook } from '@/store/modules/user';
import { useRouter } from 'vue-router'; // 新增:导入路由钩子
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const router = useRouter(); // 新增:初始化路由实例
// 扩展EquipmentVO类型添加type字段
interface ExtendedEquipmentVO extends EquipmentVO {
type: 1 | 2; // 1=已绑定2=未绑定
}
// 设备列表相关
const equipmentList = ref<ExtendedEquipmentVO[]>([]);
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 equipmentFormRef = ref<ElFormInstance>();
const bindUserFormRef = ref<ElFormInstance>();
// 项目和用户相关
const userStore = useUserStoreHook();
const currentProject = computed(() => userStore.selectedProject);
const userList = ref<any[]>([]);
const projectList = ref<any[]>([]);
const projectLoading = ref(false);
const userLoading = ref(false);
const viewAllButtonText = computed(() => {
return queryParams.value.type === 1 ? '查看未绑定项目设备' : '查看已绑定项目设备';
});
// 对话框相关
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const bindDialogVisible = ref(false);
const bindButtonLoading = ref(false);
// 历史用户弹窗相关
const historyUserDialogVisible = ref(false);
const historyUserList = ref<HistoryUserVO[]>([]);
const historyUserLoading = ref(false);
// 绑定用户表单数据
const bindForm = reactive({
id: undefined,
projectId: undefined,
userId: undefined,
deviceName: undefined
});
// 绑定用户表单验证规则
const bindRules = reactive({
projectId: [{ required: true, message: '请选择项目', trigger: 'change' }],
userId: [{ required: true, message: '请选择用户', trigger: 'change' }]
});
// 初始化表单数据
const initFormData: EquipmentForm = {
id: undefined,
projectId: undefined,
userId: undefined,
deviceName: undefined,
udp: undefined,
remoteAddressStr: undefined,
creationTime: undefined,
lastAccessedTime: undefined,
registered: undefined,
remark: undefined
};
// 页面数据
const data = reactive<PageData<EquipmentForm, EquipmentQuery>>({
form: { ...initFormData },
queryParams: {
type: 1, // 默认查询已绑定设备
pageNum: 1,
pageSize: 10,
projectId: undefined,
userId: undefined,
clientId: undefined,
deviceName: undefined,
udp: undefined,
remoteAddressStr: undefined,
creationTime: undefined,
lastAccessedTime: undefined,
registered: undefined,
params: {}
},
rules: {
deviceName: [{ required: true, message: '请输入设备名称', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 格式化日期时间 */
const formatDateTime = (timestamp: any): string => {
if (!timestamp) return '-';
let date: Date;
// 处理自定义的 yyyyMMddHHmmss 格式数字
if (typeof timestamp === 'number' && timestamp.toString().length === 14) {
const timeStr = timestamp.toString();
const year = timeStr.substring(0, 4);
const month = timeStr.substring(4, 6);
const day = timeStr.substring(6, 8);
const hours = timeStr.substring(8, 10);
const minutes = timeStr.substring(10, 12);
const seconds = timeStr.substring(12, 14);
date = new Date(`${year}-${month}-${day} ${hours}:${minutes}:${seconds}`);
}
// 处理时间戳(毫秒数)
else if (typeof timestamp === 'number') {
date = new Date(timestamp.toString().length === 10 ? timestamp * 1000 : timestamp);
}
// 处理字符串类型的时间戳或日期
else if (typeof timestamp === 'string') {
if (/^\d+$/.test(timestamp)) {
const num = parseInt(timestamp);
if (timestamp.length === 14) {
const year = timestamp.substring(0, 4);
const month = timestamp.substring(4, 6);
const day = timestamp.substring(6, 8);
const hours = timestamp.substring(8, 10);
const minutes = timestamp.substring(10, 12);
const seconds = timestamp.substring(12, 14);
date = new Date(`${year}-${month}-${day} ${hours}:${minutes}:${seconds}`);
} else {
date = new Date(timestamp.length === 10 ? num * 1000 : num);
}
} else {
date = new Date(timestamp);
}
}
// 其他情况
else {
return '-';
}
// 检查日期是否有效
if (isNaN(date.getTime())) return '-';
// 格式化日期
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
/** 获取设备列表 */
const getList = async () => {
loading.value = true;
try {
// 已绑定设备默认关联当前项目
if (queryParams.value.type === 1 && currentProject.value?.id && !queryParams.value.projectId) {
queryParams.value.projectId = currentProject.value.id;
} else if (queryParams.value.type === 2) {
// 未绑定设备清空项目筛选
queryParams.value.projectId = undefined;
}
const res = await listEquipment(queryParams.value);
equipmentList.value = res.rows as ExtendedEquipmentVO[];
total.value = res.total;
} catch (error) {
console.error('获取设备列表失败:', error);
proxy?.$modal.msgError('获取设备列表失败,请重试');
} finally {
loading.value = false;
}
};
const getProjects = async () => {
projectLoading.value = true;
try {
const res = await getProjectId();
console.log('getProjectId接口原始返回:', res);
// 从res.data中获取数组适配外层包裹的接口格式
const rawProjects = res?.data || [];
// 验证数据类型,确保是数组
if (!Array.isArray(rawProjects)) {
console.warn('接口返回的项目数据不是数组,已自动修正为空数组');
projectList.value = [];
return;
}
// 处理项目数据使用projectId和projectName
projectList.value = rawProjects
.map((project) => ({
projectId: project.projectId,
projectName: project.projectName || '未命名项目'
}))
.filter((project) => project.projectId);
console.log('处理后项目列表:', projectList.value);
} catch (error) {
console.error('获取项目列表失败:', error);
proxy?.$modal.msgError('获取项目列表失败,请重试');
} finally {
projectLoading.value = false;
}
};
/** 根据项目ID获取用户列表 */
const getUsersByProjectId = async (projectId: any) => {
if (!projectId) {
userList.value = [];
return;
}
userLoading.value = true;
try {
const res = await getUserId(projectId);
userList.value = res.data || [];
} catch (error) {
console.error('获取用户列表失败:', error);
proxy?.$modal.msgError('获取用户列表失败,请重试');
} finally {
userLoading.value = false;
}
};
/** 项目选择变化时触发 */
const handleProjectChange = (projectId: any) => {
bindForm.userId = undefined;
getUsersByProjectId(projectId);
// 仅已绑定设备需要更新项目筛选
if (queryParams.value.type === 1) {
queryParams.value.projectId = projectId;
getList();
}
};
/** 切换查看已绑定/未绑定设备 */
const handleViewAll = () => {
queryParams.value.pageNum = 1;
if (queryParams.value.type === 1) {
// 切换到未绑定设备
queryParams.value.type = 2;
proxy?.$modal.msgSuccess('已切换到查看未绑定设备模式');
} else {
// 切换到已绑定设备
queryParams.value.type = 1;
queryParams.value.projectId = currentProject.value?.id;
proxy?.$modal.msgSuccess('已切换到查看已绑定设备模式');
}
getList();
};
const handleGoToEmptyPage = (userId: any, projectId: any, clientId: any) => {
router.push({
path: './equipmentGPS',
query: {
userId: userId,
projectId: projectId,
clientId: clientId
}
});
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData, projectId: currentProject.value?.id };
equipmentFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
queryParams.value.type = 1;
queryParams.value.projectId = currentProject.value?.id;
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: ExtendedEquipmentVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 修改按钮操作 */
const handleUpdate = async (row?: ExtendedEquipmentVO) => {
reset();
const _id = row?.id || ids.value[0];
try {
const res = await getEquipment(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改GPS设备详细';
} catch (error) {
console.error('获取设备详情失败:', error);
proxy?.$modal.msgError('获取设备详情失败,请重试');
}
};
/** 提交表单 */
const submitForm = () => {
equipmentFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
if (form.value.id) {
await updateEquipment(form.value);
} else {
await addEquipment(form.value);
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
} catch (error) {
console.error('提交表单失败:', error);
proxy?.$modal.msgError('操作失败');
} finally {
buttonLoading.value = false;
}
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: ExtendedEquipmentVO) => {
const _ids = row?.id || ids.value;
try {
await proxy?.$modal.confirm('是否确认删除GPS设备详细编号为"' + _ids + '"的数据项');
await delEquipment(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
} catch (error) {
console.error('删除设备失败:', error);
proxy?.$modal.msgError('删除失败请重试');
} finally {
loading.value = false;
}
};
/** 绑定用户按钮操作 */
const handleBindUser = async (row?: EquipmentVO) => {
Object.assign(bindForm, {
id: undefined,
projectId: undefined,
userId: undefined,
clientId: row?.clientId,
deviceName: undefined
});
bindUserFormRef.value?.resetFields();
userList.value = [];
const _id = row?.id || ids.value[0];
try {
const res = await getEquipment(_id);
const equipmentData = res.data;
bindForm.id = equipmentData.id;
bindForm.deviceName = equipmentData.deviceName;
bindForm.clientId = row?.clientId || equipmentData.clientId;
if (projectList.value.length === 0) {
await getProjects();
}
if (equipmentData.projectId) {
bindForm.projectId = equipmentData.projectId;
await getUsersByProjectId(equipmentData.projectId);
} else if (currentProject.value?.id) {
bindForm.projectId = currentProject.value.id;
await getUsersByProjectId(currentProject.value.id);
}
bindDialogVisible.value = true;
} catch (error) {
console.error('获取绑定用户信息失败:', error);
proxy?.$modal.msgError('获取数据失败请重试');
}
};
/** 提交绑定用户 */
const submitBindUser = () => {
bindUserFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
if (!bindForm.clientId) {
proxy?.$modal.msgWarning('设备标识clientId不存在无法完成绑定');
bindButtonLoading.value = false;
return;
}
bindButtonLoading.value = true;
try {
const bindData = {
id: bindForm.id,
projectId: bindForm.projectId,
userId: bindForm.userId,
clientId: bindForm.clientId,
deviceName: bindForm.deviceName
};
console.log('提交绑定用户参数:', bindData);
await bindUser(bindData);
proxy?.$modal.msgSuccess('用户绑定成功');
bindDialogVisible.value = false;
await getList();
} catch (error) {
console.error('绑定用户失败:', error);
proxy?.$modal.msgError('绑定失败请重试');
} finally {
bindButtonLoading.value = false;
}
}
});
};
/** 取消绑定用户对话框 */
const cancelBindUser = () => {
bindDialogVisible.value = false;
};
/** 取消用户绑定操作 */
const handleUnbindUser = async (row: ExtendedEquipmentVO) => {
try {
if (!row) {
proxy?.$modal.msgWarning('未获取到设备信息');
return;
}
if (row.id === undefined || row.id === null || row.id === '') {
proxy?.$modal.msgWarning('设备ID不能为空');
return;
}
const deviceId = Number(row.id);
if (isNaN(deviceId) || deviceId <= 0) {
proxy?.$modal.msgWarning(`设备ID格式错误必须是正整数当前值: ${row.id}`);
return;
}
if (!row.clientId || typeof row.clientId !== 'string' || row.clientId.trim() === '') {
proxy?.$modal.msgWarning(`设备标识clientId格式错误必须是非空字符串当前值: ${row.clientId}`);
return;
}
const clientId = row.clientId.trim();
const unbindParams = {
id: deviceId,
clientId: clientId
};
await proxy?.$modal.confirm('是否确认解除该设备的用户绑定');
const response = await getRemoveBind(unbindParams);
if (response && response.code === 200) {
proxy?.$modal.msgSuccess('解除绑定成功');
await getList();
} else {
throw new Error(`接口返回非成功状态: ${JSON.stringify(response)}`);
}
} catch (error) {
console.error('解除绑定 - 错误详情:', error);
if (error === 'cancel' || (error instanceof Error && error.message.includes('cancel'))) {
console.log('用户取消了解除绑定操作');
return;
}
const errorMsg = error instanceof Error ? error.message : typeof error === 'string' ? error : '未知错误';
proxy?.$modal.msgError(`解除绑定失败: ${errorMsg}`);
}
};
/** 打开历史用户弹窗 */
const handleOpenHistoryUser = async (clientId: string | number | undefined, currentUserId: string | number | undefined) => {
if (!clientId) {
proxy?.$modal.msgWarning('设备标识clientId不存在无法获取历史用户');
return;
}
historyUserDialogVisible.value = true;
historyUserLoading.value = true;
historyUserList.value = [];
try {
const res = await gethistroyUser({
clientId: clientId,
userId: currentUserId || ''
});
const rawUserList = res.data || [];
if (currentUserId && rawUserList.length > 0) {
const currentUser = rawUserList.find((user: HistoryUserVO) => user.sysUserId === currentUserId);
const historyUsers = rawUserList.filter((user: HistoryUserVO) => user.sysUserId !== currentUserId);
historyUserList.value = currentUser ? [currentUser, ...historyUsers] : historyUsers;
} else {
historyUserList.value = rawUserList;
}
} catch (error) {
console.error('获取历史用户接口调用失败clientId%s:', clientId, error);
proxy?.$modal.msgError('获取历史用户失败请重试');
historyUserList.value = [];
} finally {
historyUserLoading.value = false;
}
};
/** 页面挂载时初始化 */
onMounted(() => {
getList();
});
/** 监听项目变化 */
const listeningProject = watch(
() => currentProject.value?.id,
(nid) => {
if (queryParams.value.type === 1) {
queryParams.value.projectId = nid;
form.value.projectId = nid;
getList();
}
}
);
/** 页面卸载时清理 */
onUnmounted(() => {
listeningProject();
});
// 类型定义
interface DialogOption {
visible: boolean;
title: string;
}
interface PageData<T, Q> {
form: T;
queryParams: Q;
rules: Record<string, any[]>;
}
interface HistoryUserVO {
sysUserId: string | number;
userName: string;
lastUseTime?: number | string;
bindTime?: number | string;
type?: number; // 1:当前绑定,其他:历史绑定
}
</script>

View File

@ -0,0 +1,90 @@
<template>
<div class="p-2">
<el-table size="small" v-if="props.row.length !== 0" :data="props.row">
<el-table-column label="材料名称" align="center" prop="materialsName" />
<!-- <el-table-column label="材料数量" align="center" prop="quantityCount" /> -->
<el-table-column label="剩余量" align="center" prop="residue">
<template #default="scope">
<span>{{ scope.row.residue }}</span>
</template>
</el-table-column>
<el-table-column label="出库数量" align="center" prop="number">
<template #default="scope">
<el-input-number v-if="scope.row.type" v-model="scope.row.number" :controls="false" :min="0" :max="scope.row.residue" :precision="0" />
<span v-else>{{ scope.row.number }}</span>
</template>
</el-table-column>
<el-table-column label="交接单位" align="center" prop="recipient">
<template #default="scope">
<el-input v-if="scope.row.type" style="width: 150px" v-model="scope.row.recipient" />
<span v-else>{{ scope.row.recipient }}</span>
</template>
</el-table-column>
<el-table-column label="领用人" align="center" prop=" shipper">
<template #default="scope">
<el-input v-if="scope.row.type" style="width: 150px" v-model="scope.row.shipper" />
<span v-else>{{ scope.row.shipper }}</span>
</template>
</el-table-column>
<el-table-column label="操作人" align="center" prop="operator">
<template #default="scope">
<el-input v-if="scope.row.type" style="width: 150px" v-model="scope.row.operator" />
<span v-else>{{ scope.row.operator }}</span>
</template>
</el-table-column>
<el-table-column label="出库时间" align="center" prop="outPutTime" width="180" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" @click="handleConfirm(scope.row)" v-if="scope.row.type"> 确认 </el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { useUserStoreHook } from '@/store/modules/user';
import { addOutbound } from '@/api/materials/materialOutbound';
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const emit = defineEmits(['success']);
const props = defineProps({
row: {
type: Array,
default: () => []
}
});
const handleConfirm = async (row: any) => {
if (row.number == 0 || !row.number) {
ElMessage.error('请输入出库数量');
return;
}
if (!row.shipper) {
ElMessage.error('请输入领用人');
return;
}
if (!row.operator) {
ElMessage.error('请输入操作人');
return;
}
console.log(row);
const params = {
...row,
materialsId: row.id,
projectId: currentProject.value?.id,
outPut: '1'
};
console.log(params);
const data = await addOutbound(params);
if (data.code === 200) {
ElMessage.success('出库成功');
emit('success');
}
};
</script>

View File

@ -0,0 +1,148 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="材料名称" prop="materialName">
<el-input v-model="queryParams.materialName" 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">
<el-table
v-loading="loading"
:data="tableList"
:row-key="
(row) => {
return row.id;
}
"
@row-click="
(row, column, event) => {
// 阻止点击行时自动展开
if (column.property) event.stopPropagation();
}
"
:preserve-expanded-content="true"
>
<el-table-column type="expand">
<template #default="{ row }">
<outbound :row="row.children ?? []" @success="getTableList" />
</template>
</el-table-column>
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="材料名称" align="center" prop="materialsName" />
<el-table-column label="规格" align="center" prop="typeSpecificationName" />
<el-table-column label="计量单位" align="center" prop="weightId" />
<el-table-column label="材料数量" align="center" prop="quantityCount" />
<el-table-column label="剩余量" align="center" prop="residue" />
<el-table-column label="操作人" align="center" prop="operator"> </el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Plus" @click="handleoutbound(scope.row)"> 新增出库 </el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getTableList"
/>
</el-card>
</div>
</template>
<script setup lang="ts">
import { outboundMaterials } from '@/api/materials/materialOutbound';
import { useUserStoreHook } from '@/store/modules/user';
import outbound from './component/outbound.vue';
import { number } from 'vue-types';
const { proxy } = getCurrentInstance() as any;
const queryFormRef = ref();
const queryParams = ref({
materialName: '',
pageNum: 1,
pageSize: 10
});
const total = ref(0);
const loading = ref(false);
const tableList = ref([]);
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
//获取列表数据
const getTableList = async () => {
try {
loading.value = true;
const res = await outboundMaterials({ ...queryParams.value, projectId: currentProject.value?.id });
if (res.code === 200) {
loading.value = false;
tableList.value = res.rows;
total.value = res.data.total;
}
} catch (error) {
loading.value = false;
}
};
getTableList();
// 出入库
const handleoutbound = (row: any) => {
console.log(row);
if (row.children == null) {
row.children = [];
}
if (row.children.some((child) => child.type === 'add')) {
ElMessage.warning('已经存在出库记录,不能重复添加');
return;
}
row.children.push({
type: 'add',
id: row.id,
number: 0,
operator: '',
shipper: '',
recipient: '',
residue: row.residue,
materialsName: row.materialsName
});
// 手动触发展开行
const table = document.querySelector('.el-table__body-wrapper');
const rows = table?.querySelectorAll('.el-table__row');
const rowEl = rows?.[Array.from(tableList.value).indexOf(row)];
const expandBtn: any = rowEl?.querySelector('.el-table__expand-icon');
// 如果行未展开,则点击展开按钮
if (expandBtn && !expandBtn.classList.contains('el-table__expand-icon--expanded')) {
expandBtn.click();
}
};
//搜索
const handleQuery = () => {
queryParams.value.pageNum = 1;
getTableList();
};
//重置
const resetQuery = () => {
console.log(111111111);
queryFormRef.value?.resetFields();
handleQuery();
};
</script>
<style scoped></style>

View File

@ -27,19 +27,6 @@
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['materials:materials:add']"> 新增 </el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['materials:materials:edit']"
>修改
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['materials:materials:remove']"
>删除
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['materials:materials:export']">导出 </el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
@ -92,48 +79,57 @@
<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-dialog draggable :title="dialog.title" v-model="dialog.visible" width="800px" append-to-body>
<el-form ref="materialsFormRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="材料名称" prop="materialsName">
<el-input v-model="form.materialsName" placeholder="请输入材料名称" />
</el-form-item>
<el-form-item label="规格型号名称" prop="typeSpecificationName">
<el-input v-model="form.typeSpecificationName" placeholder="请输入规格型号名称" />
</el-form-item>
<el-form-item label="材料供应商" prop="companyId">
<el-select v-model="form.companyId" clearable placeholder="请选择材料提供商">
<el-option v-for="item in companyOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="使用部位" prop="usePart">
<el-input v-model="form.usePart" placeholder="请输入使用部位" />
</el-form-item>
<el-form-item label="计量单位" prop="weightId">
<el-input v-model="form.weightId" placeholder="请输入计量单位" />
</el-form-item>
<el-form-item label="预计材料数量" prop="quantityCount">
<el-input v-model="form.quantityCount" placeholder="请输入预计材料数量" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
<el-form-item label="材料文件" prop="fileOssIdMap">
<div :key="item.value" v-for="item in materials_file_type">
<h3>{{ item.label }}</h3>
<file-upload
v-model="ossIdMap[item.value]"
:limit="1"
:file-size="50"
:file-type="['pdf']"
@update:model-value="
(args) => {
handleOssUpdate(args, item.value);
}
"
/>
</div>
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item label="材料名称" prop="materialsName"> <el-input v-model="form.materialsName" placeholder="请输入材料名称" /> </el-form-item
></el-col>
<el-col :span="12">
<el-form-item label="规格型号名称" prop="typeSpecificationName">
<el-input v-model="form.typeSpecificationName" placeholder="请输入规格型号名称" /> </el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="材料提供商" prop="companyId">
<el-select v-model="form.companyId" clearable placeholder="请选择材料提供商">
<el-option v-for="item in companyOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-form-item
></el-col>
<el-col :span="12">
<el-form-item label="使用部位" prop="usePart"> <el-input v-model="form.usePart" placeholder="请输入使用部位" /> </el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="计量单位" prop="weightId"> <el-input v-model="form.weightId" placeholder="请输入计量单位" /> </el-form-item
></el-col>
<el-col :span="12">
<el-form-item label="预计材料数量" prop="quantityCount">
<el-input v-model="form.quantityCount" type="number" min="0" placeholder="请输入预计材料数量" /> </el-form-item
></el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark"> <el-input type="textarea" v-model="form.remark" placeholder="请输入备注" /> </el-form-item
></el-col>
<el-col :span="12" :key="item.value" v-for="item in materials_file_type">
<el-form-item label="材料文件" prop="fileOssIdMap">
<div>
<h3>{{ item.label }}</h3>
<file-upload
:isShowTip="false"
v-model="ossIdMap[item.value]"
:limit="1"
:file-size="50"
:file-type="['pdf']"
@update:model-value="
(args) => {
handleOssUpdate(args, item.value);
}
"
/>
</div> </el-form-item
></el-col>
<el-col :span="24" style="color: rgb(237 70 61)">注意:请上传pdf格式文件</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
@ -154,7 +150,7 @@ import { MaterialsForm, MaterialsQuery, MaterialsVO } from '@/api/materials/mate
import { useUserStoreHook } from '@/store/modules/user';
import MaterialsInventoryTable from '@/views/materials/materials/component/MaterialsInventoryTable.vue';
import MaterialsInventoryAddDialog from '@/views/materials/materials/component/MaterialsInventoryAddDialog.vue';
import { listCompany } from '@/api/materials/company';
import { listCompany, supplierInputGet } from '@/api/materials/company';
import { CompanyVO } from '@/api/materials/company/types';
import MaterialsDetailDrawer from '@/views/materials/materials/component/MaterialsDetailDrawer.vue';
@ -230,14 +226,14 @@ const getList = async () => {
/** 获取当前项目下的公司列表 */
const getCompanyList = async () => {
loading.value = true;
const companyRes = await listCompany({
pageNum: 1,
pageSize: 1000,
const companyRes = await supplierInputGet({
projectId: currentProject.value?.id
});
companyOptions.value = companyRes.rows.map((company: CompanyVO) => ({
console.log(companyRes);
companyOptions.value = companyRes.data.map((company) => ({
value: company.id,
label: company.companyName
label: company.supplierName
}));
loading.value = false;
};
@ -366,6 +362,7 @@ const listeningProject = watch(
(nid, oid) => {
queryParams.value.projectId = nid;
form.value.projectId = nid;
getCompanyList();
getList();
}
);

View File

@ -1,76 +1,101 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave"> </transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="使用部位" prop="usePart">
<el-input v-model="queryParams.usePart" 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>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-row :gutter="10" class="mb8">
<el-col :span="4">
<el-card shadow="never">
<el-tree style="max-width: 600px" :data="TreeData" :props="defaultProps" @node-click="handleNodeClick" />
</el-card>
</el-col>
<!-- 外层表格添加ref用于控制展开状态 -->
<el-table ref="outerTableRef" v-loading="loading" :data="materialsUseInventoryList" @expand-change="handleExpandChange" border>
<el-table-column type="expand">
<template #default="props">
<div style="margin-left: 60px">
<el-table :data="materialsUseRecordList" border v-loading="loadingChild">
<el-table-column label="序号" align="center" type="index" width="60" />
<el-table-column label="使用数量" align="center" prop="useNumber" />
<el-table-column label="剩余量" align="center" prop="residueNumber" />
<el-table-column label="使用部位" align="center" prop="usePart" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
link
type="primary"
icon="delete"
v-if="scope.row.ishow"
@click="handleDelete(scope.row)"
v-hasPermi="['materials:materialsUseRecord:remove']"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<pagination
v-show="totalChild > 0"
:total="totalChild"
v-model:page="queryParamsChild.pageNum"
v-model:limit="queryParamsChild.pageSize"
@pagination="getListChild"
/>
</div>
<el-col :span="20">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave"> </transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="物资名称" prop="materialsName">
<el-input v-model="queryParams.materialsName" 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>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
</el-table-column>
<el-table-column label="序号" align="center" type="index" width="60" />
<el-table-column label="物资名称" align="center" prop="materialsName" />
<el-table-column label="交接单位" align="center" prop="recipient" />
<el-table-column label="计划数量" align="center" prop="quantityCount" />
<el-table-column label="数量" align="center" prop="number" />
<el-table-column label="出库人" align="center" prop="operator" />
<el-table-column label="领用人" align="center" prop="shipper" />
<el-table-column label="剩余量" align="center" prop="residue" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['materials:materialsUseRecord:add']"
>添加登记</el-button
>
</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>
<!-- 外层表格使用expandedRowKeys控制展开状态 -->
<el-table
ref="outerTableRef"
v-loading="loading"
:data="materialsUseInventoryList"
@expand-change="handleExpandChange"
border
:expanded-row-keys="expandedRowKeys"
>
<el-table-column type="expand">
<template #default="props">
<div style="margin-left: 60px">
<!-- 子表格使用当前行的独立数据 -->
<el-table :data="getChildData(props.row.id)" border v-loading="getChildLoading(props.row.id)">
<el-table-column label="序号" align="center" type="index" width="60" />
<el-table-column label="使用数量" align="center" prop="useNumber" />
<el-table-column label="剩余量" align="center" prop="residueNumber" />
<el-table-column label="使用部位" align="center" prop="usePart" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
link
type="primary"
icon="delete"
v-if="scope.row.ishow"
@click="handleDelete(scope.row, props.row.id)"
v-hasPermi="['materials:materialsUseRecord:remove']"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<!-- 子分页使用当前行的独立分页参数 -->
<pagination
v-show="getChildTotal(props.row.id) > 0"
:total="getChildTotal(props.row.id)"
v-model:page="getChildQueryParams(props.row.id).pageNum"
v-model:limit="getChildQueryParams(props.row.id).pageSize"
@pagination="(page, limit) => handleChildPagination(props.row.id, page, limit)"
/>
</div>
</template>
</el-table-column>
<el-table-column label="序号" align="center" type="index" width="60" />
<el-table-column label="物资名称" align="center" prop="materialsName" />
<el-table-column label="交接单位" align="center" prop="recipient" />
<el-table-column label="计划数量" align="center" prop="quantityCount" />
<el-table-column label="数量" align="center" prop="number" />
<el-table-column label="出库人" align="center" prop="operator" />
<el-table-column label="领用人" align="center" prop="shipper" />
<el-table-column label="剩余量" align="center" prop="residue" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['materials:materialsUseRecord:add']"
>添加登记</el-button
>
</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-col>
</el-row>
<!-- 添加或修改材料使用登记对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="materialsUseRecordFormRef" :model="form" :rules="rules" label-width="80px">
@ -104,39 +129,53 @@ import {
updateMaterialsUseRecord
} from '@/api/materials/materialsUseRecord';
import { MaterialsUseRecordVO, MaterialsUseRecordQuery, MaterialsUseRecordForm } from '@/api/materials/materialsUseRecord/types';
import { getCurrentInstance, ComponentInternalInstance, onMounted, ref, reactive, toRefs, computed } from 'vue';
import { getCurrentInstance, ComponentInternalInstance, onMounted, ref, reactive, toRefs, computed, watch, WatchStopHandle } from 'vue';
import { ElFormInstance, ElTable } from 'element-plus';
import useUserStore from '@/store/modules/user';
import { get } from 'lodash';
import { getMaterialsList } from '@/api/materials/materialOutbound';
// 类型定义补充若项目中无全局DialogOption类型需添加
// 类型定义补充
interface DialogOption {
visible: boolean;
title: string;
}
// 子列表状态接口:为每个父行存储独立数据
interface ChildRowState {
list: MaterialsUseRecordVO[];
queryParams: MaterialsUseRecordQuery & {
pageNum: number;
pageSize: number;
inventoryId: number;
projectId?: number;
};
loading: boolean;
total: number;
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const userStore = useUserStore();
const currentProject = computed(() => userStore.selectedProject);
// 核心数据响应式定义
const materialsUseRecordList = ref<MaterialsUseRecordVO[]>([]);
const materialsUseInventoryList = ref<any[]>([]); // 外层列表数据类型可根据实际接口返回调整
const materialsUseInventoryList = ref<any[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const loadingChild = 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 totalChild = ref(0);
const expandedRowKeys = ref<number[]>([]); // 控制展开行的ID集合
// 子列表状态管理使用对象存储不同父行的子数据key为inventoryId
const childRowStates = ref<Record<number, ChildRowState>>({});
// 组件Ref定义
const queryFormRef = ref<ElFormInstance>();
const materialsUseRecordFormRef = ref<ElFormInstance>();
const outerTableRef = ref<InstanceType<typeof ElTable> | undefined>(undefined); // 外层表格Ref控制展开
const currentExpandInventoryId = ref<number | undefined>(undefined); // 存储当前展开行的inventoryId
const outerTableRef = ref<InstanceType<typeof ElTable> | undefined>(undefined);
const currentOperateInventoryId = ref<number | undefined>(undefined); // 当前操作的父行ID
// 对话框与表单数据
const dialog = reactive<DialogOption>({
@ -160,72 +199,159 @@ const data = reactive({
pageSize: 10,
projectId: currentProject.value?.id,
outPut: 1,
usePart: undefined // 补充usePart字段与表单prop对应
} as MaterialsUseRecordQuery & { outPut?: number; usePart?: string },
queryParamsChild: {
pageNum: 1,
pageSize: 10,
inventoryId: undefined,
materialsName: undefined,
usePart: undefined,
useNumber: undefined,
residueNumber: undefined
} as MaterialsUseRecordQuery,
materialsId: ''
} as MaterialsUseRecordQuery & {
outPut?: number;
materialsName?: string;
usePart?: string;
},
rules: {
id: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
projectId: [{ required: true, message: '项目ID不能为空', trigger: 'blur' }],
inventoryId: [{ required: true, message: '库存ID不能为空', trigger: 'blur' }],
usePart: [{ required: true, message: '使用部位不能为空', trigger: 'blur' }],
useNumber: [{ required: true, message: '使用数量不能为空', trigger: 'blur' }],
residueNumber: [{ required: true, message: '剩余量不能为空', trigger: 'blur' }]
useNumber: [{ required: true, message: '使用数量不能为空', trigger: 'blur' }]
}
});
const { queryParams, form, rules, queryParamsChild } = toRefs(data);
const { form, rules } = toRefs(data);
const queryParams = ref<typeof data.queryParams>({ ...data.queryParams });
const TreeData: any = ref([]);
//获取材料列表
const getMaterialsListData = async () => {
const res = await getMaterialsList({ projectId: currentProject.value?.id, pageNum: 1, pageSize: 999 });
if (res.code === 200) {
// 将数据转换为树形结构
TreeData.value = res.rows.map((item) => ({
...item,
label: item.materialsName + '_' + item.createTime,
children: []
}));
queryParams.value.materialsId = TreeData.value[0].id;
getList();
}
};
/** 查询材料使用登记列表(外层列表) */
const handleNodeClick = (data: any) => {
console.log(data);
queryParams.value.materialsId = data.id;
getList();
};
const defaultProps = {
children: 'children',
label: 'label'
};
// ------------------------------ 子列表状态工具函数 ------------------------------
/** 获取或初始化指定父行的子列表状态 */
const getOrInitChildState = (inventoryId: number): ChildRowState => {
if (!childRowStates.value[inventoryId]) {
childRowStates.value[inventoryId] = {
list: [],
loading: false,
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10,
inventoryId,
projectId: currentProject.value?.id
}
};
}
return childRowStates.value[inventoryId];
};
/** 获取指定父行的子列表数据 */
const getChildData = (inventoryId: number): MaterialsUseRecordVO[] => {
return getOrInitChildState(inventoryId).list;
};
/** 获取指定父行的子列表加载状态 */
const getChildLoading = (inventoryId: number): boolean => {
return getOrInitChildState(inventoryId).loading;
};
/** 获取指定父行的子列表总条数 */
const getChildTotal = (inventoryId: number): number => {
return getOrInitChildState(inventoryId).total;
};
/** 获取指定父行的子分页参数 */
const getChildQueryParams = (inventoryId: number) => {
return getOrInitChildState(inventoryId).queryParams;
};
// ------------------------------ 核心业务逻辑 ------------------------------
/** 查询外层列表数据 */
const getList = async () => {
loading.value = true;
try {
const res = await listMaterialsUseInventory(queryParams.value);
materialsUseInventoryList.value = res.rows;
total.value = res.total;
// 保持已展开行的状态
if (expandedRowKeys.value.length > 0 && outerTableRef.value) {
expandedRowKeys.value.forEach((id) => {
const targetRow = materialsUseInventoryList.value.find((item) => item.id === id);
targetRow && outerTableRef.value!.toggleRowExpansion(targetRow, true);
});
}
} finally {
loading.value = false;
}
};
/** 处理外层表格展开/折叠记录当前展开行的inventoryId */
const handleExpandChange = (row: any) => {
currentExpandInventoryId.value = row.id; // 记录展开行ID
queryParamsChild.value.inventoryId = row.id;
getListChild();
/** 处理外层表格展开/折叠 */
const handleExpandChange = async (row: any, expandedRows: any[]) => {
const inventoryId = row.id;
// 更新展开行ID集合
expandedRowKeys.value = expandedRows.map((item) => item.id);
// 展开时加载子数据(仅首次加载)
const childState = getOrInitChildState(inventoryId);
if (expandedRows.some((item) => item.id === inventoryId) && childState.list.length === 0) {
await getListChild(inventoryId);
}
};
/** 查询材料子级登记列表(内层列表) */
const getListChild = async () => {
loadingChild.value = true;
/** 查询指定父行的子列表数据 */
const getListChild = async (inventoryId: number) => {
const childState = getOrInitChildState(inventoryId);
childState.loading = true;
try {
const res = await listMaterialsUseRecord(queryParamsChild.value);
materialsUseRecordList.value = res.rows;
const res = await listMaterialsUseRecord(childState.queryParams);
childState.list = res.rows;
// 控制首行删除按钮显示
if (res.rows.length > 0) {
materialsUseRecordList.value[0].ishow = true;
if (childState.list.length > 0) {
childState.list[0].ishow = true;
}
totalChild.value = res.total;
childState.total = res.total;
} finally {
loadingChild.value = false;
childState.loading = false;
}
};
/** 处理子列表分页变化 */
const handleChildPagination = async (inventoryId: number, page: number, limit: number) => {
const childState = getOrInitChildState(inventoryId);
childState.queryParams.pageNum = page;
childState.queryParams.pageSize = limit;
await getListChild(inventoryId);
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
currentOperateInventoryId.value = undefined;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
form.value = { ...initFormData, projectId: currentProject.value?.id };
materialsUseRecordFormRef.value?.resetFields();
};
@ -238,10 +364,16 @@ const handleQuery = () => {
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
queryParams.value = {
...data.queryParams,
pageNum: 1,
pageSize: 10,
projectId: currentProject.value?.id
};
handleQuery();
};
/** 多选框选中数据(当前模板未使用,保留原逻辑) */
/** 多选框选中数据处理 */
const handleSelectionChange = (selection: MaterialsUseRecordVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length !== 1;
@ -252,76 +384,87 @@ const handleSelectionChange = (selection: MaterialsUseRecordVO[]) => {
const handleAdd = async (row: any) => {
reset();
form.value.inventoryId = row.id;
currentOperateInventoryId.value = row.id;
dialog.visible = true;
dialog.title = '添加材料使用登记';
};
/** 修改按钮操作(当前模板未使用,保留原逻辑) */
/** 修改按钮操作 */
const handleUpdate = async (row?: MaterialsUseRecordVO) => {
if (!row && ids.value.length === 0) return;
reset();
const _id = row?.id || ids.value[0];
const res = await getMaterialsUseRecord(_id);
Object.assign(form.value, res.data);
currentOperateInventoryId.value = form.value.inventoryId;
dialog.visible = true;
dialog.title = '修改材料使用登记';
};
/** 提交按钮:核心优化逻辑(刷新外层列表+保留展开状态) */
/** 提交表单操作 */
const submitForm = () => {
materialsUseRecordFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
// 1. 执行添加/修改接口请求
await addMaterialsUseRecord(form.value);
// 2. 刷新外层列表(确保外层数据同步,如剩余量)
await getList();
// 3. 刷新当前展开行的子列表
await getListChild();
// 4. 恢复展开状态根据记录的inventoryId重新展开行
if (currentExpandInventoryId.value && outerTableRef.value) {
const targetRow = materialsUseInventoryList.value.find((item) => item.id === currentExpandInventoryId.value);
if (targetRow) {
outerTableRef.value.toggleRowExpansion(targetRow, true);
}
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
} finally {
buttonLoading.value = false;
}
if (!valid || !currentOperateInventoryId.value) return;
buttonLoading.value = true;
try {
// 执行添加或修改操作
const apiFn = form.value.id ? updateMaterialsUseRecord : addMaterialsUseRecord;
await apiFn(form.value);
// 刷新外层列表和当前操作行的子列表
await getList();
await getListChild(currentOperateInventoryId.value);
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
} finally {
buttonLoading.value = false;
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: MaterialsUseRecordVO) => {
const _ids = row?.id || ids.value;
/** 删除操作 */
const handleDelete = async (row: MaterialsUseRecordVO, inventoryId: number) => {
try {
await proxy?.$modal.confirm(`是否确认删除材料使用登记编号为"${_ids}"的数据项`);
await delMaterialsUseRecord(_ids);
await proxy?.$modal.confirm(`是否确认删除该记录`);
await delMaterialsUseRecord(row.id);
await getList();
await getListChild();
if (currentExpandInventoryId.value && outerTableRef.value) {
const targetRow = materialsUseInventoryList.value.find((item) => item.id === currentExpandInventoryId.value);
if (targetRow) {
outerTableRef.value.toggleRowExpansion(targetRow, true);
}
}
await getListChild(inventoryId);
proxy?.$modal.msgSuccess('删除成功');
await getListChild();
} finally {
loading.value = false;
} catch (error) {
// 取消删除时不做处理
}
};
/** 导出按钮操作(保留原逻辑) */
/** 导出操作 */
const handleExport = () => {
proxy?.download('materials/materialsUseRecord/export', { ...queryParams.value }, `materialsUseRecord_${new Date().getTime()}.xlsx`);
};
/** 页面挂载时加载外层列表 */
// 监听项目ID变化刷新数据
const listeningProject: WatchStopHandle = watch(
() => currentProject.value?.id,
(newId) => {
queryParams.value.projectId = newId;
form.value.projectId = newId;
// 更新所有子列表的项目ID
Object.values(childRowStates.value).forEach((state) => {
state.queryParams.projectId = newId;
});
getList();
}
);
// 页面卸载时清理监听
onUnmounted(() => {
listeningProject();
});
// 页面挂载时加载数据
onMounted(() => {
getList();
// getList();
getMaterialsListData();
});
</script>

View File

@ -0,0 +1,619 @@
<template>
<div class="overall-plan-material-supply">
<el-card shadow="always">
<template #header>
<el-row :gutter="10" class="mb8">
<el-form :inline="true">
<el-form-item v-if="state.masterData.status == 'draft'">
<el-button type="primary" icon="edit" @click="clickApprovalSheet1()">审批</el-button>
</el-form-item>
<el-form-item v-if="state.masterData.status == 'waiting' || state.masterData.status == 'finish'">
<el-button icon="view" @click="lookApprovalFlow()" type="warning">查看流程</el-button>
</el-form-item>
<el-form-item>
<file-upload
upload-url="/design/totalsupplyplan/import"
v-model="file"
:limit="1"
:file-type="['xls', 'xlsx']"
:on-upload-success="handleSuccess"
>
<el-button :disabled="state.masterData.status == 'finish'" type="primary" plain icon="upload">导入</el-button>
</file-upload>
</el-form-item>
<el-form-item>
<el-button type="primary" plain icon="Download" @click="handleExport">导出</el-button>
</el-form-item>
<el-form-item>
<el-button icon="Finished" @click="editApprovalSheet()" type="success" :disabled="state.masterData.status == 'finish'"
>一键全部保存</el-button
>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="toggleExpandAll">{{ isExpandAll ? '一键收起' : '一键展开' }}</el-button>
</el-form-item>
</el-form>
<right-toolbar @queryTable="getMasterDataList"></right-toolbar>
</el-row>
</template>
</el-card>
<!-- 本地数据懒加载表格 -->
<el-table
:data="state.tableData"
v-loading="state.loading.list"
ref="tableRef"
stripe
style="width: 100%; margin-bottom: 20px; height: calc(100vh - 230px)"
row-key="id"
border
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:lazy="true"
:load="loadLocalChildNodes"
@expand-change="handleExpandChange"
>
<el-table-column align="center" prop="num" label="编号" />
<el-table-column prop="name" label="工程或费用名称" width="180" />
<el-table-column prop="unit" label="单位" />
<el-table-column prop="specification" label="规格型号" width="80" />
<el-table-column prop="quantity" label="数量" width="100" />
<el-table-column prop="batchNumber" label="批次号" width="200" />
<!-- 优化的输入框列 -->
<el-table-column prop="brand" label="品牌">
<template #default="{ row }">
<el-input
v-model.lazy="row.brand"
:disabled="state.masterData.status != 'draft'"
v-if="!row.hasChildren"
placeholder=""
clearable
:key="`brand-${row.id}`"
@change="handleInputChange(row, 'brand')"
/>
</template>
</el-table-column>
<el-table-column prop="texture" label="材质">
<template #default="{ row }">
<el-input
v-model.lazy="row.texture"
:disabled="state.masterData.status != 'draft'"
v-if="!row.hasChildren"
placeholder=""
clearable
:key="`texture-${row.id}`"
@change="handleInputChange(row, 'texture')"
/>
</template>
</el-table-column>
<el-table-column prop="qualityStandard" label="质量标准">
<template #default="{ row }">
<el-input
:disabled="state.masterData.status != 'draft'"
v-model.lazy="row.qualityStandard"
v-if="!row.hasChildren"
placeholder=""
clearable
:key="`qualityStandard-${row.id}`"
@change="handleInputChange(row, 'qualityStandard')"
/>
</template>
</el-table-column>
<el-table-column prop="partUsed" label="使用部位">
<template #default="{ row }">
<el-input
:disabled="state.masterData.status != 'draft'"
v-model.lazy="row.partUsed"
v-if="!row.hasChildren"
placeholder=""
clearable
:key="`partUsed-${row.id}`"
@change="handleInputChange(row, 'partUsed')"
/>
</template>
</el-table-column>
<el-table-column prop="deliveryPoints" label="交货地点">
<template #default="{ row }">
<el-input
:disabled="state.masterData.status != 'draft'"
v-model.lazy="row.deliveryPoints"
v-if="!row.hasChildren"
placeholder=""
clearable
:key="`deliveryPoints-${row.id}`"
@change="handleInputChange(row, 'deliveryPoints')"
/>
</template>
</el-table-column>
<el-table-column label="预计使用日期" width="150">
<template #default="{ row }">
<el-date-picker
v-model="row.dateService"
v-if="!row.hasChildren"
type="date"
value-format="YYYY-MM-DD"
:disabled="state.masterData.status != 'draft'"
placeholder="请选择日期"
:key="`dateService-${row.id}`"
@change="handleInputChange(row, 'dateService')"
/>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" />
</el-table>
<!-- 编辑弹窗 -->
<el-dialog v-model="visible" title="修改物料信息" :width="800" :close-on-click-modal="false" @close="handleClose">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px" class="space-y-4">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="批次号" prop="batchNumber">
<el-input disabled v-model="formData.batchNumber" placeholder="请输入批次号" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="品牌" prop="brand">
<el-input v-model="formData.brand" placeholder="请输入品牌" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="材质" prop="texture">
<el-input v-model="formData.texture" placeholder="请输入材质" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="质量标准" prop="qualityStandard">
<el-input v-model="formData.qualityStandard" placeholder="请输入质量标准" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="使用部位" prop="partUsed">
<el-input v-model="formData.partUsed" placeholder="请输入使用部位" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="交货地点" prop="deliveryPoints">
<el-input v-model="formData.deliveryPoints" placeholder="请输入交货地点" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="预计使用日期" prop="dateService">
<el-date-picker v-model="formData.dateService" type="date" placeholder="选择预计使用日期" format="YYYY-MM-DD" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注信息" type="textarea" rows="3" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="billofQuantities">
import { ref, reactive, onMounted, computed, getCurrentInstance, watch, onUnmounted } from 'vue';
import { ElMessage, ElLoading } from 'element-plus';
import {
obtainMasterDataList,
totalsupplyplan,
totalSupplyplanDetails,
materialChangeSupplyplan,
totalSupplyplanBatchEdit
} from '@/api/materials/overallPlanMaterialSupply/index';
import { useUserStoreHook } from '@/store/modules/user';
// 全局状态与实例获取
const userStore = useUserStoreHook();
const currentProject = computed(() => userStore.selectedProject);
const { proxy } = getCurrentInstance();
// 基础状态管理
const visible = ref(false);
const formRef = ref(null);
const tableRef = ref(null);
const file = ref(null);
const isExpandAll = ref(false);
const expandRowKeys = ref([]);
const editDataMap = ref(new Map()); // 用Map存储修改的数据提高查询效率
const loadingInstance = ref(null);
const fullTreeData = ref([]); // 存储完整树形数据,用于本地懒加载
// 页面核心状态
const state = reactive({
tableData: [], // 初始只存放根节点
queryForm: {
projectId: currentProject.value?.id,
versions: '',
sheet: '',
pageSize: 20,
pageNum: 1
},
loading: {
versions: false,
sheets: false,
list: false
},
masterData: {}
});
// 表单数据
const formData = reactive({
batchNumber: '',
brand: '',
compileDate: '',
dateService: '',
deliveryPoints: '',
id: undefined,
name: '',
num: '',
partUsed: '',
planNumber: '',
projectId: undefined,
qualityStandard: '',
quantity: 0,
remark: '',
specification: '',
status: '',
texture: '',
unit: ''
});
// 表单验证规则
const formRules = reactive({
name: [
{ required: true, message: '请输入名称', trigger: 'blur' },
{ max: 100, message: '名称长度不能超过100个字符', trigger: 'blur' }
],
num: [
{ required: true, message: '请输入编号', trigger: 'blur' },
{ max: 50, message: '编号长度不能超过50个字符', trigger: 'blur' }
],
quantity: [
{ required: true, message: '请输入数量', trigger: 'blur' },
{ type: 'number', min: 0, message: '数量不能为负数', trigger: 'blur' }
],
compileDate: [{ required: true, message: '请选择编制日期', trigger: 'change' }]
});
/**
* 本地数据懒加载实现:从完整数据中筛选子节点
*/
const loadLocalChildNodes = (row, treeNode, resolve) => {
// 避免重复加载
if (row.children && row.children.length > 0) {
resolve(row.children);
return;
}
// 显示加载状态
loadingInstance.value = ElLoading.service({
target: tableRef.value.$el,
text: '加载子节点中...',
background: 'rgba(255, 255, 255, 0.8)'
});
try {
// 从完整数据中筛选当前行的子节点假设父ID对应子节点的pid
const childNodes = fullTreeData.value.filter((node) => node.pid === row.id);
// 标记子节点是否有子节点
const formattedChildren = childNodes.map((node) => ({
...node,
hasChildren: fullTreeData.value.some((child) => child.pid === node.id)
}));
// 缓存子节点到当前行
row.children = formattedChildren;
resolve(formattedChildren);
} catch (error) {
ElMessage.error(`加载子节点异常:${error.message}`);
resolve([]);
} finally {
loadingInstance.value?.close();
}
};
/**
* 处理展开状态变化
*/
const handleExpandChange = (row, expanded) => {
if (expanded) {
if (!expandRowKeys.value.includes(row.id)) {
expandRowKeys.value.push(row.id);
}
} else {
expandRowKeys.value = expandRowKeys.value.filter((id) => id !== row.id);
}
};
/**
* 一键展开/收起
*/
const toggleExpandAll = async () => {
isExpandAll.value = !isExpandAll.value;
if (isExpandAll.value) {
loadingInstance.value = ElLoading.service({
target: tableRef.value.$el,
text: '正在加载所有节点...',
background: 'rgba(255, 255, 255, 0.8)'
});
try {
// 递归加载所有层级子节点
const loadAllChildren = (nodes) => {
nodes.forEach((node) => {
const children = fullTreeData.value.filter((child) => child.sid === node.id);
const formattedChildren = children.map((child) => ({
...child,
hasChildren: fullTreeData.value.some((c) => c.sid === child.id)
}));
node.children = formattedChildren;
if (formattedChildren.length > 0) {
loadAllChildren(formattedChildren);
}
});
};
loadAllChildren(state.tableData);
// 展开所有节点
state.tableData.forEach((row) => {
tableRef.value.toggleRowExpansion(row, true);
});
ElMessage.success('所有节点已展开');
} catch (error) {
ElMessage.error(`一键展开失败:${error.message}`);
} finally {
loadingInstance.value?.close();
}
} else {
// 收起所有节点
state.tableData.forEach((row) => {
tableRef.value.toggleRowExpansion(row, false);
});
expandRowKeys.value = [];
}
};
/**
* 处理输入框变化(优化性能)
*/
const handleInputChange = (row, field) => {
// 只记录修改过的数据,避免全量对比
if (!editDataMap.value.has(row.id)) {
editDataMap.value.set(row.id, { ...row });
}
const storedData = editDataMap.value.get(row.id);
storedData[field] = row[field];
editDataMap.value.set(row.id, storedData);
};
/**
* 获取主数据列表(核心优化:拆分根节点和完整数据)
*/
async function getMasterDataList() {
try {
state.loading.list = true;
// 获取主数据
const masterDataRes = await obtainMasterDataList({
projectId: currentProject.value?.id
});
const { data: masterData } = masterDataRes;
if (!masterData[0]?.id) {
state.tableData = [];
fullTreeData.value = [];
return;
}
state.masterData = masterData[0];
// 获取完整供应计划数据
const supplyPlanRes = await totalsupplyplan({ id: masterData[0].id });
const allData = supplyPlanRes.rows || [];
// 存储完整数据到本地
fullTreeData.value = [...allData];
// 初始只加载根节点pid为空或0的节点
const rootNodes = allData.filter((item) => !item.pid || item.pid === 0);
// 标记根节点是否有子节点
state.tableData = rootNodes.map((node) => ({
...node,
hasChildren: allData.some((child) => child.pid === node.id)
}));
// 初始化可编辑数据映射
editDataMap.value.clear();
allData.forEach((item) => {
if (!item.hasChildren) {
// 只关注叶子节点
editDataMap.value.set(item.id, { ...item });
}
});
} catch (error) {
ElMessage.error(`数据加载失败:${error.message}`);
state.tableData = [];
fullTreeData.value = [];
} finally {
state.loading.list = false;
}
}
/**
* 获取详情
*/
async function totalSupplyplanDetail(id) {
try {
const result = await totalSupplyplanDetails(id);
if (result?.code === 200) {
const detailData = result.data || {};
// 清空表单并赋值
Object.keys(formData).forEach((key) => {
formData[key] = undefined;
});
// 处理日期格式
const formatDate = (date) => {
if (!date) return '';
const d = typeof date === 'string' ? new Date(date) : date;
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
};
Object.assign(formData, {
...detailData,
compileDate: formatDate(detailData.compileDate),
dateService: formatDate(detailData.dateService)
});
} else {
ElMessage.error(`获取详情失败: ${result?.msg || '未知错误'}`);
}
} catch (err) {
ElMessage.error(`接口请求失败: ${err.message}`);
console.error('详情接口错误:', err);
} finally {
state.loading.list = false;
}
}
/**
* 一键保存修改
*/
const editApprovalSheet = async () => {
state.loading.list = true;
try {
// 只提交修改过的数据从Map中获取
const modifiedData = Array.from(editDataMap.value.values());
if (modifiedData.length === 0) {
ElMessage.info('没有数据需要修改');
return;
}
await totalSupplyplanBatchEdit(modifiedData);
ElMessage.success('修改成功');
getMasterDataList(); // 重新加载数据
} catch (error) {
ElMessage.error(`保存失败:${error.message}`);
} finally {
state.loading.list = false;
}
};
/**
* 提交表单
*/
const handleSubmit = async () => {
try {
await formRef.value.validate();
await materialChangeSupplyplan(formData);
ElMessage.success('修改成功');
handleClose();
getMasterDataList();
} catch (error) {
console.error('表单验证失败:', error);
}
};
/**
* 导出数据
*/
const handleExport = async () => {
proxy?.download('/design/totalsupplyplan/export', { projectId: currentProject.value?.id }, `物资供应总计划.xlsx`, true);
};
/**
* 关闭弹窗
*/
const handleClose = () => {
visible.value = false;
formRef.value?.resetFields();
Object.keys(formData).forEach((key) => {
formData[key] = undefined;
});
};
/**
* 审批
*/
function clickApprovalSheet1() {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/overallPlanMaterialSupply/indexEdit`,
query: { id: state.masterData.id, type: 'update' }
});
}
/**
* 查看流程
*/
function lookApprovalFlow() {
proxy.$router.push({
path: `/approval/overallPlanMaterialSupply/indexEdit`,
query: { id: state.masterData.id, type: 'view' }
});
}
/**
* 监听项目变化
*/
const listeningProject = watch(
() => currentProject.value?.id,
(nid) => {
if (nid) getMasterDataList();
}
);
// 导入成功处理
const handleSuccess = () => {
ElMessage.success('导入成功');
getMasterDataList();
};
// 生命周期
onMounted(() => {
if (currentProject.value?.id) {
getMasterDataList();
}
});
onUnmounted(() => {
listeningProject();
loadingInstance.value?.close();
});
</script>
<style>
.overall-plan-material-supply {
padding: 20px;
}
.space-y-4 > .el-row {
margin-bottom: 16px;
}
.space-y-4 > .el-row:last-child {
margin-bottom: 0;
}
</style>

View File

@ -1,6 +1,5 @@
<template>
<div class="overall-plan-material-supply">
<!-- tabPosition="left" -->
<el-card shadow="always">
<template #header>
<el-row :gutter="10" class="mb8">
@ -30,14 +29,16 @@
>一键全部保存</el-button
>
</el-form-item>
<el-form-item>
<!-- <el-form-item>
<el-button type="primary" @click="toggleExpandAll">{{ isExpandAll ? '一键收起' : '一键展开' }}</el-button>
</el-form-item>
</el-form-item> -->
</el-form>
<right-toolbar @queryTable="getMasterDataList"></right-toolbar>
</el-row>
</template>
</el-card>
<!-- 本地数据懒加载表格 -->
<el-table
:data="state.tableData"
v-loading="state.loading.list"
@ -47,6 +48,9 @@
row-key="id"
border
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:lazy="true"
:load="loadLocalChildNodes"
@expand-change="handleExpandChange"
>
<el-table-column align="center" prop="num" label="编号" />
<el-table-column prop="name" label="工程或费用名称" width="180" />
@ -54,87 +58,95 @@
<el-table-column prop="specification" label="规格型号" width="80" />
<el-table-column prop="quantity" label="数量" width="100" />
<el-table-column prop="batchNumber" label="批次号" width="200" />
<!-- 优化的输入框列 -->
<el-table-column prop="brand" label="品牌">
<template #default="{ row }">
<!-- <el-input
v-model="row.brand"
<el-input
v-model.lazy="row.brand"
:disabled="state.masterData.status != 'draft'"
v-if="!(row.children && row.children.length > 0)"
v-if="!row.hasChildren"
placeholder=""
clearable
/> -->
:key="`brand-${row.id}`"
@change="handleInputChange(row, 'brand')"
/>
</template>
</el-table-column>
<el-table-column prop="texture" label="材质">
<template #default="{ row }">
<!-- <el-input
v-model="row.texture"
<el-input
v-model.lazy="row.texture"
:disabled="state.masterData.status != 'draft'"
v-if="!(row.children && row.children.length > 0)"
v-if="!row.hasChildren"
placeholder=""
clearable
/> -->
:key="`texture-${row.id}`"
@change="handleInputChange(row, 'texture')"
/>
</template>
</el-table-column>
<el-table-column prop="qualityStandard" label="质量标准">
<template #default="{ row }">
<!-- <el-input
:disabled="state.masterData.status != 'draft'"
v-model="row.qualityStandard"
v-if="!(row.children && row.children.length > 0)"
v-model.lazy="row.qualityStandard"
v-if="!row.hasChildren"
placeholder=""
clearable
/> -->
:key="`qualityStandard-${row.id}`"
@change="handleInputChange(row, 'qualityStandard')"
/>
</template>
</el-table-column>
<el-table-column prop="partUsed" label="使用部位">
<template #default="{ row }">
<!-- <el-input
:disabled="state.masterData.status != 'draft'"
v-model="row.partUsed"
v-if="!(row.children && row.children.length > 0)"
v-model.lazy="row.partUsed"
v-if="!row.hasChildren"
placeholder=""
clearable
/> -->
:key="`partUsed-${row.id}`"
@change="handleInputChange(row, 'partUsed')"
/>
</template>
</el-table-column>
<el-table-column prop="deliveryPoints" label="交货地点">
<template #default="{ row }">
<!-- <el-input
:disabled="state.masterData.status != 'draft'"
v-model="row.deliveryPoints"
v-if="!(row.children && row.children.length > 0)"
v-model.lazy="row.deliveryPoints"
v-if="!row.hasChildren"
placeholder=""
clearable
/> -->
:key="`deliveryPoints-${row.id}`"
@change="handleInputChange(row, 'deliveryPoints')"
/>
</template>
</el-table-column>
<el-table-column label="预计使用日期" width="150">
<template #default="{ row }">
<!-- <el-date-picker
v-model="row.dateService"
v-if="!(row.children && row.children.length > 0)"
v-if="!row.hasChildren"
type="date"
value-format="YYYY-MM-DD"
:disabled="state.masterData.status != 'draft'"
placeholder="请选择日期"
/> -->
:key="`dateService-${row.id}`"
@change="handleInputChange(row, 'dateService')"
/>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" />
<!-- <el-table-column label="操作">
<template #default="{ row }">
<el-button
:disabled="state.masterData.status == 'waiting' || state.masterData.status == 'finish'"
type="primary"
v-hasPermi="['design:totalsupplyplan:edit']"
@click="editApprovalSheet(row)"
>修改</el-button
>
</template>
</el-table-column> -->
</el-table>
<!-- 编辑 -->
<!-- 编辑弹窗 -->
<el-dialog v-model="visible" title="修改物料信息" :width="800" :close-on-click-modal="false" @close="handleClose">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px" class="space-y-4">
<el-row :gutter="20">
@ -149,7 +161,6 @@
</el-form-item>
</el-col>
</el-row>
<!-- 物料属性区域 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="材质" prop="texture">
@ -162,7 +173,6 @@
</el-form-item>
</el-col>
</el-row>
<!-- 日期与状态区域 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="使用部位" prop="partUsed">
@ -175,7 +185,6 @@
</el-form-item>
</el-col>
</el-row>
<!-- 其他信息区域 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="预计使用日期" prop="dateService">
@ -200,7 +209,8 @@
</template>
<script setup name="billofQuantities">
import { ref, reactive, onMounted, computed, getCurrentInstance } from 'vue';
import { ref, reactive, onMounted, computed, getCurrentInstance, watch, onUnmounted } from 'vue';
import { ElMessage, ElLoading } from 'element-plus';
import {
obtainMasterDataList,
totalsupplyplan,
@ -209,13 +219,26 @@ import {
totalSupplyplanBatchEdit
} from '@/api/materials/overallPlanMaterialSupply/index';
import { useUserStoreHook } from '@/store/modules/user';
// 全局状态与实例获取
const userStore = useUserStoreHook();
const currentProject = computed(() => userStore.selectedProject);
const { proxy } = getCurrentInstance();
// 基础状态管理
const visible = ref(false);
const formRef = ref(null);
const tableRef = ref(null);
const file = ref(null);
const isExpandAll = ref(false);
const expandRowKeys = ref([]);
const editDataMap = ref(new Map()); // 用Map存储修改的数据提高查询效率
const loadingInstance = ref(null);
const fullTreeData = ref([]); // 存储完整树形数据,用于本地懒加载
// 页面核心状态
const state = reactive({
tableData: [],
tableData: [], // 初始只存放根节点
queryForm: {
projectId: currentProject.value?.id,
versions: '',
@ -228,9 +251,9 @@ const state = reactive({
sheets: false,
list: false
},
// 主id
masterData: {}
});
// 表单数据
const formData = reactive({
batchNumber: '',
@ -252,6 +275,7 @@ const formData = reactive({
texture: '',
unit: ''
});
// 表单验证规则
const formRules = reactive({
name: [
@ -268,82 +292,190 @@ const formRules = reactive({
],
compileDate: [{ required: true, message: '请选择编制日期', trigger: 'change' }]
});
// 展开状态
const isExpandAll = ref(false);
const tableRef = ref(null);
// 切换展开状态
const toggleExpandAll = () => {
isExpandAll.value = !isExpandAll.value;
console.log(isExpandAll.value);
state.tableData.forEach((row) => {
tableRef.value.toggleRowExpansion(row, isExpandAll.value);
/**
* 本地数据懒加载实现:从完整数据中筛选子节点
*/
const loadLocalChildNodes = (row, treeNode, resolve) => {
// 避免重复加载
if (row.children && row.children.length > 0) {
resolve(row.children);
return;
}
// 显示加载状态
loadingInstance.value = ElLoading.service({
target: tableRef.value.$el,
text: '加载中...',
background: 'rgba(255, 255, 255, 0.8)'
});
try {
// 从完整数据中筛选当前行的子节点假设父ID对应子节点的pid
const childNodes = fullTreeData.value.filter((node) => node.pid === row.sid);
// 标记子节点是否有子节点
const formattedChildren = childNodes.map((node) => ({
...node,
hasChildren: fullTreeData.value.some((child) => child.pid === node.sid)
}));
// 缓存子节点到当前行
row.children = formattedChildren;
resolve(formattedChildren);
} catch (error) {
resolve([]);
} finally {
loadingInstance.value?.close();
}
};
// 获取主表数据
/**
* 处理展开状态变化
*/
const handleExpandChange = (row, expanded) => {
if (expanded) {
if (!expandRowKeys.value.includes(row.id)) {
expandRowKeys.value.push(row.id);
}
} else {
expandRowKeys.value = expandRowKeys.value.filter((id) => id !== row.id);
}
};
/**
* 一键展开/收起
*/
const toggleExpandAll = async () => {
isExpandAll.value = !isExpandAll.value;
if (isExpandAll.value) {
loadingInstance.value = ElLoading.service({
target: tableRef.value.$el,
text: '正在加载...',
background: 'rgba(255, 255, 255, 0.8)'
});
try {
// 递归加载所有层级子节点
const loadAllChildren = (nodes) => {
nodes.forEach((node) => {
const children = fullTreeData.value.filter((child) => child.pid === node.sid);
const formattedChildren = children.map((child) => ({
...child,
hasChildren: fullTreeData.value.some((c) => c.pid === child.sid)
}));
node.children = formattedChildren;
if (formattedChildren.length > 0) {
loadAllChildren(formattedChildren);
}
});
};
loadAllChildren(state.tableData);
// 展开所有节点
state.tableData.forEach((row) => {
tableRef.value.toggleRowExpansion(row, true);
});
ElMessage.success('所有节点已展开');
} catch (error) {
ElMessage.error(`一键展开失败:${error.message}`);
} finally {
loadingInstance.value?.close();
}
} else {
// 收起所有节点
state.tableData.forEach((row) => {
tableRef.value.toggleRowExpansion(row, false);
});
expandRowKeys.value = [];
}
};
/**
* 处理输入框变化(优化性能)
*/
const handleInputChange = (row, field) => {
// 只记录修改过的数据,避免全量对比
if (!editDataMap.value.has(row.id)) {
editDataMap.value.set(row.id, { ...row });
}
const storedData = editDataMap.value.get(row.id);
storedData[field] = row[field];
editDataMap.value.set(row.id, storedData);
};
/**
* 获取主数据列表(核心优化:拆分根节点和完整数据)
*/
async function getMasterDataList() {
try {
// 获取主数据列表
state.loading.list = true;
// 获取主数据
const masterDataRes = await obtainMasterDataList({
projectId: currentProject.value?.id
});
const { data: masterData } = masterDataRes;
console.log('masterData', masterData);
if (!masterData[0].id) {
console.warn('未获取到有效的主数据ID');
if (!masterData[0]?.id) {
state.tableData = [];
fullTreeData.value = [];
return;
}
state.masterData = masterData[0];
// 获取供应计划
const supplyPlanRes = await totalsupplyplan({ id: masterData[0].id });
// 处理结果
if (supplyPlanRes.list == null) {
// state.tableData = supplyPlanRes.rows || [];
state.tableData = proxy?.handleTree(supplyPlanRes.rows, 'sid', 'pid');
console.log('state.tableData', state.tableData);
} else {
// 根据实际业务逻辑处理有list的情况
state.tableData = [];
}
// 获取完整供应计划数据
const supplyPlanRes = await totalsupplyplan({ id: masterData[0].id, projectId: currentProject.value?.id });
const allData = supplyPlanRes.rows || [];
// 存储完整数据到本地
fullTreeData.value = [...allData];
// 初始只加载根节点pid为空或0的节点
const rootNodes = allData.filter((item) => !item.pid || item.pid == 0);
// 标记根节点是否有子节点
state.tableData = rootNodes.map((node) => ({
...node,
hasChildren: allData.some((child) => child.pid === node.sid)
}));
// 初始化可编辑数据映射
editDataMap.value.clear();
allData.forEach((item) => {
if (!item.hasChildren) {
// 只关注叶子节点
editDataMap.value.set(item.id, { ...item });
}
});
} catch (error) {
console.error('获取主数据列表失败:', error);
// 错误情况下给默认值,避免页面出错
state.tableData = [];
fullTreeData.value = [];
} finally {
state.loading.list = false;
}
}
// 获取详情
// 修改获取详情的方法
/**
* 获取详情
*/
async function totalSupplyplanDetail(id) {
try {
const result = await totalSupplyplanDetails(id);
if (result?.code === 200) {
const detailData = result.data || {};
// 1. 清空原有表单数据
// 清空表单并赋值
Object.keys(formData).forEach((key) => {
formData[key] = undefined;
});
// 2. 处理日期格式假设接口返回的是Date对象或ISO字符串
// 处理日期格式
const formatDate = (date) => {
if (!date) return '';
// 若为字符串先转为Date对象
const d = typeof date === 'string' ? new Date(date) : date;
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
};
// 3. 合并数据到formData响应式赋值
Object.assign(formData, {
...detailData,
// 单独处理日期字段,转为表单可识别的字符串格式
compileDate: formatDate(detailData.compileDate),
dateService: formatDate(detailData.dateService)
});
console.log('表单数据已更新:', formData);
} else {
ElMessage.error(`获取详情失败: ${result?.msg || '未知错误'}`);
}
@ -354,87 +486,113 @@ async function totalSupplyplanDetail(id) {
state.loading.list = false;
}
}
// 修改
/**
* 一键保存修改
*/
const editApprovalSheet = async () => {
state.loading.list = true;
await totalSupplyplanBatchEdit(state.tableData);
proxy.$modal.msgSuccess('修改成功');
state.loading.list = false;
};
// 提交表单
const handleSubmit = async () => {
try {
// 表单验证
await formRef.value.validate();
// 触发提交事件
editMaterialSupply(formData);
handleClose();
// 只提交修改过的数据从Map中获取
const modifiedData = Array.from(editDataMap.value.values());
if (modifiedData.length === 0) {
ElMessage.info('没有数据需要修改');
return;
}
await totalSupplyplanBatchEdit(modifiedData);
ElMessage.success('修改成功');
getMasterDataList(); // 重新加载数据
} catch (error) {
// 验证失败不提交
console.error('表单验证失败:', error);
return;
ElMessage.error(`保存失败:${error.message}`);
} finally {
state.loading.list = false;
}
};
// 修改物资
function editMaterialSupply(formData) {
materialChangeSupplyplan(formData).then((res) => {
if (res.code === 200) {
ElMessage.success('修改成功');
getMasterDataList();
} else {
ElMessage.error('修改失败');
}
});
}
const handleExport = async () => {
proxy?.download(
'/design/totalsupplyplan/export',
{
projectId: currentProject.value?.id
},
`物资供应总计划.xlsx`,
true
);
/**
* 提交表单
*/
const handleSubmit = async () => {
try {
await formRef.value.validate();
await materialChangeSupplyplan(formData);
ElMessage.success('修改成功');
handleClose();
getMasterDataList();
} catch (error) {
console.error('表单验证失败:', error);
}
};
// 关闭弹窗
/**
* 导出数据
*/
const handleExport = async () => {
proxy?.download('/design/totalsupplyplan/export', { projectId: currentProject.value?.id }, `物资供应总计划.xlsx`, true);
};
/**
* 关闭弹窗
*/
const handleClose = () => {
visible.value = false;
// 清空表单数据
formRef.value?.resetFields();
Object.keys(formData).forEach((key) => {
formData[key] = undefined;
});
// 重置表单验证状态
formRef.value?.resetFields();
};
// 审批
/**
* 审批
*/
function clickApprovalSheet1() {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/approval/overallPlanMaterialSupply/indexEdit`,
query: {
id: state.masterData.id,
type: 'update'
}
query: { id: state.masterData.id, type: 'update' }
});
}
// 审核流程
/**
* 查看流程
*/
function lookApprovalFlow() {
proxy.$router.push({
path: `/approval/overallPlanMaterialSupply/indexEdit`,
query: {
id: state.masterData.id,
type: 'view'
}
query: { id: state.masterData.id, type: 'view' }
});
}
onMounted(() => {
/**
* 监听项目变化
*/
const listeningProject = watch(
() => currentProject.value?.id,
(nid) => {
if (nid) getMasterDataList();
}
);
// 导入成功处理
const handleSuccess = () => {
ElMessage.success('导入成功');
getMasterDataList();
};
// 生命周期
onMounted(() => {
if (currentProject.value?.id) {
getMasterDataList();
}
});
onUnmounted(() => {
listeningProject();
loadingInstance.value?.close();
});
</script>
<style>
.overall-plan-material-supply {
padding: 20px;

View File

@ -51,9 +51,10 @@
size="small"
icon="Plus"
type="primary"
v-if="scope.row.purchaseSubmission == '0'"
link
@click="handleAddSon(scope.row)"
>添加</el-button
>提交</el-button
>
<el-button type="primary" v-hasPermi="['cailiaoshebei:physicalsupply:edit']" size="small" icon="Edit" link @click="handleEdit(scope.row)"
>修改</el-button
@ -113,7 +114,7 @@
</el-col>
<el-col :span="12">
<el-form-item label="运算周期(天)" prop="executionCycle">
<el-input v-model.number="formData.executionCycle" placeholder="请输入运算周期" type="number"></el-input>
<el-input v-model.number="formData.executionCycle" min="0" placeholder="请输入运算周期" type="number"></el-input>
</el-form-item>
</el-col>
</el-row>
@ -121,12 +122,12 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="安装量" prop="installationQuantity">
<el-input v-model="formData.installationQuantity" placeholder="请输入安装量"></el-input>
<el-input v-model="formData.installationQuantity" min="0" type="number" placeholder="请输入安装量"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="安装比例" prop="installationRatio">
<el-input v-model="formData.installationRatio" placeholder="请输入安装比例" suffix="%"></el-input>
<el-input v-model="formData.installationRatio" min="0" type="number" placeholder="请输入安装比例" suffix="%"></el-input>
</el-form-item>
</el-col>
</el-row>
@ -296,22 +297,7 @@ const handleSearch = () => {
// 刷新数据
const refreshData = () => {
fetchData();
ElMessage.success('数据已刷新');
};
// 分页大小改变
const handleSizeChange = (val) => {
pageSize.value = val;
currentPage.value = 1;
fetchData();
};
// 当前页改变
const handleCurrentChange = (val) => {
currentPage.value = val;
fetchData();
};
// 新增
const handleAdd = () => {
dialogType.value = 'add';
@ -476,6 +462,17 @@ const jumpRouter = (row) => {
}
});
};
//监听项目id刷新数据
const listeningProject = watch(
() => currentProject.value?.id,
(nid, oid) => {
fetchData();
}
);
onUnmounted(() => {
listeningProject();
});
// 初始化页面
onMounted(() => {
fetchData();

View File

@ -30,9 +30,7 @@
:row-class-name="tableRowClassName"
>
<!-- 基础信息列 -->
<el-table-column prop="id" label="ID" width="180" align="center"></el-table-column>
<el-table-column prop="batch" label="批次" align="center"></el-table-column>
<el-table-column prop="physicalsupplyId" label="使用情况ID" width="180" align="center"></el-table-column>
<!-- 时间相关列 -->
<el-table-column prop="issuanceTime" label="联系单下达时间" min-width="160" align="center">
<template #default="scope">

View File

@ -14,6 +14,7 @@
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select v-model="queryParams.type" placeholder="请选择类型">
<el-option label="全部" value="0" />
<el-option label="对甲" value="1" />
<el-option label="对乙" value="2" />
</el-select>
@ -102,7 +103,7 @@
<script setup name="MonthPlan" lang="ts">
import { listMonthPlan, getMonthPlan, delMonthPlan, addMonthPlan, updateMonthPlan } from '@/api/out/monthPlan';
import { MonthPlanVO, MonthPlanQuery, MonthPlanForm } from '@/api/out/monthPlan/types';
import { MonthPlanVO } from '@/api/out/monthPlan/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { out_value_type } = toRefs<any>(proxy?.useDict('out_value_type'));
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
@ -163,7 +164,7 @@ const data = reactive({
valueType: undefined,
planAuditStatus: undefined,
completeAuditStatus: undefined,
type: '1',
type: '0',
params: {}
},
rules: {
@ -180,7 +181,11 @@ const { queryParams, form, rules } = toRefs(data);
/** 查询月度产值计划列表 */
const getList = async () => {
loading.value = true;
const res = await listMonthPlan(queryParams.value);
let type = queryParams.value.type;
if (type == '0') {
type = '';
}
const res = await listMonthPlan({ ...queryParams.value, type: type });
monthPlanList.value = res.rows;
total.value = res.total;
loading.value = false;

View File

@ -161,7 +161,7 @@
:total="detailTotalWork"
v-model:page="detailQueryParamsWork.pageNum"
v-model:limit="detailQueryParamsWork.pageSize"
@pagination="getPvModuleList"
@pagination="getDailyBookList"
layout="total, sizes, prev, pager, next"
:isSmall="5"
/>
@ -436,10 +436,10 @@ const handleView = (row: any, obj: any) => {
// 获取已填日报
const getDailyBookList = (doneTime: string) => {
detialWordList.value = [];
// detialWordList.value = [];
getDailyBook({
id: formDetail.value.id,
...detailQueryParams.value
...detailQueryParamsWork.value
}).then((res: any) => {
if (res.code === 200) {
detialWordList.value = res.rows || [];

View File

@ -27,9 +27,9 @@
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['land:enterRoad:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button type="success" plain icon="Plus" @click="downloadTemplate" v-hasPermi="['land:enterRoad:import']">模板下载</el-button>
</el-col>
</el-col> -->
<el-col :span="1.5">
<el-upload
ref="uploadRef"

View File

@ -36,9 +36,9 @@
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['land:landBlock:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button type="success" plain icon="Plus" @click="downloadTemplate" v-hasPermi="['land:enterRoad:import']">模板下载</el-button>
</el-col>
</el-col> -->
<el-col :span="1.5">
<el-upload ref="uploadRef" class="upload-demo" :http-request="handleImport" :show-file-list="false">
<template #trigger>

View File

@ -150,7 +150,7 @@
<el-button link type="primary" icon="Edit" @click="handleCheckRules(scope.row)" v-hasPermi="['project:attendanceRule:byProjectId']"
>打卡规则
</el-button>
<!-- <el-button link type="primary" icon="Edit" @click="handleScope(scope.row)" v-hasPermi="['project:project:edit']">打卡范围 </el-button> -->
<el-button link type="primary" icon="Edit" @click="handleScope(scope.row)" v-hasPermi="['project:project:edit']">打卡范围 </el-button>
<el-button link type="primary" icon="FolderOpened" @click="handleShowUpload(scope.row)" v-hasPermi="['project:project:edit']"
>导入安全协议书
</el-button>
@ -367,23 +367,28 @@
</el-dialog>
<el-dialog draggable title="打卡范围" v-model="ScopeFlag" width="600">
<div v-for="(item, i) of punchRangeList" :key="i" class="options_item">
<el-row>
<el-row style="margin-bottom: 10px;">
<el-col :span="1"> <el-color-picker v-model="item.punchColor" show-alpha /></el-col>
<el-col :span="12"> <el-input v-model="item.punchName" placeholder="请输入打卡范围名称" class="ml-8" /></el-col>
<el-col :span="10" style="text-align: right; margin-top: 5px">
<el-button link type="primary" icon="view">预览</el-button>
<el-button link type="primary" icon="plus">添加</el-button>
<el-button link type="primary" icon="delete">移除</el-button>
<el-button v-if="item.id" link type="primary" icon="view" @click="previewPunchRange(item)">预览</el-button>
<el-button v-if="item.id" link type="primary" icon="delete" @click="handleScopeDel(item)">移除</el-button>
<el-button v-if="!item.id" link type="primary" icon="plus" @click="addPunchRange(item)">添加</el-button>
<el-button v-if="item.id" link type="primary" icon="download" @click="handleScopeEdit(item)">保存</el-button>
</el-col>
</el-row>
</div>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="scopeSubmit"> 提交</el-button>
<el-button type="primary" @click="scopeSubmit">确定</el-button>
<el-button @click="ScopeFlag = false">取消</el-button>
</div>
</template>
</el-dialog>
<CesiumEarthDialog ref="earthDialog" @send-data="handleEarthData"
:position="position"
:data="earthData"
@close="handleEarthClose"></CesiumEarthDialog>
</div>
</template>
@ -398,10 +403,17 @@ import {
updateProject,
attendanceRuleAdd,
attendanceRuleEdit,
byProjectIdDetail
byProjectIdDetail,
getAttendanceRangeList,
updateAttendanceRange,
delAttendanceRange
} from '@/api/project/project';
import { ProjectForm, ProjectQuery, ProjectVO, childProjectQuery, locationType } from '@/api/project/project/types';
import amap from '@/components/amap/index.vue';
import CesiumEarthDialog from "./map.vue"
import { useUserStoreHook } from '@/store/modules/user';
const userStore = useUserStoreHook();
const currentProject = computed(() => userStore.selectedProject);
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable, project_category_type, project_type, project_stage } = toRefs<any>(
proxy?.useDict('sys_normal_disable', 'project_category_type', 'project_type', 'project_stage')
@ -424,6 +436,14 @@ const projectId = ref<string>('');
const designId = ref<string>('');
const ruleFlag = ref(false);
const ScopeFlag = ref(false);
const earthDialog = ref(null);
const position = ref(''); //预览
let earthData = reactive({
projectId: '',
punchName: '',
punchColor: '#1983ff',
punchRange: '',
}); //传值给地图组件
const punchRangeList = ref<any>([
{
punchName: '',
@ -710,10 +730,48 @@ const handleSetChild = async () => {
}
};
const handleScope = (row) => {
console.log('row',row);
// 打卡范围
ScopeFlag.value = true;
projectId.value = row.id;
// 获取打卡范围列表
getListScope(row.id);
};
// 获取打卡范围列表
const getListScope = (id) => {
punchRangeList.value = [{
punchName: '',
punchColor: '#1983ff'
}];
getAttendanceRangeList({ projectId: id, }).then((res) => {
if (res.code === 200) {
punchRangeList.value.unshift(...res.rows);
}
});
}
// 修改打卡范围
const handleScopeEdit = (row) => {
updateAttendanceRange(row).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('修改成功');
// ScopeFlag.value = false;
getListScope(projectId.value);
}
});
}
// 删除打卡范围
const handleScopeDel = (row) => {
proxy.$modal.confirm('是否确认删除打卡范围?').then(() => {
delAttendanceRange(row.id).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('删除成功');
getListScope(projectId.value);
}
});
}).catch(() => {});
}
const scopeSubmit = () => {
// 提交打卡范围
};
@ -761,7 +819,36 @@ const handleExport = () => {
`project_${new Date().getTime()}.xlsx`
);
};
// 打开绘制范围
const addPunchRange = (item) => {
earthData.punchName = item.punchName
earthData.punchColor = item.punchColor
earthData.projectId = projectId.value
earthData.id = ''
earthData.punchRange = ''
if (earthData.punchName=='') {
proxy.$modal.msgError('请先填写打卡范围名称');
return
}
earthDialog.value.show()
}
// 接受绘制范围
const handleEarthData = (data) => {
ScopeFlag.value = false;
}
// 关闭绘制范围
const handleEarthClose = () => {
earthDialog.value.show = false
}
// 预览范围
const previewPunchRange = (item)=>{
earthData.id = item.id
earthData.projectId = item.projectId
earthData.punchName = item.punchName
earthData.punchColor = item.punchColor
earthData.punchRange = item.punchRange
earthDialog.value.show()
}
onMounted(() => {
getList();
});

View File

@ -0,0 +1,305 @@
<template>
<el-dialog v-model="visible" title="地球可视化" :width="dialogWidth" :height="dialogHeight" :before-close="handleClose"
destroy-on-close>
<div v-loading="loading" id="earthMap" class="earth-container"></div>
<template #footer>
<el-button type="warning" @click="redraw">重新绘制</el-button>
<el-button v-if="isDraw" type="success" @click="drawRange">绘制</el-button>
<el-button type="primary" @click="handlesubmit">确定</el-button>
<el-button type="danger" @click="handleClose">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, onMounted, watch, defineEmits, defineProps,onUnmounted } from 'vue';
import { ElMessage } from 'element-plus';
import {
addAttendanceRange,
updateAttendanceRange,
} from '@/api/project/project';
const emit = defineEmits(['send-data', 'close']);
const props = defineProps({
position: {
type: String,
default: ''
},
data: {
type: Object,
default: () => { }
}
});
// 弹窗控制变量
const visible = ref(false);
const dialogWidth = ref('90%');
const dialogHeight = ref('90%');
let earthInstance = null;
let earthContainer = ref(null);
let loading = ref(true);
let positions = ref([]);
const entityObject = ref(null);
const isDraw = ref(true);
// 显示弹窗
const show = () => {
visible.value = true;
console.log(props.data);
if (props.data?.id) {
console.log(231, props.data);
isDraw.value = false;
}
};
// 关闭弹窗
const handleClose = () => {
visible.value = false;
// 清除地球实例
if (earthInstance && earthInstance.destroy) {
earthInstance.destroy();
earthInstance = null;
}
// 清除全局变量引用
if (window.Earth2) {
delete window.Earth2;
}
};
// 监听弹窗显示状态,初始化地球
watch(
() => visible.value,
(newVal) => {
if (newVal && earthContainer.value) {
createEarth();
}
}
);
// 创建地球
const createEarth = () => {
if (!window.YJ) {
ElMessage.error('YJ库未加载请检查依赖');
return;
}
window.YJ.on({
ws: true,
// host: getIP(), // 资源所在服务器地址
// username: this.loginForm.username, // 用户名
// password: md5pass, // 密码
}).then((res) => {
if (!earthContainer.value) return;
loading.value = false;
// 创建地球实例
earthInstance = new YJ.YJEarth(earthContainer.value);
window.Earth2 = earthInstance;
// 开启右键和左键点击事件
YJ.Global.openRightClick(window.Earth2);
YJ.Global.openLeftClick(window.Earth2);
// 设置初始视角
const view = {
position: {
lng: 102.03643298211526,
lat: 34.393586474501,
alt: 11298179.51993155
},
orientation: {
heading: 360,
pitch: -89.94481747201486,
roll: 0
}
};
YJ.Global.CesiumContainer(window.Earth2, {
compass: false, //罗盘
});
// 加载底图
loadBaseMap(earthInstance.viewer);
// 可以取消注释以下代码来设置初始视角
// YJ.Global.flyTo(earthInstance, view);
// YJ.Global.setDefaultView(earthInstance.viewer, view)
if (props.data.punchRange) {
renderRange(JSON.parse(props.data.punchRange));
}
})
.catch((err) => {
console.error('初始化地球失败:', err);
ElMessage.error('初始化地球失败,请稍后重试');
});
};
// 加载底图
const loadBaseMap = (viewer) => {
if (!viewer || !Cesium) {
ElMessage.error('Cesium库未加载请检查依赖');
return;
}
try {
// 创建瓦片提供器
const imageryProvider = new Cesium.UrlTemplateImageryProvider({
url: 'https://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
fileExtension: 'png',
minimumLevel: 0,
maximumLevel: 18,
projection: Cesium.WebMercatorProjection,
credit: new Cesium.Credit('卫星图数据来源')
});
// 添加图层到视图
viewer.imageryLayers.addImageryProvider(imageryProvider);
} catch (err) {
console.error('加载底图失败:', err);
ElMessage.error('加载底图失败');
}
};
// 处理数据传递
const handlesubmit = () => {
if (!earthInstance || !earthInstance.viewer) {
ElMessage.warning('地球实例未初始化,无法获取数据');
return;
}
// 要传递的对象数据
const dataToSend = {
...props.data,
punchRange: JSON.stringify(positions.value),
};
if (props.data.id) {
updateAttendanceRange1(dataToSend)
} else {
addAttendanceRange1(dataToSend);
}
emit('send-data', dataToSend);
};
// 新增打卡范围接口
const addAttendanceRange1 = (data) => {
addAttendanceRange(data).then((res) => {
if (res.code === 200) {
ElMessage.success('新增打卡范围成功');
handleClose();
} else {
ElMessage.error(res.msg);
}
}).catch((err) => {
console.error('新增打卡范围失败:', err);
ElMessage.error('新增打卡范围失败');
});
};
// 修改打卡范围接口
const updateAttendanceRange1 = (data) => {
updateAttendanceRange(data).then((res) => {
if (res.code === 200) {
ElMessage.success('修改打卡范围成功');
handleClose();
} else {
ElMessage.error(res.msg);
}
}).catch((err) => {
console.error('修改打卡范围失败:', err);
ElMessage.error('修改打卡范围失败');
});
};
// 绘制范围
const drawRange = () => {
if (!earthInstance || !earthInstance.viewer) {
ElMessage.warning('地球实例未初始化,无法绘制');
return;
}
let draw = new YJ.Draw.DrawPolygon(window.Earth2);
draw.start((err, params) => {
if (err) {
console.error('绘制失败:', err);
ElMessage.error('绘制失败');
return;
}
console.log('绘制成功:', params);
positions.value = params;
renderRange(params);
});
}
// 渲染范围
const renderRange = (params) => {
let option = {
id: 'Checkinrange',
info: {
type: "richText",
text: "",
hrefs: "",
},
positions: params,
color: props.data.punchColor || "rgba(255,0,0,0.5)",
line: {
width: 3,
color: "rgba(255,0,0,1)",
},
type: 0,
show: true,
}
// PolygonObject
let entity = new YJ.Obj.PolygonObject(
window.Earth2,
option
);
entity.flyTo();
entityObject.value = entity
}
// 重新绘制
const redraw = () => {
if (entityObject.value) {
entityObject.value.remove();
}
drawRange();
}
// 组件挂载时设置容器ID
onMounted(() => {
if (!earthContainer.value) {
earthContainer.value = 'earthMap'; // 设置与初始化代码中相同的ID
}
});
// 暴露显示方法给父组件
defineExpose({
show
});
//
onUnmounted(() => {
if (earthInstance) {
earthInstance.destroy();
earthInstance = null;
window.Earth2 = null;
}
});
</script>
<style scoped>
.earth-container {
width: 100%;
height: 600px;
overflow: hidden;
position: relative;
}
:deep(.el-dialog__body) {
padding: 10px;
height: calc(100% - 100px);
overflow: hidden;
}
:deep(.el-dialog) {
display: flex;
flex-direction: column;
max-height: 90vh;
}
:deep(.el-dialog__content) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
</style>

View File

@ -80,6 +80,11 @@
<el-option v-for="item in team_clock_type" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="打卡范围" prop="punchRangeList" v-if="form.isClockIn == 1">
<el-select v-model="form.punchRangeList" multiple clearable placeholder="请选择打卡范围">
<el-option v-for="item in projectTeamRangeList" :key="item.id" :label="item.punchName" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
@ -98,13 +103,15 @@
</template>
<script setup name="ProjectTeam" lang="ts">
import { addProjectTeam, delProjectTeam, getProjectTeam, listProjectTeam, updateProjectTeam } from '@/api/project/projectTeam';
import { addProjectTeam, delProjectTeam, getProjectTeam, listProjectTeam, updateProjectTeam,getProjectTeamClockIn } from '@/api/project/projectTeam';
import { ProjectTeamForm, ProjectTeamQuery, ProjectTeamVO } from '@/api/project/projectTeam/types';
import { useUserStoreHook } from '@/store/modules/user';
import UserListDialog from '@/views/project/projectTeam/component/UserListDialog.vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { team_clock_type } = toRefs<any>(proxy?.useDict('team_clock_type'));
console.log(team_clock_type);
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
@ -117,6 +124,7 @@ const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const projectTeamRangeList = ref([]);
const currentRow = ref<ProjectTeamVO>({
id: undefined,
projectId: undefined,
@ -140,7 +148,8 @@ const initFormData: ProjectTeamForm = {
teamName: undefined,
isClockIn: undefined,
remark: undefined,
peopleNumber: undefined
peopleNumber: undefined,
punchRangeList: undefined
};
const data = reactive<PageData<ProjectTeamForm, ProjectTeamQuery>>({
form: { ...initFormData },
@ -171,6 +180,14 @@ const getList = async () => {
total.value = res.total;
loading.value = false;
};
/** 获取该项目的打开范围 "*/
const getClockIn = async () => {
if(currentProject.value?.id){
const res = await getProjectTeamClockIn({projectId:currentProject.value?.id});
projectTeamRangeList.value = res.rows
}
};
/** 取消按钮 */
const cancel = () => {
@ -280,5 +297,6 @@ onUnmounted(() => {
onMounted(() => {
getList();
getClockIn();
});
</script>

View File

@ -5,6 +5,11 @@
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="项目名称" prop="projectId">
<el-select v-model="queryParams.projectId" clearable placeholder="全部">
<el-option v-for="item in projectList" :key="item.value" :label="item.projectName" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="人员姓名" prop="userName">
<el-input v-model="queryParams.userName" placeholder="请输入人员姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
@ -156,6 +161,7 @@
</el-button>
<!-- <el-button link type="primary" icon="Switch" @click="handleToggle(scope.row)"> 切换人脸 </el-button> -->
<el-button link type="primary" icon="Switch" @click="handleChange(scope.row)"> 人员迁移 </el-button>
<el-button link type="primary" icon="Switch" @click="handleAssign(scope.row)"> 分配班组 </el-button>
<el-button link type="primary" icon="ChatLineSquare" @click="handleExit(scope.row)"> 入退场记录 </el-button>
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)"
v-hasPermi="['contractor:constructionUser:remove']">
@ -443,6 +449,30 @@
</template>
</el-calendar>
</el-dialog>
<el-dialog draggable :title="skipName + '-人员分配'" v-model="personnelAllocation" width="500px">
<el-form-item label="所属项目" label-width="130px">
<el-select v-model="personnelAllocationObject.projectId" @change="selectProject1" placeholder="请选择所属项目" style="width: 240px">
<el-option v-for="item in projectList" :key="item.id" :label="item.projectName" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="岗位" label-width="130px">
<el-select v-model="personnelAllocationObject.postId" placeholder="请选择岗位" style="width: 240px">
<el-option v-for="item in user_post_type" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="班组" label-width="130px">
<el-select v-model="personnelAllocationObject.teamId" :disabled="!personnelAllocationObject.projectId" placeholder="请选择分包单位"
style="width: 240px">
<el-option v-for="item in teamList" :key="item.id" :label="item.teamName" :value="item.id" />
</el-select>
</el-form-item>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handlePersonnelAllocation">确认</el-button>
<el-button @click="personnelAllocation = false"> 取消 </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
@ -462,7 +492,10 @@ import {
getConstructionUserExit,
dowloadConstructionUserTemplate,
importConstructionUserInfo,
listConstructionMonth
listConstructionMonth,
ProjectList,
TeamList,
TeamDistribution
} from '@/api/project/constructionUser';
import {
ConstructionUserForm,
@ -494,8 +527,8 @@ import { parseTime } from '@/utils/ruoyi';
const calendar = ref<CalendarInstance>();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { type_of_work, user_sex_type, user_clock_type, user_file_type, user_status_type, wage_measure_unit_type } = toRefs<any>(
proxy?.useDict('type_of_work', 'user_sex_type', 'user_clock_type', 'user_file_type', 'user_status_type', 'wage_measure_unit_type')
const { type_of_work, user_sex_type, user_clock_type, user_file_type, user_status_type, wage_measure_unit_type,user_post_type } = toRefs<any>(
proxy?.useDict('type_of_work', 'user_sex_type', 'user_clock_type', 'user_file_type', 'user_status_type', 'wage_measure_unit_type','user_post_type')
);
// 获取用户 store
const userStore = useUserStoreHook();
@ -511,6 +544,7 @@ const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const skip = ref(false);
const personnelAllocation = ref(false);
const fileStatus = ref(false);
const showFaceDrawer = ref(false);
const statusDialog = ref(false);
@ -531,6 +565,10 @@ const queryFormRef = ref<ElFormInstance>();
const constructionUserFormRef = ref<ElFormInstance>();
const skipName = ref('');
const calendarList = ref<Array<AttendanceMonthVO>>([]);
// 项目列表
const projectList = ref([]);
// 班组列表
const teamList = ref([]);
const dialog = reactive<DialogOption>({
visible: false,
title: '',
@ -543,6 +581,14 @@ const skipObject: skipType = reactive({
projectId: '',
contractorId: ''
});
// 人员分配
const personnelAllocationObject = reactive({
memberId: null,
projectId: '',
teamId: '',
postId: '',
});
const contractorList = ref<Array<skipTeamType>>([]);
//项目列表
const skipOptions = ref<Array<skipOptionType>>([]);
@ -661,6 +707,13 @@ const uploadPath = computed(() => {
return list;
});
// 获取项目列表
const getProjectList = async () => {
const res = await ProjectList({});
projectList.value = res.rows;
projectList.value.unshift({ id: '', projectName: '全部' });
};
/** 返回文件上传状态 */
const uploadStatusColor = computed(() => (str: string) => {
switch (str) {
@ -825,7 +878,7 @@ const reset = () => {
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
if (contractorOpt.value.length == 1) queryParams.value.contractorId = contractorOpt.value[0].value;
// if (contractorOpt.value.length == 1) queryParams.value.contractorId = contractorOpt.value[0].value;
getList();
};
@ -1053,6 +1106,36 @@ const listeningProject = watch(
getContractorList();
}
);
// 分配班组
const handleAssign = async (row: ConstructionUserVO) => {
const _id = row?.id || ids.value[0];
currentUserId.value = _id;
personnelAllocationObject.memberId = row?.sysUserId;
personnelAllocation.value = true;
};
// 选择项目1
const selectProject1 = (e: any) => {
// 请求班组
getTeamList(personnelAllocationObject.projectId);
};
const getTeamList = async (projectId) => {
const res = await TeamList({
projectId,
pageNum: 1,
pageSize: 100
});
teamList.value = res.rows;
};
// 人员分配
const handlePersonnelAllocation = async () => {
let res = await TeamDistribution(personnelAllocationObject);
if (res.code == 200) {
ElMessage.success(res.msg);
personnelAllocation.value = false;
getList();
}
};
onUnmounted(() => {
listeningProject();
@ -1060,6 +1143,7 @@ onUnmounted(() => {
onMounted(() => {
getContractorList();
getProjectList();
});
</script>
<style scoped lang="scss">

View File

@ -0,0 +1,413 @@
<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="menuName">
<el-input v-model="queryParams.menuName" placeholder="请输入菜单名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="菜单状态" clearable>
<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>
<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="hover">
<template #header>
<el-row :gutter="10">
<el-col :span="1.5">
<el-button v-hasPermi="['system:menu:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增 </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:show-search="showSearch" @query-table="getList"></right-toolbar>
</el-row>
</template>
<el-table
ref="menuTableRef"
v-loading="loading"
:data="menuList"
row-key="menuId"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:default-expand-all="isExpandAll"
>
<el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
<el-table-column prop="icon" label="图标" align="center" width="100">
<template #default="scope">
<svg-icon :icon-class="scope.row.icon" />
</template>
</el-table-column>
<el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
<el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime">
<template #default="scope">
<span>{{ scope.row.createTime }}</span>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="180">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button v-hasPermi="['system:menu:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)" />
</el-tooltip>
<el-tooltip content="新增" placement="top">
<el-button v-hasPermi="['system:menu:add']" link type="primary" icon="Plus" @click="handleAdd(scope.row)" />
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button v-hasPermi="['system:menu:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)" />
</el-tooltip>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="dialog.visible" :title="dialog.title" destroy-on-close append-to-bod width="750px">
<el-form ref="menuFormRef" :model="form" :rules="rules" label-width="100px">
<el-row>
<el-col :span="24">
<el-form-item label="上级菜单">
<el-tree-select
v-model="form.parentId"
:data="menuOptions"
:props="{ value: 'menuId', label: 'menuName', children: 'children' }"
value-key="menuId"
placeholder="选择上级菜单"
check-strictly
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="菜单类型" prop="menuType">
<el-radio-group v-model="form.menuType">
<el-radio value="M">目录</el-radio>
<el-radio value="C">菜单</el-radio>
<el-radio value="F">按钮</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col v-if="form.menuType !== 'F'" :span="24">
<el-form-item label="菜单图标" prop="icon">
<!-- 图标选择器 -->
<icon-select v-model="form.icon" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="菜单名称" prop="menuName">
<el-input v-model="form.menuName" placeholder="请输入菜单名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="显示排序" prop="orderNum">
<el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
</el-form-item>
</el-col>
<el-col v-if="form.menuType !== 'F'" :span="12">
<el-form-item>
<template #label>
<span>
<el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top">
<el-icon>
<question-filled />
</el-icon> </el-tooltip
>是否外链
</span>
</template>
<el-radio-group v-model="form.isFrame">
<el-radio label="0"></el-radio>
<el-radio label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col v-if="form.menuType !== 'F'" :span="12">
<el-form-item prop="path">
<template #label>
<span>
<el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top">
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
路由地址
</span>
</template>
<el-input v-model="form.path" placeholder="请输入路由地址" />
</el-form-item>
</el-col>
<el-col v-if="form.menuType === 'C'" :span="12">
<el-form-item prop="component">
<template #label>
<span>
<el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
组件路径
</span>
</template>
<el-input v-model="form.component" placeholder="请输入组件路径" />
</el-form-item>
</el-col>
<el-col v-if="form.menuType !== 'M'" :span="12">
<el-form-item>
<el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" />
<template #label>
<span>
<el-tooltip content="控制器中定义的权限字符,如:@SaCheckPermission('system:user:list')" placement="top">
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
权限字符
</span>
</template>
</el-form-item>
</el-col>
<el-col v-if="form.menuType === 'C'" :span="12">
<el-form-item>
<el-input v-model="form.queryParam" placeholder="请输入路由参数" maxlength="255" />
<template #label>
<span>
<el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top">
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
路由参数
</span>
</template>
</el-form-item>
</el-col>
<el-col v-if="form.menuType === 'C'" :span="12">
<el-form-item>
<template #label>
<span>
<el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top">
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
是否缓存
</span>
</template>
<el-radio-group v-model="form.isCache">
<el-radio label="0">缓存</el-radio>
<el-radio label="1">不缓存</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col v-if="form.menuType !== 'F'" :span="12">
<el-form-item>
<template #label>
<span>
<el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top">
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
显示状态
</span>
</template>
<el-radio-group v-model="form.visible">
<el-radio v-for="dict in sys_show_hide" :key="dict.value" :label="dict.value">{{ dict.label }} </el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
<el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top">
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
菜单状态
</span>
</template>
<el-radio-group v-model="form.status">
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Menu" lang="ts">
import { addMenu, delMenu, getMenu, listMenu, updateMenu } from '@/api/system/menu';
import { MenuForm, MenuQuery, MenuVO } from '@/api/system/menu/types';
import { MenuTypeEnum } from '@/enums/MenuTypeEnum';
interface MenuOptionsType {
menuId: number;
menuName: string;
children: MenuOptionsType[] | undefined;
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_show_hide, sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_show_hide', 'sys_normal_disable'));
const menuList = ref<MenuVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const menuOptions = ref<MenuOptionsType[]>([]);
const isExpandAll = ref(false);
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const queryFormRef = ref<ElFormInstance>();
const menuFormRef = ref<ElFormInstance>();
const initFormData = {
path: '',
menuId: undefined,
parentId: 0,
menuName: '',
icon: '',
menuType: MenuTypeEnum.M,
orderNum: 1,
isFrame: '1',
isCache: '0',
visible: '0',
status: '0',
menuSource: '2'
};
const data = reactive<PageData<MenuForm, MenuQuery>>({
form: { ...initFormData },
queryParams: {
menuName: undefined,
status: undefined,
menuSource: 2
},
rules: {
menuName: [{ required: true, message: '菜单名称不能为空', trigger: 'blur' }],
orderNum: [{ required: true, message: '菜单顺序不能为空', trigger: 'blur' }],
path: [{ required: true, message: '路由地址不能为空', trigger: 'blur' }]
}
});
const menuTableRef = ref<ElTableInstance>();
const { queryParams, form, rules } = toRefs<PageData<MenuForm, MenuQuery>>(data);
/** 查询菜单列表 */
const getList = async () => {
loading.value = true;
const res = await listMenu(queryParams.value);
const data = proxy?.handleTree<MenuVO>(res.data, 'menuId');
if (data) {
menuList.value = data;
}
loading.value = false;
};
/** 查询菜单下拉树结构 */
const getTreeselect = async () => {
menuOptions.value = [];
const response = await listMenu();
const menu: MenuOptionsType = { menuId: 0, menuName: '主类目', children: [] };
menu.children = proxy?.handleTree<MenuOptionsType>(response.data, 'menuId');
menuOptions.value.push(menu);
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
menuFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 新增按钮操作 */
const handleAdd = (row?: MenuVO) => {
reset();
getTreeselect();
row && row.menuId ? (form.value.parentId = row.menuId) : (form.value.parentId = 0);
dialog.visible = true;
dialog.title = '添加菜单';
};
/** 展开/折叠操作 */
const handleToggleExpandAll = () => {
isExpandAll.value = !isExpandAll.value;
toggleExpandAll(menuList.value, isExpandAll.value);
};
/** 展开/折叠所有 */
const toggleExpandAll = (data: MenuVO[], status: boolean) => {
data.forEach((item: MenuVO) => {
menuTableRef.value?.toggleRowExpansion(item, status);
if (item.children && item.children.length > 0) toggleExpandAll(item.children, status);
});
};
/** 修改按钮操作 */
const handleUpdate = async (row: MenuVO) => {
reset();
await getTreeselect();
if (row.menuId) {
const { data } = await getMenu(row.menuId);
form.value = data;
}
dialog.visible = true;
dialog.title = '修改菜单';
};
/** 提交按钮 */
const submitForm = () => {
menuFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
form.value.menuId ? await updateMenu(form.value) : await addMenu(form.value);
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row: MenuVO) => {
await proxy?.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?');
await delMenu(row.menuId);
await getList();
proxy?.$modal.msgSuccess('删除成功');
};
onMounted(() => {
getList();
});
</script>

View File

@ -0,0 +1,160 @@
<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="search">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="用户名称" prop="userName">
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="queryParams.phonenumber" 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>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10">
<el-col :span="1.5">
<el-button v-hasPermi="['system:role:add']" type="primary" plain icon="Plus" @click="openSelectUser">添加用户</el-button>
</el-col>
<el-col :span="1.5">
<el-button v-hasPermi="['system:role:remove']" type="danger" plain icon="CircleClose" :disabled="multiple" @click="cancelAuthUserAll">
批量取消授权
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Close" @click="handleClose">关闭</el-button>
</el-col>
<right-toolbar v-model:show-search="showSearch" :search="true" @query-table="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="用户名称" prop="userName" :show-overflow-tooltip="true" />
<el-table-column label="用户昵称" prop="nickName" :show-overflow-tooltip="true" />
<el-table-column label="邮箱" prop="email" :show-overflow-tooltip="true" />
<el-table-column label="手机" prop="phonenumber" :show-overflow-tooltip="true" />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span>{{ scope.row.createTime }}</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 v-hasPermi="['system:role:remove']" link type="primary" icon="CircleClose" @click="cancelAuthUser(scope.row)"> </el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
<select-user ref="selectRef" :role-id="queryParams.roleId" @ok="handleQuery" />
</el-card>
</div>
</template>
<script setup name="AuthUser" lang="ts">
import { allocatedUserList, authUserCancel, authUserCancelAll } from '@/api/system/role';
import { UserQuery } from '@/api/system/user/types';
import { UserVO } from '@/api/system/user/types';
import SelectUser from './selectUser.vue';
import { RouteLocationNormalized } from 'vue-router';
const route = useRoute();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
const userList = ref<UserVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const multiple = ref(true);
const total = ref(0);
const userIds = ref<Array<string | number>>([]);
const queryFormRef = ref<ElFormInstance>();
const selectRef = ref<InstanceType<typeof SelectUser>>();
const queryParams = reactive<UserQuery>({
pageNum: 1,
pageSize: 10,
roleId: route.params.roleId as string,
userName: undefined,
phonenumber: undefined
});
/** 查询授权用户列表 */
const getList = async () => {
loading.value = true;
const res = await allocatedUserList(queryParams);
userList.value = res.rows;
total.value = res.total;
loading.value = false;
};
// 返回按钮
const handleClose = () => {
const obj: RouteLocationNormalized = {
path: '/system/role',
fullPath: '',
hash: '',
matched: [],
meta: undefined,
name: undefined,
params: undefined,
query: undefined,
redirectedFrom: undefined
};
proxy?.$tab.closeOpenPage(obj);
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
// 多选框选中数据
const handleSelectionChange = (selection: UserVO[]) => {
userIds.value = selection.map((item) => item.userId);
multiple.value = !selection.length;
};
/** 打开授权用户表弹窗 */
const openSelectUser = () => {
selectRef.value?.show();
};
/** 取消授权按钮操作 */
const cancelAuthUser = async (row: UserVO) => {
await proxy?.$modal.confirm('确认要取消该用户"' + row.userName + '"角色吗?');
await authUserCancel({ userId: row.userId, roleId: queryParams.roleId });
await getList();
proxy?.$modal.msgSuccess('取消授权成功');
};
/** 批量取消授权按钮操作 */
const cancelAuthUserAll = async () => {
const roleId = queryParams.roleId;
const uIds = userIds.value.join(',');
await proxy?.$modal.confirm('是否取消选中用户授权数据项?');
await authUserCancelAll({ roleId: roleId, userIds: uIds });
await getList();
proxy?.$modal.msgSuccess('取消授权成功');
};
onMounted(() => {
getList();
});
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,574 @@
<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="roleName">
<el-input v-model="queryParams.roleName" placeholder="请输入角色名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="权限字符" prop="roleKey">
<el-input v-model="queryParams.roleKey" placeholder="请输入权限字符" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="角色状态" clearable>
<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>
<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-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 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>
<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="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>
<el-form-item prop="roleKey">
<template #label>
<span>
<el-tooltip content="控制器中定义的权限字符,如:@SaCheckRole('admin')" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
权限字符
</span>
</template>
<el-input v-model="form.roleKey" placeholder="请输入权限字符" />
</el-form-item>
<el-form-item label="角色顺序" prop="roleSort">
<el-input-number v-model="form.roleSort" controls-position="right" :min="0" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="菜单权限">
<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
ref="menuRef"
class="tree-border"
:data="menuOptions"
show-checkbox
node-key="id"
:check-strictly="!form.menuCheckStrictly"
empty-text="加载中请稍候"
: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>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
<!-- 分配角色数据权限对话框 -->
<el-dialog v-model="openDataScope" :title="dialog.title" width="500px" append-to-body>
<el-form ref="dataScopeRef" :model="form" label-width="80px">
<el-form-item label="角色名称">
<el-input v-model="form.roleName" :disabled="true" />
</el-form-item>
<el-form-item label="权限字符">
<el-input v-model="form.roleKey" :disabled="true" />
</el-form-item>
<el-form-item label="权限范围">
<el-select v-model="form.dataScope" @change="dataScopeSelectChange">
<el-option v-for="item in dataScopeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item v-show="form.dataScope === '2'" label="数据权限">
<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
ref="deptRef"
class="tree-border"
:data="deptOptions"
show-checkbox
default-expand-all
node-key="id"
:check-strictly="!form.deptCheckStrictly"
empty-text="加载中请稍候"
:props="{ label: 'label', children: 'children' }"
></el-tree>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitDataScope"> </el-button>
<el-button @click="cancelDataScope"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Role" lang="ts">
import { addRole, changeRoleStatus, dataScope, delRole, getRole, listRole, updateRole, deptTreeSelect } from '@/api/system/role';
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, { uploadCertList } from '@/api/system/user';
import { DeptTreeVO, DeptVO } from '@/api/system/dept/types';
const router = useRouter();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
const roleList = ref<RoleVO[]>();
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 dateRange = ref<[DateModelType, DateModelType]>(['', '']);
const menuOptions = ref<MenuTreeOption[]>([]);
const menuExpand = ref(false);
const menuNodeAll = ref(false);
const deptExpand = ref(true);
const deptNodeAll = ref(false);
const deptOptions = ref<DeptTreeVO[]>([]);
const enabledDeptOptions = ref<DeptTreeVO[]>([]);
const openDataScope = ref(false);
const deptName = ref('');
/** 数据范围选项*/
const dataScopeOptions = ref([
{ value: '1', label: '全部数据权限' },
{ value: '2', label: '自定数据权限' },
{ value: '3', label: '本部门数据权限' },
{ value: '4', label: '本部门及以下数据权限' },
{ value: '5', label: '仅本人数据权限' },
{ value: '6', label: '部门及以下或本人数据权限' }
]);
const queryFormRef = ref<ElFormInstance>();
const roleFormRef = ref<ElFormInstance>();
const dataScopeRef = ref<ElFormInstance>();
const menuRef = ref<ElTreeInstance>();
const deptRef = ref<ElTreeInstance>();
const deptTreeRef = ref<ElTreeInstance>();
const initForm = {
roleId: undefined,
roleSort: 1,
status: '0',
roleName: '',
roleKey: '',
menuCheckStrictly: true,
deptCheckStrictly: true,
remark: '',
dataScope: '1',
menuIds: [],
deptId: '',
isSpecial: null,
deptIds: [],
roleSource: '2'
};
const data = reactive({
form: { ...initForm },
queryParams: {
pageNum: 1,
pageSize: 10,
roleName: '',
roleKey: '',
deptId: '',
status: '',
roleSource: '2'
},
rules: {
roleName: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }],
roleKey: [{ required: true, message: '权限字符不能为空', trigger: 'blur' }],
roleSort: [{ required: true, message: '角色顺序不能为空', trigger: 'blur' }]
}
});
const { form, queryParams, rules } = toRefs(data);
const dialog = reactive<DialogOption>({
visible: false,
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();
};
/**
* 查询角色列表
*/
const getList = () => {
loading.value = true;
listRole(proxy?.addDateRange(queryParams.value, dateRange.value)).then((res) => {
roleList.value = res.rows;
total.value = res.total;
loading.value = false;
});
};
/**
* 搜索按钮操作
*/
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置 */
const resetQuery = () => {
dateRange.value = ['', ''];
queryFormRef.value?.resetFields();
queryParams.value.pageNum = 1;
queryParams.value.deptId = undefined;
deptTreeRef.value?.setCurrentKey(undefined);
handleQuery();
};
/**删除按钮操作 */
const handleDelete = async (row?: RoleVO) => {
const roleids = row?.roleId || ids.value;
await proxy?.$modal.confirm('是否确认删除角色编号为' + roleids + '数据项目');
await delRole(roleids);
getList();
proxy?.$modal.msgSuccess('删除成功');
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'system/role/export',
{
...queryParams.value
},
`role_${new Date().getTime()}.xlsx`
);
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: RoleVO[]) => {
ids.value = selection.map((item: RoleVO) => item.roleId);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 角色状态修改 */
const handleStatusChange = async (row: RoleVO) => {
let text = row.status === '0' ? '启用' : '停用';
try {
await proxy?.$modal.confirm('确认要"' + text + '""' + row.roleName + '"角色吗?');
await changeRoleStatus(row.roleId, row.status);
proxy?.$modal.msgSuccess(text + '成功');
} catch {
row.status = row.status === '0' ? '1' : '0';
}
};
/** 分配用户 */
const handleAuthUser = (row: RoleVO) => {
router.push('/system/role-auth/user/' + row.roleId);
};
/** 查询菜单树结构 */
const getMenuTreeselect = async () => {
const res = await menuTreeselect({ menuSource: '2' });
menuOptions.value = res.data;
};
/** 所有部门节点数据 */
const getDeptAllCheckedKeys = (): any => {
// 目前被选中的部门节点
let checkedKeys = deptRef.value?.getCheckedKeys();
// 半选中的部门节点
let halfCheckedKeys = deptRef.value?.getHalfCheckedKeys();
if (halfCheckedKeys) {
checkedKeys?.unshift.apply(checkedKeys, halfCheckedKeys);
}
return checkedKeys;
};
/** 重置新增的表单以及其他数据 */
const reset = () => {
menuRef.value?.setCheckedKeys([]);
menuExpand.value = false;
menuNodeAll.value = false;
deptExpand.value = true;
deptNodeAll.value = false;
form.value = { ...initForm };
roleFormRef.value?.resetFields();
};
/** 添加角色 */
const handleAdd = () => {
reset();
getMenuTreeselect();
dialog.visible = true;
dialog.title = '添加角色';
};
/** 修改角色 */
const handleUpdate = async (row?: RoleVO) => {
reset();
const roleId = row?.roleId || ids.value[0];
const { data } = await getRole(roleId);
Object.assign(form.value, data);
form.value.roleSort = Number(form.value.roleSort);
const res = await getRoleMenuTreeselect(roleId);
dialog.title = '修改角色';
dialog.visible = true;
res.checkedKeys.forEach((v) => {
nextTick(() => {
menuRef.value?.setChecked(v, true, false);
});
});
};
/** 根据角色ID查询菜单树结构 */
const getRoleMenuTreeselect = (roleId: string | number) => {
return roleMenuTreeselect(roleId, { menuSource: '2' }).then((res): RoleMenuTree => {
menuOptions.value = res.data.menus;
return res.data;
});
};
/** 根据角色ID查询部门树结构 */
const getRoleDeptTreeSelect = async (roleId: string | number) => {
const res = await deptTreeSelect(roleId, { roleSource: '2' });
deptOptions.value = res.data.depts;
return res.data;
};
/** 树权限(展开/折叠)*/
const handleCheckedTreeExpand = (value: boolean, type: string) => {
if (type == 'menu') {
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') {
let treeList = deptOptions.value;
for (let i = 0; i < treeList.length; i++) {
if (deptRef.value) {
deptRef.value.store.nodesMap[treeList[i].id].expanded = value;
}
}
}
};
/** 树权限(全选/全不选) */
const handleCheckedTreeNodeAll = (value: any, type: string) => {
if (type == 'menu') {
menuRef.value?.setCheckedNodes(value ? (menuOptions.value as any) : []);
} else if (type == 'dept') {
deptRef.value?.setCheckedNodes(value ? (deptOptions.value as any) : []);
}
};
/** 树权限(父子联动) */
const handleCheckedTreeConnect = (value: any, type: string) => {
if (type == 'menu') {
form.value.menuCheckStrictly = value;
} else if (type == 'dept') {
form.value.deptCheckStrictly = value;
}
};
/** 所有菜单节点数据 */
const getMenuAllCheckedKeys = (): any => {
// 目前被选中的菜单节点
let checkedKeys = menuRef.value?.getCheckedKeys();
// 半选中的菜单节点
let halfCheckedKeys = menuRef.value?.getHalfCheckedKeys();
if (halfCheckedKeys) {
checkedKeys?.unshift.apply(checkedKeys, halfCheckedKeys);
}
return checkedKeys;
};
/** 提交按钮 */
const submitForm = () => {
roleFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
form.value.menuIds = getMenuAllCheckedKeys();
form.value.roleId ? await updateRole(form.value) : await addRole(form.value);
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
getList();
}
});
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 选择角色权限范围触发 */
const dataScopeSelectChange = (value: string) => {
if (value !== '2') {
deptRef.value?.setCheckedKeys([]);
}
};
/** 分配数据权限操作 */
const handleDataScope = async (row: RoleVO) => {
const response = await getRole(row.roleId);
Object.assign(form.value, response.data);
const res = await getRoleDeptTreeSelect(row.roleId);
openDataScope.value = true;
dialog.title = '分配数据权限';
await nextTick(() => {
deptRef.value?.setCheckedKeys(res.checkedKeys);
});
};
/** 提交按钮(数据权限) */
const submitDataScope = async () => {
if (form.value.roleId) {
form.value.deptIds = getDeptAllCheckedKeys();
await dataScope(form.value);
proxy?.$modal.msgSuccess('修改成功');
openDataScope.value = false;
getList();
}
};
/** 取消按钮(数据权限)*/
const cancelDataScope = () => {
dataScopeRef.value?.resetFields();
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

@ -0,0 +1,132 @@
<template>
<el-row>
<el-dialog v-model="visible" title="选择用户" width="800px" top="5vh" append-to-body>
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="用户名称" prop="userName">
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="queryParams.phonenumber" 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-row>
<el-table ref="tableRef" :data="userList" height="260px" @row-click="clickRow" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column label="用户名称" prop="userName" :show-overflow-tooltip="true" />
<el-table-column label="用户昵称" prop="nickName" :show-overflow-tooltip="true" />
<el-table-column label="邮箱" prop="email" :show-overflow-tooltip="true" />
<el-table-column label="手机" prop="phonenumber" :show-overflow-tooltip="true" />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span>{{ proxy.parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
</el-table>
<pagination v-if="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
</el-row>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSelectUser"> </el-button>
<el-button @click="visible = false"> </el-button>
</div>
</template>
</el-dialog>
</el-row>
</template>
<script setup name="SelectUser" lang="ts">
import { authUserSelectAll, unallocatedUserList } from '@/api/system/role';
import { UserVO } from '@/api/system/user/types';
import { UserQuery } from '@/api/system/user/types';
const props = defineProps({
roleId: {
type: [Number, String],
required: true
}
});
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
const userList = ref<UserVO[]>([]);
const visible = ref(false);
const total = ref(0);
const userIds = ref<Array<string | number>>([]);
const queryParams = reactive<UserQuery>({
pageNum: 1,
pageSize: 10,
roleId: undefined,
userName: undefined,
phonenumber: undefined
});
const tableRef = ref<ElTableInstance>();
const queryFormRef = ref<ElFormInstance>();
const show = () => {
queryParams.roleId = props.roleId;
getList();
visible.value = true;
};
/**
* 选择行
*/
const clickRow = (row: any) => {
// ele的bug
tableRef.value?.toggleRowSelection(row, false);
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: UserVO[]) => {
userIds.value = selection.map((item: UserVO) => item.userId);
};
/** 查询数据 */
const getList = async () => {
const res = await unallocatedUserList(queryParams);
userList.value = res.rows;
total.value = res.total;
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
getList();
};
const emit = defineEmits(['ok']);
/**选择授权用户操作 */
const handleSelectUser = async () => {
const roleId = queryParams.roleId;
const ids = userIds.value.join(',');
if (ids == '') {
proxy?.$modal.msgError('请选择要分配的用户');
return;
}
await authUserSelectAll({ roleId, userIds: ids });
proxy?.$modal.msgSuccess('分配成功');
emit('ok');
visible.value = false;
};
// 暴露
defineExpose({
show
});
</script>
<style scoped></style>

View File

@ -34,6 +34,7 @@
</el-row>
</template>
<!-- 关键修改开启lazy模式使用本地数据筛选子节点 -->
<el-table
ref="menuTableRef"
v-loading="loading"
@ -41,6 +42,9 @@
row-key="menuId"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:default-expand-all="isExpandAll"
lazy
:load="loadChildren"
:has-children="hasChildren"
>
<el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
<el-table-column prop="icon" label="图标" align="center" width="100">
@ -77,7 +81,7 @@
</el-table>
</el-card>
<el-dialog v-model="dialog.visible" :title="dialog.title" destroy-on-close append-to-bod width="750px">
<el-dialog v-model="dialog.visible" :title="dialog.title" destroy-on-close append-to-body width="750px">
<el-form ref="menuFormRef" :model="form" :rules="rules" label-width="100px">
<el-row>
<el-col :span="24">
@ -103,7 +107,6 @@
</el-col>
<el-col v-if="form.menuType !== 'F'" :span="24">
<el-form-item label="菜单图标" prop="icon">
<!-- 图标选择器 -->
<icon-select v-model="form.icon" />
</el-form-item>
</el-col>
@ -122,10 +125,9 @@
<template #label>
<span>
<el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top">
<el-icon>
<question-filled />
</el-icon> </el-tooltip
>是否外链
<el-icon><question-filled /></el-icon>
</el-tooltip>
是否外链
</span>
</template>
<el-radio-group v-model="form.isFrame">
@ -139,9 +141,7 @@
<template #label>
<span>
<el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top">
<el-icon>
<question-filled />
</el-icon>
<el-icon><question-filled /></el-icon>
</el-tooltip>
路由地址
</span>
@ -154,9 +154,7 @@
<template #label>
<span>
<el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
<el-icon>
<question-filled />
</el-icon>
<el-icon><question-filled /></el-icon>
</el-tooltip>
组件路径
</span>
@ -170,9 +168,7 @@
<template #label>
<span>
<el-tooltip content="控制器中定义的权限字符,如:@SaCheckPermission('system:user:list')" placement="top">
<el-icon>
<question-filled />
</el-icon>
<el-icon><question-filled /></el-icon>
</el-tooltip>
权限字符
</span>
@ -185,9 +181,7 @@
<template #label>
<span>
<el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top">
<el-icon>
<question-filled />
</el-icon>
<el-icon><question-filled /></el-icon>
</el-tooltip>
路由参数
</span>
@ -199,9 +193,7 @@
<template #label>
<span>
<el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top">
<el-icon>
<question-filled />
</el-icon>
<el-icon><question-filled /></el-icon>
</el-tooltip>
是否缓存
</span>
@ -217,9 +209,7 @@
<template #label>
<span>
<el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top">
<el-icon>
<question-filled />
</el-icon>
<el-icon><question-filled /></el-icon>
</el-tooltip>
显示状态
</span>
@ -234,9 +224,7 @@
<template #label>
<span>
<el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top">
<el-icon>
<question-filled />
</el-icon>
<el-icon><question-filled /></el-icon>
</el-tooltip>
菜单状态
</span>
@ -264,6 +252,20 @@
import { addMenu, delMenu, getMenu, listMenu, updateMenu } from '@/api/system/menu';
import { MenuForm, MenuQuery, MenuVO } from '@/api/system/menu/types';
import { MenuTypeEnum } from '@/enums/MenuTypeEnum';
import { ElFormInstance, ElTableInstance } from 'element-plus';
import { ComponentInternalInstance, getCurrentInstance, onMounted, ref, reactive, toRefs } from 'vue';
// 类型定义
interface DialogOption {
visible: boolean;
title: string;
}
interface PageData<T, U> {
form: T;
queryParams: U;
rules: Record<string, any[]>;
}
interface MenuOptionsType {
menuId: number;
@ -271,20 +273,29 @@ interface MenuOptionsType {
children: MenuOptionsType[] | undefined;
}
// 全局变量
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_show_hide, sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_show_hide', 'sys_normal_disable'));
// 1. 存储完整数据的数组(一次性获取后本地保存)
const fullMenuList = ref<MenuVO[]>([]);
// 2. 子节点缓存(优化查询性能)
const childrenCache = ref<Record<number, MenuVO[]>>({});
// 页面状态
const menuList = ref<MenuVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const menuOptions = ref<MenuOptionsType[]>([]);
const isExpandAll = ref(false);
// 弹窗状态
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
// 表单相关
const queryFormRef = ref<ElFormInstance>();
const menuFormRef = ref<ElFormInstance>();
const initFormData = {
@ -298,35 +309,114 @@ const initFormData = {
isFrame: '1',
isCache: '0',
visible: '0',
status: '0'
status: '0',
menuSource: 1
};
const data = reactive<PageData<MenuForm, MenuQuery>>({
const data = reactive({
form: { ...initFormData },
queryParams: {
menuName: undefined,
status: undefined
status: undefined,
menuSource: 1
},
rules: {
menuName: [{ required: true, message: '菜单名称不能为空', trigger: 'blur' }],
orderNum: [{ required: true, message: '菜单顺序不能为空', trigger: 'blur' }],
path: [{ required: true, message: '路由地址不能为空', trigger: 'blur' }]
path: [
{
required: (() => data.form.menuType !== 'F') as any,
message: '路由地址不能为空',
trigger: 'blur'
}
]
}
});
const menuTableRef = ref<ElTableInstance>();
const { queryParams, form, rules } = toRefs<PageData<MenuForm, MenuQuery>>(data);
/** 查询菜单列表 */
/**
* 初始化加载:一次性获取所有数据并处理
* 仅在表格展示根节点,子节点通过懒加载从本地数据中筛选
*/
const getList = async () => {
loading.value = true;
const res = await listMenu(queryParams.value);
const data = proxy?.handleTree<MenuVO>(res.data, 'menuId');
if (data) {
menuList.value = data;
}
try {
// 1. 一次性获取所有数据
const res = await listMenu(queryParams.value);
fullMenuList.value = res.data || [];
loading.value = false;
// 2. 构建缓存(提前计算所有节点的子节点)
buildChildrenCache();
console.log(11111);
// 3. 只展示根节点parentId=0
menuList.value = filterChildren(0);
} catch (err) {
proxy?.$modal.msgError('加载菜单失败');
} finally {
loading.value = false;
}
};
/**
* 构建子节点缓存
* 一次性处理所有数据建立父ID到子节点列表的映射
*/
const buildChildrenCache = () => {
const cache: Record<number, MenuVO[]> = {};
// 初始化所有可能的父ID
fullMenuList.value.forEach((item) => {
if (!cache[item.parentId!]) {
cache[item.parentId!] = [];
}
});
// 为每个父ID添加子节点
fullMenuList.value.forEach((item) => {
if (cache[item.parentId!]) {
// 标记节点是否有子节点(用于显示展开图标)
const hasChildren = fullMenuList.value.some((child) => child.parentId == item.menuId);
cache[item.parentId!].push({ ...item, hasChildren });
}
});
// 按排序号排序子节点
// Object.keys(cache).forEach((parentId) => {
// cache[Number(parentId)].sort((a, b) => (a.orderNum || 0) - (b.orderNum || 0));
// });
childrenCache.value = cache;
};
/**
* 从缓存中筛选指定父节点的子节点
* @param parentId 父节点ID
* @returns 子节点列表
*/
const filterChildren = (parentId: number): MenuVO[] => {
return childrenCache.value[parentId] || [];
};
/**
* 懒加载子节点实现
* 从本地缓存中获取子节点,不发起额外请求
*/
const loadChildren = async (row, treeNode: any, resolve: (data: MenuVO[]) => void) => {
const parentId = row.menuId;
console.log(childrenCache.value);
// 从缓存获取子节点并返回
resolve(filterChildren(parentId));
};
/**
* 判断节点是否有子节点
* @param row 节点数据
* @returns 是否有子节点
*/
const hasChildren = (row: MenuVO) => {
return row.hasChildren || false;
};
/** 查询菜单下拉树结构 */
const getTreeselect = async () => {
menuOptions.value = [];
@ -335,11 +425,13 @@ const getTreeselect = async () => {
menu.children = proxy?.handleTree<MenuOptionsType>(response.data, 'menuId');
menuOptions.value.push(menu);
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
@ -350,11 +442,13 @@ const reset = () => {
const handleQuery = () => {
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 新增按钮操作 */
const handleAdd = (row?: MenuVO) => {
reset();
@ -363,18 +457,24 @@ const handleAdd = (row?: MenuVO) => {
dialog.visible = true;
dialog.title = '添加菜单';
};
/** 展开/折叠操作 */
const handleToggleExpandAll = () => {
isExpandAll.value = !isExpandAll.value;
toggleExpandAll(menuList.value, isExpandAll.value);
};
/** 展开/折叠所有 */
/** 展开/折叠所有节点 */
const toggleExpandAll = (data: MenuVO[], status: boolean) => {
data.forEach((item: MenuVO) => {
menuTableRef.value?.toggleRowExpansion(item, status);
if (item.children && item.children.length > 0) toggleExpandAll(item.children, status);
// 如果有子节点,递归处理
if (item.hasChildren) {
toggleExpandAll(filterChildren(item.menuId!), status);
}
});
};
/** 修改按钮操作 */
const handleUpdate = async (row: MenuVO) => {
reset();
@ -386,25 +486,38 @@ const handleUpdate = async (row: MenuVO) => {
dialog.visible = true;
dialog.title = '修改菜单';
};
/** 提交按钮 */
const submitForm = () => {
menuFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
form.value.menuId ? await updateMenu(form.value) : await addMenu(form.value);
// 保存数据到后端
if (form.value.menuId) {
await updateMenu(form.value);
} else {
await addMenu(form.value);
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
// 重新加载所有数据并重建缓存
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row: MenuVO) => {
await proxy?.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?');
await delMenu(row.menuId);
// 重新加载所有数据并重建缓存
await getList();
proxy?.$modal.msgSuccess('删除成功');
};
// 初始化加载所有数据
onMounted(() => {
getList();
});

View File

@ -1,7 +1,6 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter"
:leave-active-class="proxy?.animate.searchAnimate.leave">
<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">
@ -13,19 +12,11 @@
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="角色状态" clearable>
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label"
:value="dict.value" />
<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"
v-hasPermi="['system:role:query']">搜索</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>
@ -37,9 +28,18 @@
<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-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">
@ -47,25 +47,21 @@
<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-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-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-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 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" />
@ -74,8 +70,7 @@
<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-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">
@ -87,27 +82,28 @@
<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-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-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-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-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" />
<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>
@ -115,8 +111,15 @@
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
<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 }" @change="">
<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">
@ -138,23 +141,26 @@
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label
}}</el-radio>
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="菜单权限">
<el-checkbox v-model="menuExpand"
@change="handleCheckedTreeExpand(Boolean($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 ref="menuRef" class="tree-border" :data="menuOptions" show-checkbox node-key="id"
:check-strictly="!form.menuCheckStrictly" empty-text="加载中请稍候"
:props="{ label: 'label', children: 'children' }"></el-tree>
<el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event, 'menu')">父子联动</el-checkbox>
<el-tree
ref="menuRef"
class="tree-border"
:data="menuOptions"
show-checkbox
node-key="id"
:check-strictly="!form.menuCheckStrictly"
empty-text="加载中请稍候"
: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-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>
@ -179,19 +185,24 @@
</el-form-item>
<el-form-item label="权限范围">
<el-select v-model="form.dataScope" @change="dataScopeSelectChange">
<el-option v-for="item in dataScopeOptions" :key="item.value" :label="item.label"
:value="item.value"></el-option>
<el-option v-for="item in dataScopeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item v-show="form.dataScope === '2'" label="数据权限">
<el-checkbox v-model="deptExpand"
@change="handleCheckedTreeExpand(Boolean($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 ref="deptRef" class="tree-border" :data="deptOptions" show-checkbox default-expand-all node-key="id"
:check-strictly="!form.deptCheckStrictly" empty-text="加载中请稍候"
:props="{ label: 'label', children: 'children' }"></el-tree>
<el-checkbox v-model="form.deptCheckStrictly" @change="handleCheckedTreeConnect($event, 'dept')">父子联动</el-checkbox>
<el-tree
ref="deptRef"
class="tree-border"
:data="deptOptions"
show-checkbox
default-expand-all
node-key="id"
:check-strictly="!form.deptCheckStrictly"
empty-text="加载中请稍候"
:props="{ label: 'label', children: 'children' }"
></el-tree>
</el-form-item>
</el-form>
<template #footer>
@ -252,7 +263,7 @@ const menuRef = ref<ElTreeInstance>();
const deptRef = ref<ElTreeInstance>();
const deptTreeRef = ref<ElTreeInstance>();
const initForm: RoleForm = {
const initForm = {
roleId: undefined,
roleSort: 1,
status: '0',
@ -265,10 +276,11 @@ const initForm: RoleForm = {
menuIds: [],
deptId: '',
isSpecial: null,
deptIds: []
deptIds: [],
roleSource: '1'
};
const data = reactive<PageData<RoleForm, RoleQuery>>({
const data = reactive({
form: { ...initForm },
queryParams: {
pageNum: 1,
@ -276,7 +288,8 @@ const data = reactive<PageData<RoleForm, RoleQuery>>({
roleName: '',
roleKey: '',
deptId: '',
status: ''
status: '',
roleSource: '1'
},
rules: {
roleName: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }],
@ -377,7 +390,7 @@ const handleAuthUser = (row: RoleVO) => {
/** 查询菜单树结构 */
const getMenuTreeselect = async () => {
const res = await menuTreeselect();
const res = await menuTreeselect({ menuSource: '1' });
menuOptions.value = res.data;
};
/** 所有部门节点数据 */
@ -427,14 +440,14 @@ 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;
};

View File

@ -1,6 +1,6 @@
<template>
<div class="p-2 editInfo">
<el-form label-position="top" ref="userFormRef" :model="form" :rules="rules" label-width="80px">
<el-form label-position="top" ref="userFormRef" :model="form" :rules="rules" label-width="80px" size="large">
<el-row>
<el-col :span="10">
<el-form-item label="用户昵称" prop="nickName">
@ -70,15 +70,15 @@
</el-col>
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
<el-input v-model="form.remark" type="textarea" :autosize="{ minRows: 3, maxRows: 3 }" placeholder="请输入内容"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- <div class="box_submit">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel()"> </el-button>
</div> -->
<div class="box_submit">
<el-button size="large" @click="cancel()"> </el-button>
<el-button size="large" type="primary" @click="submitForm"> </el-button>
</div>
</div>
</template>
@ -247,12 +247,12 @@ defineExpose({ open, getInfoForm });
</script>
<style lang="scss" scoped>
.editInfo {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
.box_submit {
position: absolute;
right: 10px;
bottom: 10px;
align-self: flex-end;
display: flex;
gap: 10px;
}
}
</style>

View File

@ -11,7 +11,7 @@
</el-table>
</div>
</div>
<div class="title_detail" style="margin-top: 20px">
<div class="title_detail" style="margin-top: 10px">
<span>选择或修改当前角色信息</span>
<div style="margin-top: 10px" class="box_detail">
<!-- 项目列表选择区 -->
@ -34,7 +34,7 @@
</div>
<!-- 角色分配区 -->
<div class="post_list">
<span>关联项目角色分配</span>
<div class="list_title">关联项目角色分配</div>
<div v-if="selectedProjects.length === 0" class="no-selection">请从左侧选择项目进行角色分配</div>
<div v-for="(project, index) in selectedProjects" :key="project.id" class="project-role-container">
<div class="project-header">
@ -64,8 +64,8 @@
</div>
</div>
<div class="box_submit">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel()"> </el-button>
<el-button size="large" @click="cancel()"> </el-button>
<el-button size="large" type="primary" @click="submitForm">保存角色信息</el-button>
</div>
</div>
</template>
@ -219,6 +219,10 @@ const removeProject = (projectId: number | string) => {
// 提交表单
const submitForm = async () => {
// 整理项目角色数据
if (form.value.projectRoles.length == 0) {
proxy?.$modal.msgWarning('请选择项目角色');
return;
}
form.value.projectRoles = selectedProjects.value.map((project) => ({
projectId: project.id,
roleIds: [...new Set(project.webRoles), ...new Set(project.appRoles)]
@ -273,7 +277,6 @@ const open = async (row?: any, deptId?: any) => {
resetForm();
await initData();
deptName.value = row.deptName;
console.log(row);
if (row) {
try {
if (!row.createTime) {
@ -283,19 +286,20 @@ const open = async (row?: any, deptId?: any) => {
deptId = form.value.deptId;
}
const roleRes = await getRoleList(deptId);
allRoles.value = roleRes.data.filter((item: Role) => item.roleSort === 1);
AppRoles.value = roleRes.data.filter((item: Role) => item.roleSort !== 1);
allRoles.value = roleRes.data.filter((item: Role) => item.roleSource == '1');
AppRoles.value = roleRes.data.filter((item: Role) => item.roleSource != '1');
} else {
const { data } = await api.getUser(row.userId);
Object.assign(form.value, data.user);
// const { data } = await api.getUser(row.userId);
const data = row;
Object.assign(form.value, row);
// 获取角色列表
if (form.value.deptId) {
deptId = form.value.deptId;
}
const roleRes = await getRoleList(deptId);
// 区分web端和app端角色
allRoles.value = roleRes.data.filter((item: Role) => item.roleSort === 1);
AppRoles.value = roleRes.data.filter((item: Role) => item.roleSort !== 1);
allRoles.value = roleRes.data.filter((item: Role) => item.roleSource == '1');
AppRoles.value = roleRes.data.filter((item: Role) => item.roleSource != '1');
// 加载项目角色数据
if (data.projectRoles && data.projectRoles.length) {
data.projectRoles.forEach((pr: any) => {
@ -324,10 +328,12 @@ defineExpose({ open });
<style lang="scss">
.roleInfo {
position: relative;
height: 100%;
padding-bottom: 60px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 12px;
.title_detail {
> span {
@ -355,7 +361,7 @@ defineExpose({ open });
.project_list {
width: 320px;
background-color: #fff;
border: 1px solid #eee;
.project-items {
margin-top: 10px;
@ -370,8 +376,7 @@ defineExpose({ open });
border: 1px solid #eee;
&:hover {
background-color: #f5f7fa;
border-color: #e4e7ed;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.project-item-content {
@ -399,7 +404,11 @@ defineExpose({ open });
.post_list {
flex: 1;
background-color: #fff;
border: 1px solid #eee;
.list_title {
margin-bottom: 10px;
}
.no-selection {
height: 100%;
@ -413,9 +422,9 @@ defineExpose({ open });
.project-role-container {
padding: 15px;
margin-bottom: 15px;
background-color: #f9f9f9;
// background-color: #f9f9f9;
border-radius: 4px;
border: 1px solid #f0f0f0;
border: 1px solid rgb(229 231 235);
.project-header {
display: flex;
@ -441,7 +450,9 @@ defineExpose({ open });
.role-assignment {
.role-group {
margin-bottom: 15px;
display: flex;
flex-direction: column;
margin-bottom: 10px;
.role-label {
display: inline-block;
@ -467,9 +478,7 @@ defineExpose({ open });
}
.box_submit {
position: absolute;
right: 20px;
bottom: 0px;
align-self: flex-end;
display: flex;
gap: 10px;
}

View File

@ -154,11 +154,17 @@
</el-row>
<!-- 添加或修改用户配置对话框 -->
<el-dialog draggable ref="formDialogRef" v-model="dialog.visible" :title="dialog.title" width="1300px" append-to-body @close="closeDialog">
<el-dialog draggable ref="formDialogRef" style="background: rgb(249,250,251);" v-model="dialog.visible" :title="dialog.title" width="1300px" append-to-body @close="closeDialog">
<div class="boxDetial">
<div class="tab_info">
<span @click="onTab(1)" :class="{ active: type == 1 }">基本资料</span>
<span @click="onTab(2)" :class="{ active: type == 2 }">角色信息</span>
<div class="tab_item" @click="onTab(1)" :class="{ active: type == 1 }">
<Avatar style="width: 1em; height: 1em; margin-right: 8px" :style="{color: type == 1 ? '#1890ff' : '#000'}" />
<span>基本资料</span>
</div>
<div class="tab_item" @click="onTab(2)" :class="{ active: type == 2 }">
<Key style="width: 1em; height: 1em; margin-right: 8px" :style="{color: type == 2 ? '#1890ff' : '#000'}" />
<span>角色信息</span>
</div>
</div>
<div class="tab_content" v-show="type == 1">
<editInfo ref="editInfoRef" @close="dialog.visible = false" @submit="getList" @setDeptId="setDeptId"></editInfo>
@ -232,6 +238,7 @@ import ShuttleFrame from '../../project/projectRelevancy/component/ShuttleFrame.
import { listProject } from '@/api/project/project';
import editInfo from './comm/editInfo.vue';
import roleInfo from './comm/roleInfo.vue';
import { color } from 'echarts';
const router = useRouter();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable, sys_user_sex } = toRefs<any>(proxy?.useDict('sys_normal_disable', 'sys_user_sex'));
@ -690,7 +697,7 @@ const uploadCert = async () => {
certDialog.value = false;
proxy?.$modal.msgSuccess('上传证书目录成功');
};
const onTab = (val) => {
const onTab = (val: number) => {
type.value = val;
if (val == 2) {
let obj = editInfoRef.value?.getInfoForm();
@ -706,21 +713,32 @@ const onTab = (val) => {
.boxDetial {
display: flex;
justify-content: space-between;
align-items: center;
align-items: start;
height: 680px;
gap: 20px;
.tab_info {
height: 100%;
height: 40%;
width: 200px;
background: #f0f2f5;
padding: 10px;
background: #fff;
padding: 30px 10px;
text-align: center;
display: flex;
flex-direction: column;
border-radius: 0.5rem;
font-size: 18px;
> span {
.tab_item {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
margin-bottom: 10px;
padding: 5px 0;
cursor: pointer;
}
.tab_item:hover {
background-color: rgb(249, 250, 251);
}
.active {
color: #1890ff;
}
@ -729,7 +747,8 @@ const onTab = (val) => {
height: 100%;
width: calc(100% - 200px);
padding: 20px 10px;
background: #ccc;
background: #fff;
border-radius: 0.5rem;
}
}
</style>

View File

@ -17,7 +17,7 @@
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleToggleExpandAll">{{ isExpandAll ? '一键收起' : '一键展开' }}</el-button>
<el-button type="primary" @click="toggleExpandAll">{{ isExpandAll ? '一键收起' : '一键展开' }}</el-button>
</el-form-item>
<el-form-item>
@ -58,20 +58,13 @@
</el-card>
</transition>
<el-card shadow="never" class="mb8">
<el-table
ref="tableRef"
v-loading="loading"
:data="tableData"
row-key="id"
border
lazy
:default-expand-all="isExpandAll"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table ref="tableAllRef" v-loading="loading" :data="tableData" row-key="id" border lazy :expand-row-keys="expandRowKeys">
<el-table-column prop="num" label="编号" />
<el-table-column prop="name" label="工程或费用名称" />
<el-table-column prop="unit" label="单位" />
<el-table-column prop="quantity" label="数量">
<el-table-column prop="unit" label="单位" align="center" />
<el-table-column prop="specification" label="规格" align="center" />
<el-table-column prop="quantity" label="数量" align="center">
<template #default="scope">
{{ scope.row.children.length > 0 ? '' : scope.row.quantity }}
</template>
@ -99,6 +92,11 @@
{{ scope.row.price != 0 ? Number(scope.row.price).toFixed(2) : null }}
</template>
</el-table-column>
<el-table-column prop="taxRate" label="税率" width="100">
<template #default="scope">
{{ scope.row.taxRate !== false ? scope.row.taxRate : '' }}
</template>
</el-table-column>
<el-table-column prop="operate" label="操作" align="center">
<template #default="scope">
<el-button
@ -120,68 +118,141 @@
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { getCurrentInstance, ComponentInternalInstance } from 'vue';
import { ElMessage } from 'element-plus';
import { useUserStoreHook } from '@/store/modules/user';
import { obtainAllVersionNumbers, sheetList, getTableList, updatePrice, importExcelFile } from '@/api/tender/index';
// 类型定义
interface QueryForm {
versions: string;
sheet: string;
}
interface TableRow {
id: string | number;
num?: string;
name?: string;
unit?: string;
taxRate?: string | number;
specification?: string;
quantity?: number;
unitPrice?: number;
price?: number;
remark?: string;
children?: TableRow[];
}
interface VersionItem {
versions: string;
status: string;
id?: string | number;
[key: string]: any;
}
// 实例与状态初始化
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const userStore = useUserStoreHook();
const currentProject = computed(() => userStore.selectedProject);
// 标签页配置
const tabList = [
{
label: '招采工程量清单',
value: '2'
},
{
label: '物资设备清单',
value: '3'
}
{ label: '招采工程量清单', value: '2' },
{ label: '物资设备清单', value: '3' }
];
const queryForm = ref({
versions: '',
sheet: ''
});
const versionsData = ref<any>({});
// 响应式状态
const queryForm = ref<QueryForm>({ versions: '', sheet: '' });
const versionsData = ref<VersionItem>({});
const activeTab = ref('2');
const sheets = ref([]);
const options = ref([]);
const tableData = ref([]);
const tableRef = ref();
const sheets = ref<string[]>([]);
const options = ref<VersionItem[]>([]);
const tableData = ref<TableRow[]>([]);
const tableAllRef = ref<any>(null);
const uploadRef = ref<any>(null);
const isExpandAll = ref(true);
const loading = ref(false);
const versionMap = new Map();
const versionMap = new Map<string, VersionItem>();
const expandRowKeys = ref<string[] | number[]>([]); // 控制表格展开的行ID
const modifyPrice = new Map<string | number, TableRow>();
// 切换tab
// 切换标签页
const handleTabChange = (tab: string) => {
activeTab.value = tab;
tableData.value = [];
versionsData.value = {};
isExpandAll.value = true;
expandRowKeys.value = [];
getVersionNums();
};
//切换版本
const changeVersions = (value) => {
versionsData.value = options.value.find((item) => item.versions == value);
// 切换版本号
const changeVersions = (value: string) => {
versionsData.value = options.value.find((item) => item.versions === value) || {};
getSheetName();
};
//切换表格
const changeSheet = (val: any) => {
// 切换表名
const changeSheet = (val: string) => {
getTableData();
};
const handleToggleExpandAll = () => {
// 一键展开/收起
const toggleExpandAll = async () => {
isExpandAll.value = !isExpandAll.value;
toggleExpandAll(tableData.value, isExpandAll.value);
if (!tableData.value.length || !tableAllRef.value) return;
if (isExpandAll.value) {
// 收集所有已加载节点的ID
const allExpandIds = collectAllNodeIds(tableData.value);
expandRowKeys.value = allExpandIds;
// 处理懒加载节点
await loadAndExpandAllLazyNodes();
} else {
// 全部收起
expandRowKeys.value = [];
}
};
//展开树
const toggleExpandAll = (data: any[], status: boolean) => {
data.forEach((item) => {
tableRef.value[0]?.toggleRowExpansion(item, status);
if (item.children && item.children.length > 0) toggleExpandAll(item.children, status);
// 辅助函数递归收集所有节点ID包括子节点
const collectAllNodeIds = (nodes: TableRow[]): (string | number)[] => {
let ids: (string | number)[] = [];
nodes.forEach((node) => {
ids.push(node.id);
if (node.children && node.children.length > 0) {
ids = [...ids, ...collectAllNodeIds(node.children)];
}
});
return ids;
};
//获取版本号
// 辅助函数:加载并展开所有懒加载子节点
const loadAndExpandAllLazyNodes = async () => {
if (!tableAllRef.value) return;
try {
// 获取所有可能有子节点的父节点
const parentNodes = tableData.value.filter((node) => node.hasChildren || (node.children && node.children.length === 0));
if (!parentNodes.length) return;
// 逐个加载并展开子节点
for (const parent of parentNodes) {
if (tableAllRef.value.loadOrToggleRow) {
await tableAllRef.value.loadOrToggleRow(parent);
}
await nextTick();
// 递归展开子节点的子节点
if (parent.children && parent.children.length > 0) {
const childIds = collectAllNodeIds(parent.children);
expandRowKeys.value = [...new Set([...expandRowKeys.value, ...childIds])];
}
}
} catch (error) {
console.error('加载并展开懒加载节点失败:', error);
} finally {
}
};
// 获取版本号列表
const getVersionNums = async () => {
try {
const params = {
@ -192,93 +263,126 @@ const getVersionNums = async () => {
};
const res = await obtainAllVersionNumbers(params);
if (res.code == 200) {
if (res.code === 200) {
options.value = res.data;
if (res.data.length > 0) {
res.data.forEach((item: any) => {
versionMap.set(item.versions, item);
});
queryForm.value.versions = res.data[0].versions;
versionsData.value = options.value.find((item) => item.versions == queryForm.value.versions);
versionMap.clear();
options.value.forEach((item) => versionMap.set(item.versions, item));
if (options.value.length > 0) {
queryForm.value.versions = options.value[0].versions;
versionsData.value = options.value[0];
getSheetName();
} else {
queryForm.value.versions = '';
// getSheetName();
sheets.value = [];
tableData.value = [];
expandRowKeys.value = [];
}
}
} catch (error) {
console.log(error);
console.error('获取版本号失败:', error);
ElMessage.error('获取版本号失败,请刷新重试');
}
};
//获取表名
// 获取表名列表
const getSheetName = async () => {
try {
const params = {
projectId: currentProject.value?.id,
versions: queryForm.value.versions
};
const res = await sheetList(params);
if (res.code == 200) {
if (res.code === 200) {
sheets.value = res.data;
if (res.data.length > 0) {
queryForm.value.sheet = res.data[0];
if (sheets.value.length > 0) {
queryForm.value.sheet = sheets.value[0];
} else {
queryForm.value.sheet = '';
tableData.value = [];
expandRowKeys.value = [];
}
getTableData();
}
} catch (error) {
console.log(error);
console.error('获取表名失败:', error);
ElMessage.error('获取表名失败,请刷新重试');
}
};
//获取表格数据
// 获取表格数据
const getTableData = async () => {
try {
loading.value = true;
const params = {
projectId: currentProject.value?.id,
versions: queryForm.value.versions,
sheet: queryForm.value.sheet,
type: activeTab.value
};
const res = await getTableList(params);
if (res.code == 200) {
if (res.code === 200) {
tableData.value = res.data;
if (isExpandAll.value) {
// 展开全部
isExpandAll.value = false;
toggleExpandAll();
}
modifyPrice.clear();
}
} catch (error) {
console.log(error);
console.error('获取表格数据失败:', error);
ElMessage.error('获取表格数据失败,请刷新重试');
} finally {
loading.value = false;
}
};
//导入
const importExcel = (options: any): any => {
let formData = new FormData();
// 导入Excel
const importExcel = (options: any): void => {
if (!queryForm.value.versions || (activeTab.value !== '3' && !queryForm.value.sheet)) {
ElMessage.warning('请先选择版本号和表名(物资设备清单无需选择表名)');
return;
}
const formData = new FormData();
formData.append('file', options.file);
loading.value = true;
importExcelFile(
{ projectId: currentProject.value?.id, sheet: queryForm.value.sheet, versions: queryForm.value.versions, type: activeTab.value },
{
projectId: currentProject.value?.id,
sheet: queryForm.value.sheet,
versions: queryForm.value.versions,
type: activeTab.value
},
formData
)
.then((res) => {
const { code } = res;
if (code == 200) {
proxy.$modal.msgSuccess(res.msg || '导入成功');
if (res.code === 200) {
proxy?.$modal.msgSuccess(res.msg || '导入成功');
getTableData();
} else {
proxy.$modal.msgError(res.msg || '导入失败');
proxy?.$modal.msgError(res.msg || '导入失败');
}
})
.catch((err) => {
proxy.$modal.msgError(err.msg || '导入失败');
proxy?.$modal.msgError(err.msg || '导入失败');
})
.finally(() => {
loading.value = false;
});
};
//导出
// 导出Excel
const handleExport = () => {
if (!queryForm.value.versions || (activeTab.value !== '3' && !queryForm.value.sheet)) {
ElMessage.warning('请先选择版本号和表名(物资设备清单无需选择表名)');
return;
}
proxy?.download(
'/tender/tenderPlanLimitList/export',
{
@ -287,90 +391,108 @@ const handleExport = () => {
versions: queryForm.value.versions,
type: activeTab.value
},
`招标一览${queryForm.value.sheet}.xlsx`
`招标一览_${queryForm.value.sheet || (activeTab.value === '3' ? '物资设备清单' : '')}_v${queryForm.value.versions}.xlsx`
);
};
const modifyPrice = new Map();
const changePrice = (row: any) => {
modifyPrice.set(row.id, row);
// if (!row.unitPrice) {
// modifyPrice.delete(row.id);
// }
};
//修改单价
const handleSave = (row?: any, type?: any) => {
try {
if (type == 'single') {
loading.value = true;
const list = [{ ...row, type: activeTab.value }];
updatePrice(list).then((res) => {
if (res.code == 200) {
ElMessage({
message: '修改成功',
type: 'success'
});
getTableData();
}
});
}
if (type == 'all') {
loading.value = true;
const list = [];
modifyPrice.forEach((item) => {
list.push({ ...item, type: activeTab.value });
});
updatePrice(list).then((res) => {
if (res.code == 200) {
ElMessage({
message: '修改成功',
type: 'success'
});
getTableData();
}
});
}
} catch (error) {
ElMessage({
message: '修改失败',
type: 'error'
});
} finally {
loading.value = false;
// 记录待修改价格的行
const changePrice = (row: TableRow) => {
if (row.id && row.unitPrice !== undefined) {
modifyPrice.set(row.id, row);
} else if (row.id) {
modifyPrice.delete(row.id);
}
};
/** 审核按钮操作 */
const handleAudit = async () => {
let id = versionMap.get(queryForm.value.versions).id;
console.log(id);
if (activeTab.value == '2') {
// 保存价格修改
const handleSave = (row?: TableRow, type?: 'single' | 'all') => {
if (versionsData.value.status !== 'draft') {
ElMessage.warning('仅草稿状态可修改单价');
return;
}
let updateList: TableRow[] = [];
if (type === 'single' && row?.id) {
updateList = [{ ...row, type: activeTab.value }];
} else if (type === 'all') {
updateList = Array.from(modifyPrice.values()).map((item) => ({ ...item, type: activeTab.value }));
if (updateList.length === 0) {
ElMessage.warning('暂无待修改的单价数据');
return;
}
}
loading.value = true;
updatePrice(updateList)
.then((res) => {
if (res.code === 200) {
ElMessage.success('修改成功');
getTableData();
modifyPrice.clear();
} else {
ElMessage.error(res.msg || '修改失败');
}
})
.catch((err) => {
ElMessage.error(err.msg || '修改失败');
})
.finally(() => {
loading.value = false;
});
};
// 审核/查看流程
const handleAudit = () => {
const versionItem = versionMap.get(queryForm.value.versions);
if (!versionItem?.id) {
ElMessage.warning('请先选择有效的版本号');
return;
}
if (activeTab.value === '2') {
proxy?.$tab.openPage('/approval/tenderBidd/indexEdit', '招采工程量清单审核', {
id: id,
id: versionItem.id,
type: 'update'
});
}
if (activeTab.value == '3') {
} else if (activeTab.value === '3') {
proxy?.$tab.openPage('/approval/tenderBidd/indexEdit2', '物资设备清单审核', {
id: id,
id: versionItem.id,
type: 'update'
});
}
};
//监听项目id刷新数据
// 监听项目切换
const listeningProject = watch(
() => currentProject.value?.id,
(nid, oid) => {
getVersionNums();
(newId, oldId) => {
if (newId && newId !== oldId) {
getVersionNums();
} else {
tableData.value = [];
options.value = [];
sheets.value = [];
queryForm.value = { versions: '', sheet: '' };
versionsData.value = {};
expandRowKeys.value = [];
versionMap.clear();
modifyPrice.clear();
}
}
);
onUnmounted(() => {
listeningProject();
});
// 生命周期钩子
onMounted(() => {
getVersionNums();
});
onUnmounted(() => {
listeningProject();
});
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.mb8 {
margin-bottom: 8px;
}
</style>

View File

@ -195,7 +195,9 @@
(scope.row.useQuantity ? Number(scope.row.useQuantity) : 0) -
(scope.row.selectNum ? Number(scope.row.selectNum) : 0) ==
0
? ''
? activeTab == 2
? 0
: ''
: (scope.row.quantity ? Number(scope.row.quantity) : 0) -
(scope.row.selectNum ? Number(scope.row.selectNum) : 0) -
(scope.row.useQuantity ? Number(scope.row.useQuantity) : 0)
@ -613,17 +615,21 @@ const changeBiddingTime = (value: any, row: any) => {
};
//修改合同金额
const changeContractPrice = (value: any, row: any) => {
updateTenderPlan({
...row
}).then((res) => {
if (res.code == 200) {
ElMessage({
message: '修改成功',
type: 'success'
});
getList();
}
});
if (value <= Number(row.price)) {
updateTenderPlan({
...row
}).then((res) => {
if (res.code == 200) {
ElMessage({
message: '修改成功',
type: 'success'
});
getList();
}
});
} else {
ElMessage.error('合同金额不能大于限价金额');
}
};
//上传投标文件

View File

@ -104,8 +104,9 @@
<el-col :span="12">
<el-form-item label="近三年营业额" prop="pastThreeYears">
<el-input v-model="form.pastThreeYears" placeholder="请输入近三年营业额" clearable />
</el-form-item> </el-col
><el-col :span="12">
</el-form-item>
</el-col>
<!-- <el-col :span="12">
<el-form-item label="生产许可证编号" prop="safeCode">
<el-input v-model="form.safeCode" placeholder="请输入许可证编号" clearable />
</el-form-item>
@ -118,7 +119,7 @@
<el-form-item label="生产许可证发证日期" prop="safeCertificateValidity">
<el-date-picker v-model="form.safeCertificateValidity" type="date" placeholder="请选择发证日期" />
</el-form-item>
</el-col>
</el-col> -->
</el-row>
<el-row class="mb-4" v-if="form.supplierType === '劳务'">
<el-col :span="12">

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long