This commit is contained in:
tcy
2025-09-06 15:10:10 +08:00
92 changed files with 9494 additions and 2148 deletions

View File

@ -5,14 +5,20 @@ VITE_APP_TITLE = 煤科建管平台
VITE_APP_ENV = 'development' VITE_APP_ENV = 'development'
# 开发环境 # 开发环境
VITE_APP_BASE_API = 'http://192.168.110.149:8899'
# 李陈杰 209 # 李陈杰 209
<<<<<<< HEAD
VITE_APP_BASE_API = 'http://192.168.110.210:8899' VITE_APP_BASE_API = 'http://192.168.110.210:8899'
=======
# VITE_APP_BASE_API = 'http://192.168.110.209:8899'
>>>>>>> 3e4b85fc48f6514704aee4b444285b90935b0cd2
# 曾涛 # 曾涛
# 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.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' # VITE_APP_BASE_API = 'http://192.168.110.171:8899'

View File

@ -0,0 +1 @@

View File

@ -69,7 +69,8 @@
"vue-types": "5.1.3", "vue-types": "5.1.3",
"vue3-print-nb": "^0.1.4", "vue3-print-nb": "^0.1.4",
"vue3-scroll-seamless": "^1.0.6", "vue3-scroll-seamless": "^1.0.6",
"vxe-table": "4.5.22" "vxe-table": "4.5.22",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "9.15.0", "@eslint/js": "9.15.0",

Binary file not shown.

View File

@ -24,3 +24,11 @@ export const exportWord = (params) => {
method: 'post' method: 'post'
}); });
}; };
// 导出模版
export const exportExcel = (params) => {
return request({
url: '/design/collect/exportExcel',
method: 'post',
params: params
});
};

View File

@ -127,6 +127,17 @@ export const majorList = (params) => {
params: params params: params
}); });
}; };
/**
* 获取人员列表
* @param query
*/
export const copyUserList = (params) => {
return request({
url: '/design/volumeCatalog/copyUserList',
method: 'get',
params: params
});
};
/** /**
* 获取二维码信息 * 获取二维码信息
* @param query * @param query

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', method: 'get',
params: query params: query
}); });
}; /**
* 查询材料提供商
* @param query
* @returns {*}
*/
export const supplierInputGet = (query?) => {
return request({
url: '/supplierInput/supplierInput/getList',
method: 'get',
params: query
});
}; };
/** /**

View File

@ -75,3 +75,11 @@ export const inventoryList = (id: any) => {
method: 'get' method: 'get'
}); });
}; };
//获取材料表信息
export const getMaterialInfo = (id: any) => {
return request({
url: '/materials/materials/listByFormCode/' + id,
method: 'get'
});
};

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; remark: string;
} }
export interface MaterialsUseRecordForm extends BaseEntity { export interface MaterialsUseRecordForm extends BaseEntity {
@ -71,11 +70,9 @@ export interface MaterialsUseRecordForm extends BaseEntity {
* 备注 * 备注
*/ */
remark?: string; remark?: string;
} }
export interface MaterialsUseRecordQuery extends PageQuery { export interface MaterialsUseRecordQuery extends PageQuery {
/** /**
* 项目ID * 项目ID
*/ */
@ -105,7 +102,5 @@ export interface MaterialsUseRecordQuery extends PageQuery {
* 日期范围参数 * 日期范围参数
*/ */
params?: any; params?: any;
materialsId?: string | number;
} }

View File

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

View File

@ -203,3 +203,31 @@ export const importConstructionUserInfo = (file: string) => {
data: { file } 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; createTime: string;
sysUserId: string | number;
} }
export interface skipType { export interface skipType {
/** /**

View File

@ -195,3 +195,71 @@ export const changeProject = (id: string | number) => {
method: 'get' method: 'get'
}); });
}; };
/**
* 打卡规则
* @param id
*/
export const attendanceRuleEdit = (data) => {
return request({
url: '/project/attendanceRule',
method: 'put',
data
});
};
/**
* 打卡规则
* @param id
*/
export const attendanceRuleAdd = (data) => {
return request({
url: '/project/attendanceRule',
method: 'post',
data
});
};
/**
* 获取规则
* @param id
*/
export const byProjectIdDetail = (id) => {
return request({
url: '/project/attendanceRule/byProjectId/' + 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' 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; remark?: string;
/**
* 创建时间
*/
punchRangeList?: [];
} }
export interface ProjectTeamQuery extends PageQuery { 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' 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: string | number;
/**
* 父级ID
*/
parentId: string | number;
/** /**
* 项目ID * 项目ID
*/ */
@ -83,7 +88,6 @@ export interface LandTransferLedgerVO {
* 下一步策略 * 下一步策略
*/ */
nextStrategy: string; nextStrategy: string;
} }
export interface LandTransferLedgerForm extends BaseEntity { export interface LandTransferLedgerForm extends BaseEntity {
@ -92,6 +96,11 @@ export interface LandTransferLedgerForm extends BaseEntity {
*/ */
id?: string | number; id?: string | number;
/**
* 父级ID
*/
parentId: string | number;
/** /**
* 项目ID * 项目ID
*/ */
@ -171,11 +180,9 @@ export interface LandTransferLedgerForm extends BaseEntity {
* 下一步策略 * 下一步策略
*/ */
nextStrategy?: string; nextStrategy?: string;
} }
export interface LandTransferLedgerQuery extends PageQuery { export interface LandTransferLedgerQuery extends PageQuery {
/** /**
* 项目ID * 项目ID
*/ */
@ -261,6 +268,3 @@ export interface LandTransferLedgerQuery extends PageQuery {
*/ */
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({ return request({
url: '/system/menu/treeselect', url: '/system/menu/treeselect',
method: 'get' method: 'get',
params
}); });
}; };
// 根据角色ID查询菜单下拉树结构 // 根据角色ID查询菜单下拉树结构
export const roleMenuTreeselect = (roleId: string | number): AxiosPromise<RoleMenuTree> => { export const roleMenuTreeselect = (roleId: string | number, params?: any): AxiosPromise<RoleMenuTree> => {
return request({ return request({
url: '/system/menu/roleMenuTreeselect/' + roleId, url: '/system/menu/roleMenuTreeselect/' + roleId,
method: 'get' method: 'get',
params
}); });
}; };

View File

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

@ -26,11 +26,11 @@
<el-tooltip effect="dark" placement="bottom"> <el-tooltip effect="dark" placement="bottom">
<ProjectSelector /> <ProjectSelector />
</el-tooltip> </el-tooltip>
<!-- <el-tooltip content="搜索" effect="dark" placement="bottom"> <el-tooltip content="搜索" effect="dark" placement="bottom">
<div class="right-menu-item hover-effect" @click="openSearchMenu"> <div class="right-menu-item hover-effect" @click="openSearchMenu">
<svg-icon class-name="search-icon" icon-class="search" /> <svg-icon class-name="search-icon" icon-class="search" />
</div> </div>
</el-tooltip> --> </el-tooltip>
<!-- 消息 --> <!-- 消息 -->
<el-tooltip :content="proxy.$t('navbar.message')" effect="dark" placement="bottom"> <el-tooltip :content="proxy.$t('navbar.message')" effect="dark" placement="bottom">
<div> <div>

View File

@ -5,8 +5,6 @@ import { ProjectTeamVO } from '@/api/project/projectTeam/types';
import useUserStore from '@/store/modules/user'; import useUserStore from '@/store/modules/user';
export const getProjectTeam = async () => { export const getProjectTeam = async () => {
const isPermission = useUserStore().permissions.some((item) => item == 'project:team:list'); const isPermission = useUserStore().permissions.some((item) => item == 'project:team:list');
console.log(useUserStore().permissions);
if (!isPermission && useUserStore().permissions[0] != '*:*:*') return; if (!isPermission && useUserStore().permissions[0] != '*:*:*') return;
const { id } = $cache.local.getJSON('selectedProject'); const { id } = $cache.local.getJSON('selectedProject');

View File

@ -1,26 +1,27 @@
<template> <template>
<div class="centerPage"> <div class="centerPage">
<div class="topPage"> <div class="topPage">
<!-- 暂无 --> <div id="earth" style="width: 100%;height: 100%;"></div>
</div> </div>
<div class="endPage"> <div class="endPage" :class="{ 'slide-out-down': isHide }">
<Title title="AI安全巡检" :prefix="true" /> <Title title="AI安全巡检">
<img src="@/assets/projectLarge/robot.svg" alt="" height="20px" width="20px">
<div class="swiper"> </Title>
<div class="swiper" v-if="inspectionList.length">
<div class="arrow" :class="{ 'canUse': canLeft }" @click="swiperClick('left')"> <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 /> <ArrowLeft />
</el-icon> </el-icon>
</div> </div>
<div class="swiper_content" ref="swiperContent"> <div class="swiper_content" ref="swiperContent">
<div class="swiper_item" v-for="(item, index) in swiperList" :key="index"> <div class="swiper_item" v-for="(item, i) in inspectionList" :key="i">
<img src="@/assets/projectLarge/swiper.png" alt="" class="swiper_img"> <img :src="item.picture" alt="安全巡检" class="swiper_img">
<div class="swiper_date">{{ item.date }}</div> <div class="swiper_date">{{ item.createTime.slice(5) }}</div>
<div class="swiper_tip">{{ item.tip }}</div> <div class="swiper_tip">{{ item.label }}</div>
</div> </div>
</div> </div>
<div class="arrow" :class="{ 'canUse': canRight }" @click="swiperClick('right')"> <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 /> <ArrowRight />
</el-icon> </el-icon>
</div> </div>
@ -29,31 +30,39 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup>
import { ref } from "vue" import { ref, onMounted, toRefs, getCurrentInstance } from "vue"
import Title from './title.vue' import Title from './title.vue'
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
import { getScreenSafetyInspection } from '@/api/projectScreen'
const swiperList = ref([ const { proxy } = getCurrentInstance();
{ date: '03-18 15:00', tip: '未佩戴安全帽1' }, const { violation_level_type } = toRefs(proxy?.useDict('violation_level_type'));
{ 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 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 swiperItemWidth = ref(100)
const canLeft = ref(false) const canLeft = ref(false)
const canRight = ref(true) const canRight = ref(true)
const swiperClick = (direction: 'left' | 'right') => { const swiperClick = (direction) => {
if (!swiperContent.value) return
if (direction === 'right') { if (direction === 'right') {
if (swiperContent.value.scrollLeft >= swiperContent.value.scrollWidth - swiperContent.value.clientWidth) { if (swiperContent.value.scrollLeft >= swiperContent.value.scrollWidth - swiperContent.value.clientWidth) {
@ -70,10 +79,79 @@ const swiperClick = (direction: 'left' | 'right') => {
} }
swiperContent.value.scrollLeft -= swiperItemWidth.value 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(() => { onMounted(() => {
getInspectionList()
createEarth()
if (swiperContent.value && swiperContent.value.children.length > 0) {
swiperItemWidth.value = swiperContent.value.children[0].clientWidth + 20 swiperItemWidth.value = swiperContent.value.children[0].clientWidth + 20
}
}) })
</script> </script>
@ -82,24 +160,41 @@ onMounted(() => {
.centerPage { .centerPage {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 50vw;
height: 100%; height: 100%;
}
.topPage, .topPage,
.endPage { .endPage {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center;
width: 100%; width: 100%;
padding: 15px 0; padding: 15px 0;
border: 1px solid rgba(29, 214, 255, 0.1); border: 1px solid rgba(230, 247, 255, 0.1);
box-sizing: border-box; box-sizing: border-box;
} }
.topPage { .topPage {
flex: 1; flex: 1;
margin-bottom: 23px; 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 { .swiper {
@ -161,14 +256,20 @@ onMounted(() => {
.arrow { .arrow {
display: grid; display: grid;
place-items: center; place-items: center;
width: 20px; width: 24px;
height: 20px; height: 24px;
border-radius: 50%; border-radius: 50%;
border: 1px solid skyblue; border: 1px solid rgba(29, 214, 255, 0.3);
color: skyblue; color: skyblue;
cursor: pointer;
transition: all 0.3s ease;
&:canUse { &.canUse {
color: #000 !important; border: 1px solid rgba(29, 214, 255, 1);
}
&:hover:not(.canUse) {
opacity: 0.7;
} }
} }
</style> </style>

View File

@ -6,7 +6,7 @@
</div> </div>
<div style="font-size: 12px; padding-left: 10px">安全生产天数</div> <div style="font-size: 12px; padding-left: 10px">安全生产天数</div>
<div class="header_left_text"> <div class="header_left_text">
1,235 {{ safetyDay }}
<span style="font-size: 12px"></span> <span style="font-size: 12px"></span>
</div> </div>
</div> </div>
@ -14,16 +14,18 @@
<div>XXX智慧工地管理平台</div> <div>XXX智慧工地管理平台</div>
<div>XXX Smart Construction Stic Management Dashboard</div> <div>XXX Smart Construction Stic Management Dashboard</div>
</div> </div>
<div class="right"> <div class="header_right">
<div class="top-bar"> <div class="top-bar">
<!-- 左侧天气图标 + 日期文字 --> <!-- 左侧天气图标 + 日期文字 -->
<div class="left-section"> <div class="left-section">
<img src="@/assets/large/weather.png" alt="天气图标" /> <div class="weather-list" @mouseenter="requestPause" @mouseleave="resumeScroll">
<div v-for="(item, i) in weatherList" :key="i" class="weather-item"
<span> :style="{ transform: `translateY(-${offsetY}px)`, transition: transition }">
<span>多云 9°/18°</span> <img :src="`../../../src/assets/images/${item.icon}.png`" alt="" />
<span style="padding-left: 20px"> {{ week[date.week] }} ({{ date.ymd }})</span> <div>{{ item.weather }}{{ item.tempMin }}°/{{ item.tempMax }}°</div>
</span> <div>{{ item.week }}({{ item.date }})</div>
</div>
</div>
</div> </div>
<!-- 分割线 --> <!-- 分割线 -->
<div class="divider"> <div class="divider">
@ -35,48 +37,155 @@
<img src="@/assets/large/setting.png" alt="设置图标" /> <img src="@/assets/large/setting.png" alt="设置图标" />
<span>管理系统</span> <span>管理系统</span>
</div> </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> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const week = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']; import { ref, onMounted, onUnmounted } from 'vue';
const date = ref({ import { getScreenSafetyDay, getScreenWeather } from '@/api/projectScreen';
ymd: '',
hms: '',
week: 0
});
const setTime = () => { interface Weather {
let date1 = new Date(); week: string;
let year: any = date1.getFullYear(); date: string;
let month: any = date1.getMonth() + 1; icon: string;
let day: any = date1.getDate(); weather: string;
let hours: any = date1.getHours(); tempMax: string;
if (hours < 10) { tempMin: string;
hours = '0' + hours;
} }
let minutes: any = date1.getMinutes();
if (minutes < 10) { const props = defineProps({
minutes = '0' + minutes; projectId: {
type: String,
default: ''
},
isFull: {
type: Boolean,
default: false
} }
let seconds: any = date1.getSeconds(); })
if (seconds < 10) {
seconds = '0' + seconds; const emit = defineEmits(['changePage'])
}
date.value.ymd = year + '-' + month + '-' + day; const safetyDay = ref<number>(0);
date.value.hms = hours + ':' + minutes + ':' + seconds; const weatherList = ref<Weather[]>([])
date.value.week = date1.getDay(); 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 timer = setInterval(setTime, 1000); * 设置天气周期滑动
*/
const setWeatherScroll = () => {
curIndex.value += 1
transition.value = 'transform 0.3s ease';
offsetY.value = curIndex.value * 60
// 组件卸载时清除定时器 if (curIndex.value === weatherList.value.length - 1) {
onUnmounted(() => { setTimeout(() => {
clearInterval(timer); 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()
}
})
}); });
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value)
}
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -107,6 +216,12 @@ onUnmounted(() => {
} }
} }
.header_right {
width: 100%;
height: 100%;
display: flex;
}
.title { .title {
color: #fff; color: #fff;
font-family: 'AlimamaShuHeiTi', sans-serif; font-family: 'AlimamaShuHeiTi', sans-serif;
@ -124,12 +239,6 @@ onUnmounted(() => {
font-size: 14px; font-size: 14px;
} }
.right {
width: 100%;
height: 100%;
display: flex;
}
/* 顶部栏容器Flex 水平布局 + 垂直居中 */ /* 顶部栏容器Flex 水平布局 + 垂直居中 */
.top-bar { .top-bar {
width: 100%; width: 100%;
@ -137,7 +246,6 @@ onUnmounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
// background-color: #1e2128;
color: #fff; color: #fff;
padding: 8px 16px; padding: 8px 16px;
font-size: 14px; font-size: 14px;
@ -145,24 +253,37 @@ onUnmounted(() => {
/* 左侧区域(天气 + 日期):自身也用 Flex 水平排列,确保元素在一行 */ /* 左侧区域(天气 + 日期):自身也用 Flex 水平排列,确保元素在一行 */
.left-section { .left-section {
height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
// margin-right: auto; /* 让右侧元素(管理系统)居右 */
.weather-list {
height: 60px;
overflow: hidden;
.weather-item {
height: 60px;
line-height: 60px;
display: flex;
align-items: center;
&>div:last-child {
margin-left: 10px;
} }
.left-section img { img {
width: 32px; width: 50px;
height: 32px; height: 50px;
margin-right: 8px; }
/* 图标与文字间距 */ }
}
} }
/* 分割线(视觉分隔,可根据需求调整样式) */ /* 分割线(视觉分隔,可根据需求调整样式) */
.divider { .divider {
display: grid; display: grid;
grid-template-rows: 1fr 1fr; grid-template-rows: 1fr 1fr;
height: 100%; gap: 2px;
/* 根据需要调整高度 */
padding: 14px 10px; padding: 14px 10px;
} }
@ -195,4 +316,11 @@ onUnmounted(() => {
margin-right: 6px; margin-right: 6px;
/* 图标与文字间距 */ /* 图标与文字间距 */
} }
.change {
display: grid;
place-items: center;
margin-right: 10px;
cursor: pointer;
}
</style> </style>

View File

@ -2,30 +2,53 @@
<div class="leftPage"> <div class="leftPage">
<div class="topPage"> <div class="topPage">
<Title title="项目公告" /> <Title title="项目公告" />
<div class="content"> <div class="content">
<div class="content_item" v-for="item in 6" :key="item"> <div class="content_item" v-for="item in news" :key="item.id">
<div class="round"> <img src="@/assets/projectLarge/round.svg" alt="">
<div class="sub_round"></div> <div class="ellipsis">
</div> {{ item.title }}
<div class="ellipsis">2025年6月23日 重庆市两江新区广场前期准备与审批完毕区广场前期准备与审批完毕前期准备与审批完毕区广场前期准备与审批完毕</div> <span @click="showNewsDetail(item)" style="color: rgba(138, 149, 165, 1);">{{ item.id === newId ? '关闭' :
'查看' }}</span>
</div> </div>
</div> </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"> <div class="endPage">
<Title title="人员情况" /> <Title title="人员情况" />
<div class="map"> <div class="map">
<img src="@/assets/projectLarge/map.svg" alt=""> <img src="@/assets/projectLarge/map.svg" alt="">
<!-- <div ref="mapChartRef"></div> -->
</div> </div>
<div class="attendance_tag"> <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=""> <img src="@/assets/projectLarge/people.svg" alt="">
<div class="tag_title">{{ item.title }}</div> <div class="tag_title">出勤人</div>
<div class="tag_info"> <div class="tag_info">
{{ item.number }} {{ attendanceCount }}
<span style="font-size: 14px;">{{ index === 2 ? '%' : '人' }}</span> <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> </div>
</div> </div>
@ -37,11 +60,12 @@
<div class="attendance_item_title">出勤率</div> <div class="attendance_item_title">出勤率</div>
<div class="attendance_item_title">出勤时间</div> <div class="attendance_item_title">出勤时间</div>
</div> </div>
<div v-for="item in list" :key="item.title" class="attendance_item"> <div v-for="item in teamAttendanceList" :key="item.id" class="attendance_item">
<div class="attendance_item_title">{{ item.title }}</div> <div class="attendance_item_title">{{ item.teamName }}</div>
<div class="attendance_item_number">{{ item.number }} <span class="subfont">/{{ item.number }}</span></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_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> </div>
</div> </div>
@ -51,29 +75,101 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue" import { ref } from "vue"
import Title from './title.vue' import Title from './title.vue'
import { getScreenNews, getScreenPeople } from '@/api/projectScreen';
import { mapOption } from './optionList'
import * as echarts from 'echarts';
const list = ref([ const props = defineProps({
{ title: '智慧系统运维', number: 30, attendanceRate: 100, date: '2025-08-05 08:10' }, projectId: {
{ title: '智慧系统运维', number: 30, attendanceRate: 100, date: '2025-08-05 08:10' }, type: String,
{ title: '智慧系统运维', number: 30, attendanceRate: 100, date: '2025-08-05 08:10' }, default: ''
{ 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' },
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> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.leftPage { .leftPage {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: calc(25vw - 30px);
margin: 0 15px;
height: 100%; height: 100%;
.topPage, .topPage,
@ -115,11 +211,11 @@ const tagList = ref([
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 10px; gap: 10px;
// position: relative;
margin-bottom: 20px; margin-bottom: 20px;
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
color: rgba(230, 247, 255, 1); color: rgba(230, 247, 255, 1);
cursor: pointer;
.ellipsis { .ellipsis {
display: -webkit-box; display: -webkit-box;
@ -134,21 +230,8 @@ const tagList = ref([
margin-bottom: 0; margin-bottom: 0;
} }
.round { img {
display: grid;
place-items: center;
margin-top: 3px; 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; margin-top: 15px;
.tag_item { .tag_item {
width: 28%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
border: 1px dashed rgba(29, 214, 255, 0.3); border: 1px dashed rgba(29, 214, 255, 0.3);
padding: 10px 25px; padding: 10px;
.tag_info { .tag_info {
font-size: 20px; font-size: 20px;
@ -201,4 +285,45 @@ const tagList = ref([
.subfont { .subfont {
color: rgba(138, 149, 165, 1); 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> </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,172 +2,61 @@
<div class="leftPage"> <div class="leftPage">
<div class="topPage"> <div class="topPage">
<Title title="项目概况" /> <Title title="项目概况" />
<div class="content" v-html="generalize"></div>
<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> </div>
<div class="endPage"> <div class="endPage">
<!-- 饼图容器 -->
<Title title="形象进度" /> <Title title="形象进度" />
<div class="chart_container">
<div ref="pieChartRef" class="echart" /> <div ref="pieChartRef" class="echart" />
<!-- 折线图容器 -->
<div ref="lineChartRef" class="echart" /> <div ref="lineChartRef" class="echart" />
</div> </div>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from "vue" import { ref, onMounted, onUnmounted, nextTick } from "vue"
import Title from './title.vue' import Title from './title.vue'
import * as echarts from 'echarts'; 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); const pieChartRef = ref<HTMLDivElement | null>(null);
let pieChart: any = null; let pieChart: any = null;
const totalPercent = ref(0)
// 折线图相关 // 折线图相关
const lineChartRef = ref<HTMLDivElement | null>(null); const lineChartRef = ref<HTMLDivElement | null>(null);
let lineChart: any = null; let lineChart: any = null;
// 土地数据 折线图
const designAreaData = ref([])
const transferAreaData = ref([])
// 饼图数据 // 饼图数据
const pieData = [ const pieData = [
{ name: '桩点浇筑', value: 13 }, { label: 'areaPercentage', name: '厂区', value: 0 },
{ name: '水泥灌注', value: 7 }, { label: 'roadPercentage', name: '道路', value: 0 },
{ name: '箱变安装', value: 40 }, { label: 'collectorLinePercentage', name: '集电线路', value: 0 },
{ name: '支架安装', value: 20 }, { label: 'exportLinePercentage', name: '送出线路', value: 0 },
{ name: '组件安装', value: 20 }, { 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 = () => { const initPieChart = () => {
if (!pieChartRef.value) { if (!pieChartRef.value) {
console.error('未找到饼图容器元素'); console.error('未找到饼图容器元素');
return; return;
} }
pieOption.series.data = pieData
pieOption.graphic[0].style.text = totalPercent.value + '%'
pieChart = echarts.init(pieChartRef.value, null, { pieChart = echarts.init(pieChartRef.value, null, {
renderer: 'canvas', renderer: 'canvas',
useDirtyRect: false useDirtyRect: false
@ -181,6 +70,8 @@ const initLineChart = () => {
console.error('未找到折线图容器元素'); console.error('未找到折线图容器元素');
return; return;
} }
barOption.series[0].data = designAreaData.value
barOption.series[1].data = transferAreaData.value
lineChart = echarts.init(lineChartRef.value, null, { lineChart = echarts.init(lineChartRef.value, null, {
renderer: 'canvas', renderer: 'canvas',
useDirtyRect: false useDirtyRect: false
@ -194,11 +85,52 @@ const handleResize = () => {
if (lineChart) lineChart.resize(); 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(() => { onMounted(() => {
getScreenLandData()
getScreenImgProcessData()
getScreenGeneralizeData()
nextTick(() => { nextTick(() => {
initPieChart(); initPieChart();
initLineChart();
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
}); });
}); });
@ -221,8 +153,6 @@ onUnmounted(() => {
.leftPage { .leftPage {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: calc(25vw - 30px);
margin: 0 15px;
height: 100%; height: 100%;
.topPage, .topPage,
@ -240,15 +170,38 @@ onUnmounted(() => {
flex: 1; flex: 1;
margin-top: 23px; margin-top: 23px;
.echart { .chart_container {
display: flex;
flex-direction: column;
gap: 5px;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.echart {
height: 48%;
width: 100%;
}
} }
} }
.content { .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 { .content_item {
font-size: 14px; font-size: 14px;
@ -262,12 +215,6 @@ onUnmounted(() => {
} }
} }
.ellipse {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.subfont { .subfont {
color: rgba(138, 149, 165, 1); color: rgba(138, 149, 165, 1);
} }

View File

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

View File

@ -1,45 +1,108 @@
<template> <template>
<div class="large-screen"> <div class="large_screen">
<Header /> <Header :projectId="projectId" :isFull="isFull" @changePage="handleChangePage" />
<div class="nav"> <div class="nav">
<leftPage /> <div class="nav_left" :style="{ left: isHideOther ? '-25vw' : '0' }">
<centerPage /> <leftPage :projectId="projectId" />
<rightPage /> </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>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue';
import Header from './components/header.vue'; import Header from './components/header.vue';
import leftPage from './components/leftPage.vue'; import leftPage from './components/leftPage.vue';
import centerPage from './components/centerPage.vue'; import centerPage from './components/centerPage.vue';
import rightPage from './components/rightPage.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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.large-screen { .large_screen {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background: url('@/assets/large/bg.png') no-repeat; background: url('@/assets/large/bg.png') no-repeat;
background-size: 100% 100%; background-size: 100% 100%;
background-attachment: fixed;
background-color: rgba(4, 7, 17, 1); background-color: rgba(4, 7, 17, 1);
overflow: hidden;
} }
.nav { .nav {
display: flex; position: relative;
gap: 15rpx; display: grid;
width: 100%; place-items: center;
height: calc(100vh - 100px); width: calc(100vw - 30px);
height: calc(100vh - 90px);
margin: 0 auto;
box-sizing: border-box; box-sizing: border-box;
color: #fff; color: #fff;
} }
.nav_left, .nav_left,
.nav_right { .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 { .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> </style>

View File

@ -5,7 +5,7 @@
<el-form :model="queryForm" :inline="true"> <el-form :model="queryForm" :inline="true">
<el-form-item label="版本号" prop="versions"> <el-form-item label="版本号" prop="versions">
<el-select v-model="queryForm.versions" placeholder="选择版本号" @change="changeVersions"> <el-select v-model="queryForm.versions" placeholder="选择版本号" @change="changeVersions">
<el-option v-for="item in options" :key="item.id" :label="item.versions" :value="item.id" /> <el-option v-for="item in options" :key="item.id" :label="item.versions" :value="item.versions" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="表名" prop="sheet"> <el-form-item label="表名" prop="sheet">
@ -59,8 +59,9 @@
<el-table ref="tableRef" v-loading="loading" :data="tableData" row-key="id" border lazy default-expand-all> <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="num" label="编号" />
<el-table-column prop="name" label="工程或费用名称" /> <el-table-column prop="name" label="工程或费用名称" />
<el-table-column prop="unit" label="单位" /> <el-table-column prop="unit" label="单位" align="center" />
<el-table-column prop="quantity" label="数量" /> <el-table-column prop="quantity" label="数量" align="center" />
<el-table-column prop="specification" label="规格" align="center" />
<el-table-column prop="remark" label="单价" align="center"> <el-table-column prop="remark" label="单价" align="center">
<template #default="scope"> <template #default="scope">
<span>{{ scope.row.unitPrice }}</span> <span>{{ scope.row.unitPrice }}</span>
@ -166,7 +167,8 @@ const getTableData = async () => {
const params = { const params = {
projectId: currentProject.value?.id, projectId: currentProject.value?.id,
sheet: queryForm.value.sheet, sheet: queryForm.value.sheet,
versions: queryForm.value.versions versions: queryForm.value.versions,
type: '1'
}; };
const res = await getTreeLimit(params); const res = await getTreeLimit(params);
loading.value = false; loading.value = false;
@ -209,8 +211,6 @@ const tableRef = ref<any>();
const toggleExpandAll = () => { const toggleExpandAll = () => {
isExpandAll.value = !isExpandAll.value; isExpandAll.value = !isExpandAll.value;
console.log(isExpandAll.value);
tableData.value.forEach((row) => { tableData.value.forEach((row) => {
tableRef.value.toggleRowExpansion(row, isExpandAll.value); tableRef.value.toggleRowExpansion(row, isExpandAll.value);
}); });
@ -248,7 +248,7 @@ const handleExport = () => {
projectId: currentProject.value?.id, projectId: currentProject.value?.id,
sheet: queryForm.value.sheet sheet: queryForm.value.sheet
}, },
`限价一览表${queryForm.value.sheet}.xlsx` `投标成本核算清单${queryForm.value.sheet}.xlsx`
); );
}; };
// 审核 // 审核

View File

@ -210,7 +210,8 @@ const getListTable = async () => {
const res = await getTreeLimit({ const res = await getTreeLimit({
projectId: currentProject.value?.id, projectId: currentProject.value?.id,
versions: form.value.versions, versions: form.value.versions,
sheet: form.value.sheet sheet: form.value.sheet,
type: '0'
}); });
if (res.code == 200) { if (res.code == 200) {
tableData.value = res.data; tableData.value = res.data;

View File

@ -5,7 +5,7 @@
<el-form :model="queryForm" :inline="true"> <el-form :model="queryForm" :inline="true">
<el-form-item label="版本号" prop="versions"> <el-form-item label="版本号" prop="versions">
<el-select v-model="queryForm.versions" placeholder="选择版本号" @change="changeVersions"> <el-select v-model="queryForm.versions" placeholder="选择版本号" @change="changeVersions">
<el-option v-for="item in options" :key="item.id" :label="item.versions" :value="item.id" /> <el-option v-for="item in options" :key="item.id" :label="item.versions" :value="item.versions" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="表名" prop="sheet"> <el-form-item label="表名" prop="sheet">
@ -59,8 +59,9 @@
<el-table ref="tableRef" v-loading="loading" :data="tableData" row-key="id" border lazy default-expand-all> <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="num" label="编号" />
<el-table-column prop="name" label="工程或费用名称" /> <el-table-column prop="name" label="工程或费用名称" />
<el-table-column prop="unit" label="单位" /> <el-table-column prop="unit" label="单位" align="center" />
<el-table-column prop="quantity" label="数量"> <el-table-column prop="specification" label="规格" align="center"/>
<el-table-column prop="quantity" label="数量" align="center">
<template #default="scope"> <template #default="scope">
{{ scope.row.children.length > 0 ? '' : scope.row.quantity }} {{ scope.row.children.length > 0 ? '' : scope.row.quantity }}
</template> </template>
@ -184,7 +185,8 @@ const getTableData = async () => {
const params = { const params = {
projectId: currentProject.value?.id, projectId: currentProject.value?.id,
sheet: queryForm.value.sheet, sheet: queryForm.value.sheet,
versions: queryForm.value.versions versions: queryForm.value.versions,
type: '0'
}; };
const res = await getTreeLimit(params); const res = await getTreeLimit(params);
loading.value = false; loading.value = false;
@ -288,7 +290,7 @@ const handleExport = () => {
projectId: currentProject.value?.id, projectId: currentProject.value?.id,
sheet: queryForm.value.sheet sheet: queryForm.value.sheet
}, },
`限价一览表${queryForm.value.sheet}.xlsx` `投标成本核算${queryForm.value.sheet}.xlsx`
); );
}; };
// 审核 // 审核

View File

@ -138,9 +138,14 @@
<el-table-column prop="useQuantity" label="剩余量" align="center"> <el-table-column prop="useQuantity" label="剩余量" align="center">
<template #default="scope"> <template #default="scope">
{{ {{
(scope.row.quantity ? Number(scope.row.quantity) : 0) - (scope.row.useQuantity ? Number(scope.row.useQuantity) : 0) == 0 (scope.row.quantity ? Number(scope.row.quantity) : 0) -
(scope.row.useQuantity ? Number(scope.row.useQuantity) : 0) -
(scope.row.selectNum ? Number(scope.row.selectNum) : 0) ==
0
? '' ? ''
: (scope.row.quantity ? Number(scope.row.quantity) : 0) - (scope.row.useQuantity ? Number(scope.row.useQuantity) : 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)
}} }}
</template> </template>
</el-table-column> </el-table-column>
@ -149,12 +154,16 @@
<el-table-column prop="price" label="总价" align="center"> <el-table-column prop="price" label="总价" align="center">
<template #default="scope"> <template #default="scope">
{{ {{
((scope.row.quantity ? Number(scope.row.quantity) : 0) - (scope.row.useQuantity ? Number(scope.row.useQuantity) : 0)) * ((scope.row.quantity ? Number(scope.row.quantity) : 0) -
(scope.row.useQuantity ? Number(scope.row.useQuantity) : 0) -
(scope.row.selectNum ? Number(scope.row.selectNum) : 0)) *
Number(scope.row.unitPrice) == Number(scope.row.unitPrice) ==
0 0
? '' ? ''
: ( : (
((scope.row.quantity ? Number(scope.row.quantity) : 0) - (scope.row.useQuantity ? Number(scope.row.useQuantity) : 0)) * ((scope.row.quantity ? Number(scope.row.quantity) : 0) -
(scope.row.useQuantity ? Number(scope.row.useQuantity) : 0) -
(scope.row.selectNum ? Number(scope.row.selectNum) : 0)) *
Number(scope.row.unitPrice) Number(scope.row.unitPrice)
).toFixed(2) ).toFixed(2)
}} }}
@ -328,18 +337,10 @@ const getVersionNums = async () => {
getSheetName(); getSheetName();
} else { } else {
treeForm.value.versions = ''; treeForm.value.versions = '';
ElMessage({
message: '获取版本号失败',
type: 'warning'
});
} }
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
ElMessage({
message: '获取版本号失败',
type: 'warning'
});
} }
}; };
//获取表名 //获取表名
@ -356,19 +357,11 @@ const getSheetName = async () => {
treeForm.value.sheet = res.data[0]; treeForm.value.sheet = res.data[0];
} else { } else {
treeForm.value.sheet = ''; treeForm.value.sheet = '';
ElMessage({
message: '获取表名失败',
type: 'warning'
});
} }
getTreeList(); getTreeList();
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
ElMessage({
message: '获取表名失败',
type: 'warning'
});
} }
}; };
const handleSelection = (selection: any) => { const handleSelection = (selection: any) => {

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 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="num" label="编号" />
<el-table-column prop="name" label="工程或费用名称" /> <el-table-column prop="name" label="工程或费用名称" />
<el-table-column prop="unit" label="单位" /> <el-table-column prop="unit" label="单位" align="center" />
<el-table-column prop="quantity" label="数量"> <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"> <template #default="scope">
{{ scope.row.children.length > 0 ? '' : scope.row.quantity }} {{ scope.row.children.length > 0 ? '' : scope.row.quantity }}
</template> </template>
@ -124,7 +126,7 @@ const loading = ref(false);
const options = ref<any[]>([]); const options = ref<any[]>([]);
const sheets = ref<any[]>([]); const sheets = ref<any[]>([]);
const tableData = ref<any[]>([]); const tableData = ref<any[]>([]);
const isExpandAll = ref(false); const isExpandAll = ref(true);
const reviewStatus = ref(''); const reviewStatus = ref('');
const versionObj: any = ref({}); const versionObj: any = ref({});
const versionMap = new Map(); const versionMap = new Map();
@ -150,18 +152,10 @@ const getVersionNums = async () => {
getSheetName(); getSheetName();
} else { } else {
queryForm.value.versions = ''; queryForm.value.versions = '';
ElMessage({
message: '获取版本号失败',
type: 'warning'
});
} }
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
ElMessage({
message: '获取版本号失败',
type: 'warning'
});
} }
}; };
//选择版本号 //选择版本号
@ -193,19 +187,11 @@ const getSheetName = async () => {
queryForm.value.sheet = res.data[0]; queryForm.value.sheet = res.data[0];
} else { } else {
queryForm.value.sheet = ''; queryForm.value.sheet = '';
ElMessage({
message: '获取表名失败',
type: 'warning'
});
} }
getTableData(); getTableData();
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
ElMessage({
message: '获取表名失败',
type: 'warning'
});
} }
}; };
//获取表格 //获取表格
@ -323,7 +309,7 @@ const handleExport = () => {
versions: queryForm.value.versions, versions: queryForm.value.versions,
type: '1' type: '1'
}, },
`限价一览${queryForm.value.sheet}.xlsx` `限价一览${queryForm.value.sheet}.xlsx`
); );
}; };
// 审批 // 审批

View File

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

View File

@ -65,7 +65,7 @@
> >
<el-row :gutter="8" class="items-top"> <el-row :gutter="8" class="items-top">
<!-- 1. 专业选择核心统一所有角色的专业来源 --> <!-- 1. 专业选择核心统一所有角色的专业来源 -->
<el-col :span="3" class="mb-4 sm:mb-0 pl-4" style="margin-top:8px;"> <el-col :span="3" class="mb-4 sm:mb-0 pl-4" style="margin-top: 8px">
<el-form-item <el-form-item
:prop="`designers.${configIndex}.userMajor`" :prop="`designers.${configIndex}.userMajor`"
:rules="[ :rules="[
@ -76,7 +76,8 @@
label-width="60px" label-width="60px"
label="专业" label="专业"
> >
<el-select filterable <el-select
filterable
v-model="form.designers[configIndex].userMajor" v-model="form.designers[configIndex].userMajor"
placeholder="请选择专业" placeholder="请选择专业"
class="w-full transition-all duration-300 border-gray-300" class="w-full transition-all duration-300 border-gray-300"
@ -109,7 +110,8 @@
label="设计" label="设计"
label-width="50px" label-width="50px"
> >
<el-select filterable <el-select
filterable
v-model="person.userId" v-model="person.userId"
placeholder="选择人员" placeholder="选择人员"
class="w-full transition-all duration-300 border-gray-300" class="w-full transition-all duration-300 border-gray-300"
@ -165,7 +167,8 @@
label="校审" label="校审"
label-width="50px" label-width="50px"
> >
<el-select filterable <el-select
filterable
v-model="person.userId" v-model="person.userId"
placeholder="选择人员" placeholder="选择人员"
class="w-full transition-all duration-300 border-gray-300" class="w-full transition-all duration-300 border-gray-300"
@ -221,7 +224,8 @@
label="审定" label="审定"
label-width="50px" label-width="50px"
> >
<el-select filterable <el-select
filterable
v-model="person.userId" v-model="person.userId"
placeholder="选择人员" placeholder="选择人员"
class="w-full transition-all duration-300 border-gray-300" class="w-full transition-all duration-300 border-gray-300"
@ -277,7 +281,8 @@
label="审核" label="审核"
label-width="50px" label-width="50px"
> >
<el-select filterable <el-select
filterable
v-model="person.userId" v-model="person.userId"
placeholder="选择人员" placeholder="选择人员"
class="w-full transition-all duration-300 border-gray-300" class="w-full transition-all duration-300 border-gray-300"
@ -318,7 +323,7 @@
</el-col> </el-col>
<!-- 操作列 --> <!-- 操作列 -->
<el-col :span="2" class="text-right pr-4"> <el-col :span="2" class="pr-4 mt-2 text-right">
<el-button <el-button
type="text" type="text"
class="text-red-500 hover:text-red-700 transition-colors" class="text-red-500 hover:text-red-700 transition-colors"

View File

@ -6,7 +6,7 @@
<el-card v-if="index < 3" shadow="always"> <el-card v-if="index < 3" shadow="always">
<el-form :model="state.queryForm" :inline="true"> <el-form :model="state.queryForm" :inline="true">
<el-form-item label="版本号" prop="versions"> <el-form-item label="版本号" prop="versions">
<el-select v-model="state.queryForm.versions" placeholder="选择版本号"> <el-select v-model="state.queryForm.versions" placeholder="选择版本号" @change="handleChangeVersion">
<el-option v-for="item in state.options" :key="item.versions" :label="item.versions" :value="item.versions" /> <el-option v-for="item in state.options" :key="item.versions" :label="item.versions" :value="item.versions" />
</el-select> </el-select>
</el-form-item> </el-form-item>
@ -65,12 +65,7 @@
<el-button type="primary" con="edit" @click="clickApprovalSheet()">审核</el-button> <el-button type="primary" con="edit" @click="clickApprovalSheet()">审核</el-button>
</el-form-item> </el-form-item>
<el-form-item v-if="state.versionsData.status == 'waiting' || state.versionsData.status == 'finish'"> <el-form-item v-if="state.versionsData.status == 'waiting' || state.versionsData.status == 'finish'">
<el-button <el-button icon="view" @click="lookApprovalFlow()" type="warning">查看流程</el-button>
icon="view"
@click="lookApprovalFlow()"
type="warning"
>查看流程</el-button
>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
@ -171,6 +166,7 @@ const handleTabChange = (tab) => {
onMounted(async () => { onMounted(async () => {
await getVersionNums(); await getVersionNums();
}); });
// 获取版本号 // 获取版本号
async function getVersionNums(isSheet = true) { async function getVersionNums(isSheet = true) {
try { try {
@ -247,10 +243,10 @@ async function handleSheetName() {
// 获取列表 // 获取列表
async function handleQueryList(isSheet = true) { async function handleQueryList(isSheet = true) {
if (isSheet && !state.queryForm.sheet) { // if (isSheet && !state.queryForm) {
console.warn('表名不存在,无法获取列表数据'); // console.warn('表名不存在,无法获取列表数据');
return; // return;
} // }
try { try {
state.loading.list = true; state.loading.list = true;
@ -308,7 +304,7 @@ function handleChange(sheet) {
function handleChangeVersion(versions) { function handleChangeVersion(versions) {
state.queryForm.versions = versions; state.queryForm.versions = versions;
state.versionsData = state.options.find((e) => e.versions == versions); state.versionsData = state.options.find((e) => e.versions == versions);
console.log('state.versionsData', state.versionsData); // console.log('state.versionsData', state.versionsData);
state.sheets = []; state.sheets = [];
handleQueryList(); handleQueryList();
} }

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"> <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"> <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>
<div class="p-6"> <div class="p-6">
<el-form <el-form

View File

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

View File

@ -50,7 +50,7 @@
<div class="p-4"> <div class="p-4">
<p class="text-gray-600 mb-4">请选择要启动的流程</p> <p class="text-gray-600 mb-4">请选择要启动的流程</p>
<el-select v-model="flowCode" placeholder="请选择流程" style="width: 100%"> <el-select v-model="flowCode" placeholder="请选择流程" style="width: 100%">
<el-option v-for="item in flowCodeOptions" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in [flowCodeOptions[optionIndex]]" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</div> </div>
<template #footer> <template #footer>
@ -98,6 +98,7 @@ const flowCodeOptions = ref([
label: '资金设计变更审批' label: '资金设计变更审批'
} }
]); ]);
const optionIndex = ref<number>(0);
const flowCode = ref<string>(''); const flowCode = ref<string>('');
const status = ref<string>(''); const status = ref<string>('');
@ -272,12 +273,12 @@ const submit = async (status, data) => {
} else { } else {
if ((form.value.status === 'draft' && (flowCode.value === '' || flowCode.value === null)) || routeParams.value.type === 'add') { if ((form.value.status === 'draft' && (flowCode.value === '' || flowCode.value === null)) || routeParams.value.type === 'add') {
if (form.value.costEstimation == '0') { if (form.value.costEstimation == '0') {
flowCodeOptions.value = [flowCodeOptions.value[0]]; optionIndex.value = 0;
} else { } else {
console.log('🚀 ~ submit ~ flowCodeOptions.value:', flowCodeOptions.value[1]); console.log('🚀 ~ submit ~ flowCodeOptions.value:', flowCodeOptions.value[1]);
flowCodeOptions.value = [flowCodeOptions.value[1]]; optionIndex.value = 1;
} }
flowCode.value = flowCodeOptions.value[0].value; flowCode.value = flowCodeOptions.value[optionIndex.value].value;
dialogVisible.visible = true; dialogVisible.visible = true;
return; return;
} }

View File

@ -49,7 +49,32 @@
<!-- 资料文件区域 --> <!-- 资料文件区域 -->
<div class="mb-8"> <div class="mb-8">
<div class="flex items-center justify-between mb-5"> <div class="flex items-center justify-between mb-5">
<h3 class="text-lg font-semibold text-blue-700">资料文件清单</h3> <div style="display: flex; align-items: center">
<h3 class="text-lg font-semibold text-blue-700" style="margin-right: 20px">资料文件清单</h3>
<el-upload
class="upload-excel"
action="#"
v-if="!form.id || form.status == 'draft'"
ref="uploadRef"
:auto-upload="false"
:on-change="importTemplate"
:show-file-list="false"
:accept="'.xlsx,.xls'"
:limit="1"
>
<el-button type="primary" icon="Upload">导入文件</el-button>
</el-upload>
<el-button
v-if="!form.id || form.status == 'draft'"
type="primary"
style="margin-left: 20px"
icon="Download"
@click="exportTemplate"
class="transition-all hover:bg-blue-600"
>
导出模版
</el-button>
</div>
<el-button type="primary" size="small" @click="addDocumentItem" v-if="!disabledAll" icon="Plus" class="transition-all hover:bg-blue-600"> <el-button type="primary" size="small" @click="addDocumentItem" v-if="!disabledAll" icon="Plus" class="transition-all hover:bg-blue-600">
添加资料 添加资料
</el-button> </el-button>
@ -180,11 +205,11 @@ import { ref, reactive, computed, onMounted, onUnmounted, watch, getCurrentInsta
import { useUserStoreHook } from '@/store/modules/user'; import { useUserStoreHook } from '@/store/modules/user';
import { ElMessage, ElLoading, FormRules } from 'element-plus'; import { ElMessage, ElLoading, FormRules } from 'element-plus';
import { systemUserList } from '@/api/design/appointment'; import { systemUserList } from '@/api/design/appointment';
import { collectBatch, byProjectId, exportWord } from '@/api/design/received'; import { collectBatch, byProjectId, exportWord, exportExcel } from '@/api/design/received';
import { getUser } from '@/api/system/user'; import { getUser } from '@/api/system/user';
import type { ComponentInternalInstance, ElFormInstance } from 'element-plus'; import type { ComponentInternalInstance, ElFormInstance } from 'element-plus';
import { getInfo } from '@/api/login'; import { getInfo } from '@/api/login';
import * as XLSX from 'xlsx';
// 全局实例与状态管理 // 全局实例与状态管理
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const userStore = useUserStoreHook(); const userStore = useUserStoreHook();
@ -200,7 +225,7 @@ const documentsFormRef = ref<ElFormInstance>();
const userList = ref<any[]>([]); const userList = ref<any[]>([]);
const userMap = new Map<string, string>(); // 存储用户ID与昵称映射 const userMap = new Map<string, string>(); // 存储用户ID与昵称映射
const disabledAll = ref(false); // 表单是否全部禁用 const disabledAll = ref(false); // 表单是否全部禁用
const uploadRef = ref<any>();
// 表单核心数据 // 表单核心数据
const form = reactive({ const form = reactive({
projectId: currentProject.value?.id, projectId: currentProject.value?.id,
@ -445,7 +470,66 @@ const onLoad = async () => {
console.error('文件导出错误:', error); console.error('文件导出错误:', error);
} }
}; };
const exportTemplate = async () => {
// 导出模版
proxy?.download(
'design/collect/exportExcel',
{
deptId: userStore.deptId
},
`收资清单表格.xlsx`
);
};
const importTemplate = async (files, fileList) => {
// 导入表格数据
const file = fileList[0].raw; // 获取原始文件对象
const reader = new FileReader();
let obj = {
id: '编码',
name: '人员',
fliename: '目录名',
remark: '备注'
};
reader.onload = (e) => {
try {
// 读取文件内容
const data = new Uint8Array(e.target.result);
// 解析Excel
const workbook = XLSX.read(data, { type: 'array' });
// 获取第一个工作表名称
const firstSheetName = workbook.SheetNames[0];
// 获取第一个工作表内容
const worksheet = workbook.Sheets[firstSheetName];
// 转换为JSON格式
const jsonData = XLSX.utils.sheet_to_json(worksheet);
if (jsonData.length === 0) {
ElMessage.info('Excel文件中没有数据');
return;
}
let arr = [];
// 判断form.documents 是否对象
jsonData.forEach((item, index) => {
if (item[obj.id]) {
arr.push({
id: Date.now() + index,
catalogueName: item[obj.fliename],
remark: item[obj.remark],
userId: item[obj.id]
});
}
});
form.documents = arr;
uploadRef.value.clearFiles();
console.log(arr);
} catch (err) {}
};
// 以ArrayBuffer方式读取文件
reader.readAsArrayBuffer(file);
};
/** 页面挂载初始化 */ /** 页面挂载初始化 */
onMounted(() => { onMounted(() => {
// 先获取当前用户信息,再获取部门用户列表,最后回显表单数据 // 先获取当前用户信息,再获取部门用户列表,最后回显表单数据

View File

@ -143,6 +143,11 @@
<file-upload :fileType="['pdf']" :isShowTip="false" :fileSize="100" <file-upload :fileType="['pdf']" :isShowTip="false" :fileSize="100"
v-model="uploadForm.fileIds"></file-upload> v-model="uploadForm.fileIds"></file-upload>
</el-form-item> </el-form-item>
<el-form-item v-if="uploadForm.type == '3'" label="抄送人">
<el-select multiple filterable clearable v-model="form.userIds" placeholder="请选择抄送人">
<el-option :value="item.userId" v-for="item in userCoryList" :key="item.userId" :label="item.nickName + '-' + item.phonenumber" />
</el-select>
</el-form-item>
<el-form-item v-if="uploadForm.type == '1'" label="过程图纸" prop="cancellationIds"> <el-form-item v-if="uploadForm.type == '1'" label="过程图纸" prop="cancellationIds">
<file-upload :fileType="['pdf']" :isShowTip="false" :fileSize="100" <file-upload :fileType="['pdf']" :isShowTip="false" :fileSize="100"
v-model="uploadForm.cancellationIds"></file-upload> v-model="uploadForm.cancellationIds"></file-upload>
@ -228,7 +233,8 @@ import {
uploadVolumeFile, uploadVolumeFile,
majorList, majorList,
getVolumeCatafileList, getVolumeCatafileList,
volumeFileList volumeFileList,
copyUserList
} from '@/api/design/volumeCatalog'; } from '@/api/design/volumeCatalog';
import { VolumeCatalogVO } from '@/api/design/volumeCatalog/types'; import { VolumeCatalogVO } from '@/api/design/volumeCatalog/types';
import { useUserStoreHook } from '@/store/modules/user'; import { useUserStoreHook } from '@/store/modules/user';
@ -254,6 +260,7 @@ const uploadOpinionVisible = ref(false);
const design = ref(''); const design = ref('');
const total = ref(0); const total = ref(0);
const dialogHistory = ref(false); const dialogHistory = ref(false);
const userCoryList = ref([]);
const opinion = ref(''); const opinion = ref('');
const updateRow = ref({ const updateRow = ref({
opinion: [] opinion: []
@ -389,7 +396,13 @@ const cancel = () => {
reset(); reset();
dialog.visible = false; dialog.visible = false;
}; };
// 获取人员列表
const getDesignUserList = async () => {
const res = await copyUserList({ projectId: currentProject.value?.id, userType: 2 });
if (res.code === 200) {
userCoryList.value = res.data;
}
};
/** 表单重置 */ /** 表单重置 */
const reset = () => { const reset = () => {
form.value = { ...initFormData }; form.value = { ...initFormData };
@ -519,7 +532,7 @@ const onSubmit = async () => {
type: uploadForm.type type: uploadForm.type
}; };
try { try {
await uploadVolumeFile(obj); await uploadVolumeFile({ ...obj, userIds: form.value.userIds });
proxy?.$modal.msgSuccess('文件上传成功'); proxy?.$modal.msgSuccess('文件上传成功');
uploadVisible.value = false; uploadVisible.value = false;
await getList(); await getList();
@ -629,6 +642,7 @@ const handleAuditInfo = (row) => {
// 审核图纸 // 审核图纸
}; };
onMounted(() => { onMounted(() => {
getDesignUserList();
getSpecialtyList(); getSpecialtyList();
getList(); getList();
}); });
@ -639,6 +653,7 @@ const listeningProject = watch(
(nid, oid) => { (nid, oid) => {
queryParams.value.projectId = nid; queryParams.value.projectId = nid;
form.value.projectId = nid; form.value.projectId = nid;
getDesignUserList();
getSpecialtyList(); getSpecialtyList();
getList(); getList();
} }

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

@ -63,7 +63,7 @@
</el-tooltip> </el-tooltip>
</span> </span>
</el-col> </el-col>
<el-col :span="1.5"> <!-- <el-col :span="1.5">
<el-button <el-button
type="success" type="success"
plain plain
@ -73,7 +73,7 @@
v-hasPermi="['formalities:formalitiesAreConsolidated:edit']" v-hasPermi="['formalities:formalitiesAreConsolidated:edit']"
>修改</el-button >修改</el-button
> >
</el-col> </el-col> -->
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row> </el-row>
</template> </template>
@ -81,7 +81,7 @@
<el-table v-loading="loading" :data="formalitiesAreConsolidatedList" @selection-change="handleSelectionChange" row-key="id" default-expand-all> <el-table v-loading="loading" :data="formalitiesAreConsolidatedList" @selection-change="handleSelectionChange" row-key="id" default-expand-all>
<el-table-column type="selection" width="55" align="center" /> <el-table-column type="selection" width="55" align="center" />
<!-- <el-table-column label="手续办理清单模板父级" align="center" prop="formalitiesPname" /> --> <!-- <el-table-column label="手续办理清单模板父级" align="center" prop="formalitiesPname" /> -->
<el-table-column label="手续办理清单" align="center" prop="formalitiesName" /> <el-table-column label="手续办理清单" align="left" prop="formalitiesName" />
<el-table-column label="计划开始时间" align="center" prop="planTheStartTime" width="180"> <el-table-column label="计划开始时间" align="center" prop="planTheStartTime" width="180">
<template #default="scope"> <template #default="scope">
<span>{{ parseTime(scope.row.planTheStartTime, '{y}-{m}-{d}') }}</span> <span>{{ parseTime(scope.row.planTheStartTime, '{y}-{m}-{d}') }}</span>

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

@ -1,13 +1,11 @@
<template> <template>
<div class="p-2"> <div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
:leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]"> <div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover"> <el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto"> <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto">
<el-form-item label="材料名称" prop="materialsName"> <el-form-item label="材料名称" prop="materialsName">
<el-input v-model="queryParams.materialsName" placeholder="请输入材料名称" clearable <el-input v-model="queryParams.materialsName" placeholder="请输入材料名称" clearable @keyup.enter="handleQuery" />
@keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="材料提供商" prop="companyId"> <el-form-item label="材料提供商" prop="companyId">
<el-select v-model="queryParams.companyId" clearable placeholder="全部"> <el-select v-model="queryParams.companyId" clearable placeholder="全部">
@ -27,22 +25,7 @@
<template #header> <template #header>
<el-row :gutter="10" class="mb8"> <el-row :gutter="10" class="mb8">
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['materials:materials:add']"> 新增 <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['materials:materials:add']"> 新增 </el-button>
</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> </el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row> </el-row>
@ -72,64 +55,81 @@
<el-table-column fixed="right" label="操作" align="center" class-name="small-padding fixed-width" width="320"> <el-table-column fixed="right" label="操作" align="center" class-name="small-padding fixed-width" width="320">
<template #default="scope"> <template #default="scope">
<el-space> <el-space>
<el-button link type="primary" icon="View" @click="handleShowDrawer(scope.row)" <el-button link type="primary" icon="View" @click="handleShowDrawer(scope.row)" v-hasPermi="['materials:materials:query']">
v-hasPermi="['materials:materials:query']">
详情 详情
</el-button> </el-button>
<el-button link type="success" icon="Edit" @click="handleUpdate(scope.row)" <el-button link type="success" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['materials:materials:edit']"> 修改 </el-button>
v-hasPermi="['materials:materials:edit']"> 修改 </el-button> <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['materials:materials:remove']">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)"
v-hasPermi="['materials:materials:remove']">
删除 删除
</el-button> </el-button>
<el-button v-hasPermi="['materials:materialsInventory:edit']" link type="primary" icon="Plus" <el-button
@click="handleAddMaterialsInventory(scope.row)"> 出入库 </el-button> v-hasPermi="['materials:materialsInventory:edit']"
link
type="primary"
icon="Plus"
@click="handleAddMaterialsInventory(scope.row)"
>
出入库
</el-button>
</el-space> </el-space>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card> </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 ref="materialsFormRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="材料名称" prop="materialsName"> <el-row>
<el-input v-model="form.materialsName" placeholder="请输入材料名称" /> <el-col :span="12">
</el-form-item> <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-form-item label="规格型号名称" prop="typeSpecificationName">
<el-input v-model="form.typeSpecificationName" placeholder="请输入规格型号名称" /> <el-input v-model="form.typeSpecificationName" placeholder="请输入规格型号名称" /> </el-form-item
</el-form-item> ></el-col>
<el-form-item label="材料供应商" prop="companyId"> <el-col :span="12"
><el-form-item label="材料提供商" prop="companyId">
<el-select v-model="form.companyId" clearable placeholder="请选择材料提供商"> <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-option v-for="item in companyOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select> </el-form-item
</el-form-item> ></el-col>
<el-form-item label="使用部位" prop="usePart"> <el-col :span="12">
<el-input v-model="form.usePart" placeholder="请输入使用部位" /> <el-form-item label="使用部位" prop="usePart"> <el-input v-model="form.usePart" placeholder="请输入使用部位" /> </el-form-item
</el-form-item> ></el-col>
<el-form-item label="计量单位" prop="weightId"> <el-col :span="12"
<el-input v-model="form.weightId" placeholder="请输入计量单位" /> ><el-form-item label="计量单位" prop="weightId"> <el-input v-model="form.weightId" placeholder="请输入计量单位" /> </el-form-item
</el-form-item> ></el-col>
<el-col :span="12">
<el-form-item label="预计材料数量" prop="quantityCount"> <el-form-item label="预计材料数量" prop="quantityCount">
<el-input v-model="form.quantityCount" placeholder="请输入预计材料数量" /> <el-input v-model="form.quantityCount" type="number" min="0" placeholder="请输入预计材料数量" /> </el-form-item
</el-form-item> ></el-col>
<el-form-item label="备注" prop="remark"> <el-col :span="24">
<el-input v-model="form.remark" placeholder="请输入备注" /> <el-form-item label="备注" prop="remark"> <el-input type="textarea" v-model="form.remark" placeholder="请输入备注" /> </el-form-item
</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"> <el-form-item label="材料文件" prop="fileOssIdMap">
<div :key="item.value" v-for="item in materials_file_type"> <div>
<h3>{{ item.label }}</h3> <h3>{{ item.label }}</h3>
<file-upload v-model="ossIdMap[item.value]" :limit="1" :file-size="50" :file-type="['pdf']" <file-upload
:isShowTip="false"
v-model="ossIdMap[item.value]"
:limit="1"
:file-size="50"
:file-type="['pdf']"
@update:model-value=" @update:model-value="
(args) => { (args) => {
handleOssUpdate(args, item.value); handleOssUpdate(args, item.value);
} }
" /> "
</div> />
</el-form-item> </div> </el-form-item
></el-col>
<el-col :span="24" style="color: rgb(237 70 61)">注意:请上传pdf格式文件</el-col>
</el-row>
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button> <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
@ -137,8 +137,7 @@
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
<materials-inventory-add-dialog :materials-id="currentMaterialsId" :project-id="currentProject.id" ref="dialogRef" <materials-inventory-add-dialog :materials-id="currentMaterialsId" :project-id="currentProject.id" ref="dialogRef" @submit="getList" />
@submit="getList" />
<el-dialog title="材料详情" v-model="showDetailDrawer" width="700px"> <el-dialog title="材料详情" v-model="showDetailDrawer" width="700px">
<materials-detail-drawer :materials-id="currentMaterialsId" /> <materials-detail-drawer :materials-id="currentMaterialsId" />
</el-dialog> </el-dialog>
@ -151,7 +150,7 @@ import { MaterialsForm, MaterialsQuery, MaterialsVO } from '@/api/materials/mate
import { useUserStoreHook } from '@/store/modules/user'; import { useUserStoreHook } from '@/store/modules/user';
import MaterialsInventoryTable from '@/views/materials/materials/component/MaterialsInventoryTable.vue'; import MaterialsInventoryTable from '@/views/materials/materials/component/MaterialsInventoryTable.vue';
import MaterialsInventoryAddDialog from '@/views/materials/materials/component/MaterialsInventoryAddDialog.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 { CompanyVO } from '@/api/materials/company/types';
import MaterialsDetailDrawer from '@/views/materials/materials/component/MaterialsDetailDrawer.vue'; import MaterialsDetailDrawer from '@/views/materials/materials/component/MaterialsDetailDrawer.vue';
@ -227,14 +226,14 @@ const getList = async () => {
/** 获取当前项目下的公司列表 */ /** 获取当前项目下的公司列表 */
const getCompanyList = async () => { const getCompanyList = async () => {
loading.value = true; loading.value = true;
const companyRes = await listCompany({ const companyRes = await supplierInputGet({
pageNum: 1,
pageSize: 1000,
projectId: currentProject.value?.id projectId: currentProject.value?.id
}); });
companyOptions.value = companyRes.rows.map((company: CompanyVO) => ({ console.log(companyRes);
companyOptions.value = companyRes.data.map((company) => ({
value: company.id, value: company.id,
label: company.companyName label: company.supplierName
})); }));
loading.value = false; loading.value = false;
}; };
@ -363,6 +362,7 @@ const listeningProject = watch(
(nid, oid) => { (nid, oid) => {
queryParams.value.projectId = nid; queryParams.value.projectId = nid;
form.value.projectId = nid; form.value.projectId = nid;
getCompanyList();
getList(); getList();
} }
); );

View File

@ -73,7 +73,10 @@
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="表单编号" prop="formCode"> <el-form-item label="表单编号" prop="formCode">
<el-input v-model="form.formCode" placeholder="请输入表单编号" /> <!-- <el-input v-model="form.formCode" placeholder="请输入表单编号" /> -->
<el-select v-model="form.formCode" placeholder="请选择表单编号" @change="(value) => formCodeChange(value)">
<el-option v-for="item in options" :key="item.formCode" :label="item.formCode" :value="item.formCode" />
</el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
@ -123,11 +126,11 @@
<el-col :span="12"> <el-col :span="12">
<el-form-item <el-form-item
label="名称" label="名称"
:prop="`itemList.${index}.name`" :prop="`itemList.${index}.materialsId`"
:rules="[{ required: true, message: '名称不能为空', trigger: 'blur' }]" :rules="[{ required: true, message: '名称不能为空', trigger: 'blur' }]"
> >
<el-select v-model="item.inventoryId" placeholder="请选择名称" @change="(value) => getNameChange(value, index, item)"> <el-select v-model="item.materialsId" placeholder="请选择名称" @change="(value) => getNameChange(value, index, item)">
<el-option v-for="opt in optionsName" :key="opt.id" :label="opt.materialsName" :value="opt.id" /> <el-option v-for="opt in optionsName" :key="opt.id" :label="`${opt.materialsName}_${opt.createTime}`" :value="opt.id" />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -168,13 +171,9 @@
</el-col> --> </el-col> -->
<el-col :span="12"> <el-col :span="12">
<el-form-item label="领取" :prop="`itemList.${index}.issuedQuantity`"> <el-form-item label="领取" :prop="`itemList.${index}.issuedQuantity`">
<el-input <el-select v-model="item.issuedQuantity" placeholder="请选择数量">
v-model.number="item.issuedQuantity" <el-option v-for="opt in item.outList" :key="opt.id" :label="opt.number" :value="opt.number" />
disabled </el-select>
placeholder="请输入领取数量"
@input="handleIssuedChange(index)"
@blur="handleIssuedChange(index)"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- <el-col :span="12"> <!-- <el-col :span="12">
@ -247,7 +246,8 @@ import {
addMaterialIssue, addMaterialIssue,
updateMaterialIssue, updateMaterialIssue,
inventoryList, inventoryList,
getMaterialName getMaterialName,
getMaterialInfo
} from '@/api/materials/materialIssue'; } from '@/api/materials/materialIssue';
import { MaterialIssueVO, MaterialIssueQuery, MaterialIssueForm } from '@/api/materials/materialIssue/types'; import { MaterialIssueVO, MaterialIssueQuery, MaterialIssueForm } from '@/api/materials/materialIssue/types';
@ -307,6 +307,7 @@ const getInitFormData = () => {
itemList: [ itemList: [
{ {
id: undefined, id: undefined,
specification: undefined, specification: undefined,
unit: undefined, unit: undefined,
stockQuantity: undefined, stockQuantity: undefined,
@ -314,7 +315,8 @@ const getInitFormData = () => {
remainingQuantity: undefined, remainingQuantity: undefined,
name: undefined, // 数量验收的名称 name: undefined, // 数量验收的名称
remark: undefined, remark: undefined,
materialsId: undefined materialsId: undefined,
outList: []
} }
] ]
}; };
@ -365,7 +367,7 @@ const computeMaterialName = () => {
.map((item) => item.name.trim()) .map((item) => item.name.trim())
.filter((name, index, self) => self.indexOf(name) === index); // 去重(如需保留重复则删除这行) .filter((name, index, self) => self.indexOf(name) === index); // 去重(如需保留重复则删除这行)
form.value.materialName = validNames.join(','); // form.value.materialName = validNames.join(',');
}; };
/** 查询物料领料单列表 */ /** 查询物料领料单列表 */
@ -404,6 +406,7 @@ const getNameChange = (value, index, item) => {
item.unit = selected.weightId; item.unit = selected.weightId;
item.issuedQuantity = selected.number; item.issuedQuantity = selected.number;
item.stockQuantity = Number(selected.inventoryNumber) || 0; item.stockQuantity = Number(selected.inventoryNumber) || 0;
item.outList = selected.outList || [];
// calculateRemaining(index); // 计算剩余数量 // calculateRemaining(index); // 计算剩余数量
} }
}; };
@ -529,8 +532,27 @@ const handleAdd = () => {
dialog.title = '添加物料领料单'; dialog.title = '添加物料领料单';
// 新增时初始计算材料名称 // 新增时初始计算材料名称
computeMaterialName(); computeMaterialName();
getFormData();
}; };
const options = ref([]);
//新增获取表单数据
const getFormData = async () => {
const res = await getMaterialInfo(currentProject.value.id);
if (res.code == 200) {
options.value = res.data;
}
};
const formCodeChange = (value) => {
console.log(value);
const selected = options.value.find((opt) => opt.formCode === value);
if (selected) {
form.value.materialName = selected.materialName;
form.value.orderingUnit = selected.orderingUnit;
form.value.supplierUnit = selected.supplierUnit;
optionsName.value = selected.materials;
}
};
/** 修改按钮操作 */ /** 修改按钮操作 */
const handleUpdate = async (row?: MaterialIssueVO) => { const handleUpdate = async (row?: MaterialIssueVO) => {
reset(); reset();
@ -597,6 +619,7 @@ const submitForm = () => {
remainingQuantity: Number(item.remainingQuantity) remainingQuantity: Number(item.remainingQuantity)
})) }))
}; };
console.log('提交数据:', submitData);
if (form.value.id) { if (form.value.id) {
await updateMaterialIssue(submitData); await updateMaterialIssue(submitData);
@ -638,10 +661,13 @@ const addItem = () => {
// 删除数量验收条目 // 删除数量验收条目
const removeItem = (index: number) => { const removeItem = (index: number) => {
if (form.value.itemList.length > 1) { if (form.value.itemList.length > 1) {
console.log(111111);
console.log(itemWatchStopFns.value[index]);
// 停止该条目的监听 // 停止该条目的监听
if (itemWatchStopFns.value[index]) { // if (itemWatchStopFns.value[index]) {
itemWatchStopFns.value[index](); // itemWatchStopFns.value[index]();
} // }
form.value.itemList.splice(index, 1); form.value.itemList.splice(index, 1);
itemWatchStopFns.value.splice(index, 1); itemWatchStopFns.value.splice(index, 1);
// 删除后重新计算材料名称 // 删除后重新计算材料名称
@ -682,7 +708,7 @@ watch(
onMounted(() => { onMounted(() => {
getList(); getList();
getName(); // getName();
}); });
// 监听项目id刷新数据 // 监听项目id刷新数据
@ -692,7 +718,7 @@ const listeningProject = watch(
queryParams.value.projectId = nid; queryParams.value.projectId = nid;
form.value.projectId = nid; form.value.projectId = nid;
getList(); getList();
getName(); // getName();
} }
); );

View File

@ -42,7 +42,7 @@
<thead> <thead>
<tr> <tr>
<th colspan="2">领料单位</th> <th colspan="2">领料单位</th>
<td class="th-bg" colspan="2">{{ formData.placeholder }}</td> <td class="th-bg" colspan="2">{{ formData.issueUnit }}</td>
<th colspan="2">保管单位</th> <th colspan="2">保管单位</th>
<td class="th-bg" colspan="2">{{ formData.storageUnit }}</td> <td class="th-bg" colspan="2">{{ formData.storageUnit }}</td>
</tr> </tr>

View File

@ -1,7 +1,6 @@
<template> <template>
<div class="p-2"> <div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
:leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]"> <div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover"> <el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true"> <el-form ref="queryFormRef" :model="queryParams" :inline="true">
@ -16,8 +15,7 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="材料名称" prop="materialName"> <el-form-item label="材料名称" prop="materialName">
<el-input v-model="queryParams.materialName" placeholder="请输入设备材料名称" clearable <el-input v-model="queryParams.materialName" placeholder="请输入设备材料名称" clearable @keyup.enter="handleQuery" />
@keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="订货单位" prop="orderingUnit"> <el-form-item label="订货单位" prop="orderingUnit">
<el-input v-model="queryParams.orderingUnit" placeholder="请输入订货单位" clearable @keyup.enter="handleQuery" /> <el-input v-model="queryParams.orderingUnit" placeholder="请输入订货单位" clearable @keyup.enter="handleQuery" />
@ -38,8 +36,7 @@
<template #header> <template #header>
<el-row :gutter="10" class="mb8"> <el-row :gutter="10" class="mb8">
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['materials:materialReceive:add']">新增</el-button>
v-hasPermi="['materials:materialReceive:add']">新增</el-button>
</el-col> </el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row> </el-row>
@ -50,8 +47,9 @@
<el-table-column label="表单编号" align="center" prop="formCode" /> <el-table-column label="表单编号" align="center" prop="formCode" />
<el-table-column label="材料来源" align="center" prop="projectName"> <el-table-column label="材料来源" align="center" prop="projectName">
<template #default="scope"> <template #default="scope">
<el-tag :type="scope.row.materialSource == '1' ? 'success' : 'warning'">{{ scope.row.materialSource == '1' ? <el-tag :type="scope.row.materialSource == '1' ? 'success' : 'warning'">{{
'甲供材料' : '乙供材料' }}</el-tag> scope.row.materialSource == '1' ? '甲供材料' : '乙供材料'
}}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="设备材料名称" align="center" prop="materialName" /> <el-table-column label="设备材料名称" align="center" prop="materialName" />
@ -77,20 +75,28 @@
<!-- <el-button link type="primary" icon="edit" @click="handleUpdate(scope.row)" v-hasPermi="['materials:materialReceive:edit']" <!-- <el-button link type="primary" icon="edit" @click="handleUpdate(scope.row)" v-hasPermi="['materials:materialReceive:edit']"
>修改</el-button >修改</el-button
> --> > -->
<el-button link type="primary" icon="View" @click="handleView(scope.row)" <el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['materials:materialReceive:query']"
v-hasPermi="['materials:materialReceive:query']">查看</el-button> >查看</el-button
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" >
v-hasPermi="['materials:materialReceive:remove']">删除</el-button> <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['materials:materialReceive:remove']"
>删除</el-button
>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card> </el-card>
<!-- 添加或修改物料接收单对话框 --> <!-- 添加或修改物料接收单对话框 -->
<el-dialog :close-on-click-modal="false" :close-on-press-escape="false" draggable :title="dialog.title" <el-dialog
v-model="dialog.visible" width="800px" append-to-body> :close-on-click-modal="false"
:close-on-press-escape="false"
draggable
:title="dialog.title"
v-model="dialog.visible"
width="800px"
append-to-body
>
<el-form ref="materialReceiveFormRef" :model="form" :rules="rules" label-width="110px"> <el-form ref="materialReceiveFormRef" :model="form" :rules="rules" label-width="110px">
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
@ -108,10 +114,8 @@
</el-col> </el-col>
<el-col v-if="form.materialSource == '2'" :span="12"> <el-col v-if="form.materialSource == '2'" :span="12">
<el-form-item label="采购单编号" prop="docId"> <el-form-item label="采购单编号" prop="docId">
<el-select @change="handleSelect" v-model="form.docId" filterable placeholder="请选择采购单" <el-select @change="handleSelect" v-model="form.docId" filterable placeholder="请选择采购单" style="width: 100%">
style="width: 100%"> <el-option v-for="item in purchaseDocList" :key="item.id" :label="item.docCode" :value="item.id"></el-option>
<el-option v-for="item in purchaseDocList" :key="item.id" :label="item.docCode"
:value="item.id"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -130,11 +134,20 @@
<el-input disabled v-model="form.projectName" placeholder="请输入工程名称" /> <el-input disabled v-model="form.projectName" placeholder="请输入工程名称" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12" :offset="0"
><el-form-item label="材料名称" prop="materialName">
<el-input v-model="form.materialName" placeholder="请输入设备材料名称" clearable /> </el-form-item
></el-col>
<el-col :span="12" v-if="form.materialSource == '2'"> <el-col :span="12" v-if="form.materialSource == '2'">
<el-form-item label="合同编号" prop="contractName"> <el-form-item label="合同编号" prop="contractName">
<el-select v-model="form.contractName" filterable placeholder="请选择合同" style="width: 100%"> <el-select v-model="form.contractName" filterable placeholder="请选择合同" style="width: 100%">
<el-option v-for="item in contractNameList" :key="item.contractCode" :label="item.contractCode" <el-option
:value="item.contractCode"></el-option> v-for="item in contractNameList"
:key="item.contractCode"
:label="item.contractCode"
:value="item.contractCode"
></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -149,48 +162,59 @@
<div class="detail"> <div class="detail">
<div class="detail-header"> <div class="detail-header">
<span>数量验收</span> <span>数量验收</span>
<el-button type="primary" v-if="form.materialSource == '1'" link @click="addItem" <el-button type="primary" v-if="form.materialSource == '1'" link @click="addItem" icon="Plus">添加数量验收</el-button>
icon="Plus">添加数量验收</el-button>
</div> </div>
<div v-for="(item, index) in form.itemList" :key="item.id" class="detail-item"> <div v-for="(item, index) in form.itemList" :key="item.id" class="detail-item">
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="名称" :prop="`itemList.${index}.name`" <el-form-item label="名称" :prop="`itemList.${index}.name`" :rules="{ required: true, message: '名称不能为空', trigger: 'blur' }">
:rules="{ required: true, message: '名称不能为空', trigger: 'blur' }">
<el-input :disabled="form.materialSource == '2'" v-model="item.name" placeholder="请输入名称" /> <el-input :disabled="form.materialSource == '2'" v-model="item.name" placeholder="请输入名称" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="规格" :prop="`itemList.${index}.specification`" <el-form-item
:rules="{ required: true, message: '规格不能为空', trigger: 'blur' }"> label="规格"
<el-input :disabled="form.materialSource == '2'" v-model="item.specification" :prop="`itemList.${index}.specification`"
placeholder="请输入规格" /> :rules="{ required: true, message: '规格不能为空', trigger: 'blur' }"
>
<el-input :disabled="form.materialSource == '2'" v-model="item.specification" placeholder="请输入规格" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="单位" :prop="`itemList.${index}.unit`" <el-form-item label="单位" :prop="`itemList.${index}.unit`" :rules="{ required: true, message: '单位不能为空', trigger: 'blur' }">
:rules="{ required: true, message: '单位不能为空', trigger: 'blur' }">
<el-input :disabled="form.materialSource == '2'" v-model="item.unit" placeholder="请输入单位" /> <el-input :disabled="form.materialSource == '2'" v-model="item.unit" placeholder="请输入单位" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="数量" :prop="`itemList.${index}.quantity`" :rules="rules.quantityRule" <el-form-item label="数量" :prop="`itemList.${index}.quantity`" :rules="rules.quantityRule" ref="quantityFormItemRefs[index]">
ref="quantityFormItemRefs[index]"> <el-input
<el-input :disabled="form.materialSource == '2'" type="number" v-model.number="item.quantity" :disabled="form.materialSource == '2'"
placeholder="请输入数量" min="0" @input="handleQuantityInput(index)" type="number"
@blur="handleQuantityBlur(index)" /> v-model.number="item.quantity"
placeholder="请输入数量"
min="0"
@input="handleQuantityInput(index)"
@blur="handleQuantityBlur(index)"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="验收" :prop="`itemList.${index}.acceptedQuantity`" <el-form-item label="验收" :prop="`itemList.${index}.acceptedQuantity`" :rules="rules.acceptedQuantityRule">
:rules="rules.acceptedQuantityRule"> <el-input
<el-input type="number" v-model.number="item.acceptedQuantity" placeholder="请输入验收" min="0" type="number"
@input="handleAcceptedInput(index)" /> v-model.number="item.acceptedQuantity"
placeholder="请输入验收"
min="0"
@input="handleAcceptedInput(index)"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="缺件" :prop="`itemList.${index}.shortageQuantity`" <el-form-item
:rules="{ required: true, message: '缺件数量不能为空', trigger: 'blur' }"> label="缺件"
:prop="`itemList.${index}.shortageQuantity`"
:rules="{ required: true, message: '缺件数量不能为空', trigger: 'blur' }"
>
<el-input type="number" min="0" v-model="item.shortageQuantity" placeholder="自动计算" readonly /> <el-input type="number" min="0" v-model="item.shortageQuantity" placeholder="自动计算" readonly />
<span class="tips">*自动计算数量-验收数量</span> <span class="tips">*自动计算数量-验收数量</span>
</el-form-item> </el-form-item>
@ -212,26 +236,22 @@
<el-col :span="12"> <el-col :span="12">
<el-form-item label="合格证文件" prop="certCountFileId"> <el-form-item label="合格证文件" prop="certCountFileId">
<file-upload :isShowTip="false" :fileType="['pdf', 'png', 'jpg', 'jpeg']" <file-upload :isShowTip="false" :fileType="['pdf', 'png', 'jpg', 'jpeg']" v-model="form.certCountFileId" />
v-model="form.certCountFileId" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="出厂报告文件" prop="reportCountFileId"> <el-form-item label="出厂报告文件" prop="reportCountFileId">
<file-upload :isShowTip="false" :fileType="['pdf', 'png', 'jpg', 'jpeg']" <file-upload :isShowTip="false" :fileType="['pdf', 'png', 'jpg', 'jpeg']" v-model="form.reportCountFileId" />
v-model="form.reportCountFileId" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="技术资料文件" prop="techDocCountFileId"> <el-form-item label="技术资料文件" prop="techDocCountFileId">
<file-upload :isShowTip="false" :fileType="['pdf', 'png', 'jpg', 'jpeg']" <file-upload :isShowTip="false" :fileType="['pdf', 'png', 'jpg', 'jpeg']" v-model="form.techDocCountFileId" />
v-model="form.techDocCountFileId" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="厂家资质文件" prop="licenseCountFileId"> <el-form-item label="厂家资质文件" prop="licenseCountFileId">
<file-upload :isShowTip="false" :fileType="['pdf', 'png', 'jpg', 'jpeg']" <file-upload :isShowTip="false" :fileType="['pdf', 'png', 'jpg', 'jpeg']" v-model="form.licenseCountFileId" />
v-model="form.licenseCountFileId" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24"> <el-col :span="24">

View File

@ -1,12 +1,20 @@
<template> <template>
<div class="p-2"> <div class="p-2">
<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>
<el-col :span="20">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave"> </transition> <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave"> </transition>
<el-card shadow="never"> <el-card shadow="never">
<template #header> <template #header>
<el-row :gutter="10" class="mb8"> <el-row :gutter="10" class="mb8">
<el-form ref="queryFormRef" :model="queryParams" :inline="true"> <el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="使用部位" prop="usePart"> <el-form-item label="物资名称" prop="materialsName">
<el-input v-model="queryParams.usePart" placeholder="请输入使用部位" clearable @keyup.enter="handleQuery" /> <el-input v-model="queryParams.materialsName" placeholder="请输入物资名称" clearable @keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button> <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@ -16,13 +24,20 @@
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row> </el-row>
</template> </template>
<!-- 外层表格使用expandedRowKeys控制展开状态 -->
<!-- 外层表格添加ref用于控制展开状态 --> <el-table
<el-table ref="outerTableRef" v-loading="loading" :data="materialsUseInventoryList" @expand-change="handleExpandChange" border> ref="outerTableRef"
v-loading="loading"
:data="materialsUseInventoryList"
@expand-change="handleExpandChange"
border
:expanded-row-keys="expandedRowKeys"
>
<el-table-column type="expand"> <el-table-column type="expand">
<template #default="props"> <template #default="props">
<div style="margin-left: 60px"> <div style="margin-left: 60px">
<el-table :data="materialsUseRecordList" border v-loading="loadingChild"> <!-- 子表格使用当前行的独立数据 -->
<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" type="index" width="60" />
<el-table-column label="使用数量" align="center" prop="useNumber" /> <el-table-column label="使用数量" align="center" prop="useNumber" />
<el-table-column label="剩余量" align="center" prop="residueNumber" /> <el-table-column label="剩余量" align="center" prop="residueNumber" />
@ -35,19 +50,20 @@
type="primary" type="primary"
icon="delete" icon="delete"
v-if="scope.row.ishow" v-if="scope.row.ishow"
@click="handleDelete(scope.row)" @click="handleDelete(scope.row, props.row.id)"
v-hasPermi="['materials:materialsUseRecord:remove']" v-hasPermi="['materials:materialsUseRecord:remove']"
>删除</el-button >删除</el-button
> >
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 子分页使用当前行的独立分页参数 -->
<pagination <pagination
v-show="totalChild > 0" v-show="getChildTotal(props.row.id) > 0"
:total="totalChild" :total="getChildTotal(props.row.id)"
v-model:page="queryParamsChild.pageNum" v-model:page="getChildQueryParams(props.row.id).pageNum"
v-model:limit="queryParamsChild.pageSize" v-model:limit="getChildQueryParams(props.row.id).pageSize"
@pagination="getListChild" @pagination="(page, limit) => handleChildPagination(props.row.id, page, limit)"
/> />
</div> </div>
</template> </template>
@ -69,8 +85,17 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" /> <pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-card> </el-card>
</el-col>
</el-row>
<!-- 添加或修改材料使用登记对话框 --> <!-- 添加或修改材料使用登记对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body> <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"> <el-form ref="materialsUseRecordFormRef" :model="form" :rules="rules" label-width="80px">
@ -104,39 +129,53 @@ import {
updateMaterialsUseRecord updateMaterialsUseRecord
} from '@/api/materials/materialsUseRecord'; } from '@/api/materials/materialsUseRecord';
import { MaterialsUseRecordVO, MaterialsUseRecordQuery, MaterialsUseRecordForm } from '@/api/materials/materialsUseRecord/types'; 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 { ElFormInstance, ElTable } from 'element-plus';
import useUserStore from '@/store/modules/user'; import useUserStore from '@/store/modules/user';
import { get } from 'lodash'; import { getMaterialsList } from '@/api/materials/materialOutbound';
// 类型定义补充若项目中无全局DialogOption类型需添加 // 类型定义补充
interface DialogOption { interface DialogOption {
visible: boolean; visible: boolean;
title: string; 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 { proxy } = getCurrentInstance() as ComponentInternalInstance;
const userStore = useUserStore(); const userStore = useUserStore();
const currentProject = computed(() => userStore.selectedProject); const currentProject = computed(() => userStore.selectedProject);
// 核心数据响应式定义 // 核心数据响应式定义
const materialsUseRecordList = ref<MaterialsUseRecordVO[]>([]); const materialsUseInventoryList = ref<any[]>([]);
const materialsUseInventoryList = ref<any[]>([]); // 外层列表数据类型可根据实际接口返回调整
const buttonLoading = ref(false); const buttonLoading = ref(false);
const loading = ref(true); const loading = ref(true);
const loadingChild = ref(true);
const showSearch = ref(true); const showSearch = ref(true);
const ids = ref<Array<string | number>>([]); const ids = ref<Array<string | number>>([]);
const single = ref(true); const single = ref(true);
const multiple = ref(true); const multiple = ref(true);
const total = ref(0); const total = ref(0);
const totalChild = ref(0); const expandedRowKeys = ref<number[]>([]); // 控制展开行的ID集合
// 子列表状态管理使用对象存储不同父行的子数据key为inventoryId
const childRowStates = ref<Record<number, ChildRowState>>({});
// 组件Ref定义 // 组件Ref定义
const queryFormRef = ref<ElFormInstance>(); const queryFormRef = ref<ElFormInstance>();
const materialsUseRecordFormRef = ref<ElFormInstance>(); const materialsUseRecordFormRef = ref<ElFormInstance>();
const outerTableRef = ref<InstanceType<typeof ElTable> | undefined>(undefined); // 外层表格Ref控制展开 const outerTableRef = ref<InstanceType<typeof ElTable> | undefined>(undefined);
const currentExpandInventoryId = ref<number | undefined>(undefined); // 存储当前展开行的inventoryId const currentOperateInventoryId = ref<number | undefined>(undefined); // 当前操作的父行ID
// 对话框与表单数据 // 对话框与表单数据
const dialog = reactive<DialogOption>({ const dialog = reactive<DialogOption>({
@ -160,72 +199,159 @@ const data = reactive({
pageSize: 10, pageSize: 10,
projectId: currentProject.value?.id, projectId: currentProject.value?.id,
outPut: 1, outPut: 1,
usePart: undefined // 补充usePart字段与表单prop对应 materialsName: undefined,
} as MaterialsUseRecordQuery & { outPut?: number; usePart?: string },
queryParamsChild: {
pageNum: 1,
pageSize: 10,
inventoryId: undefined,
usePart: undefined, usePart: undefined,
useNumber: undefined, materialsId: ''
residueNumber: undefined } as MaterialsUseRecordQuery & {
} as MaterialsUseRecordQuery, outPut?: number;
materialsName?: string;
usePart?: string;
},
rules: { rules: {
id: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
projectId: [{ required: true, message: '项目ID不能为空', trigger: 'blur' }], projectId: [{ required: true, message: '项目ID不能为空', trigger: 'blur' }],
inventoryId: [{ required: true, message: '库存ID不能为空', trigger: 'blur' }], inventoryId: [{ required: true, message: '库存ID不能为空', trigger: 'blur' }],
usePart: [{ required: true, message: '使用部位不能为空', trigger: 'blur' }], usePart: [{ required: true, message: '使用部位不能为空', trigger: 'blur' }],
useNumber: [{ required: true, message: '使用数量不能为空', trigger: 'blur' }], useNumber: [{ required: true, message: '使用数量不能为空', trigger: 'blur' }]
residueNumber: [{ 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 () => { const getList = async () => {
loading.value = true; loading.value = true;
try { try {
const res = await listMaterialsUseInventory(queryParams.value); const res = await listMaterialsUseInventory(queryParams.value);
materialsUseInventoryList.value = res.rows; materialsUseInventoryList.value = res.rows;
total.value = res.total; 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 { } finally {
loading.value = false; loading.value = false;
} }
}; };
/** 处理外层表格展开/折叠记录当前展开行的inventoryId */ /** 处理外层表格展开/折叠 */
const handleExpandChange = (row: any) => { const handleExpandChange = async (row: any, expandedRows: any[]) => {
currentExpandInventoryId.value = row.id; // 记录展开行ID const inventoryId = row.id;
queryParamsChild.value.inventoryId = row.id; // 更新展开行ID集合
getListChild(); 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 () => { const getListChild = async (inventoryId: number) => {
loadingChild.value = true; const childState = getOrInitChildState(inventoryId);
childState.loading = true;
try { try {
const res = await listMaterialsUseRecord(queryParamsChild.value); const res = await listMaterialsUseRecord(childState.queryParams);
materialsUseRecordList.value = res.rows; childState.list = res.rows;
// 控制首行删除按钮显示 // 控制首行删除按钮显示
if (res.rows.length > 0) { if (childState.list.length > 0) {
materialsUseRecordList.value[0].ishow = true; childState.list[0].ishow = true;
} }
totalChild.value = res.total; childState.total = res.total;
} finally { } 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 = () => { const cancel = () => {
reset(); reset();
dialog.visible = false; dialog.visible = false;
currentOperateInventoryId.value = undefined;
}; };
/** 表单重置 */ /** 表单重置 */
const reset = () => { const reset = () => {
form.value = { ...initFormData }; form.value = { ...initFormData, projectId: currentProject.value?.id };
materialsUseRecordFormRef.value?.resetFields(); materialsUseRecordFormRef.value?.resetFields();
}; };
@ -238,10 +364,16 @@ const handleQuery = () => {
/** 重置按钮操作 */ /** 重置按钮操作 */
const resetQuery = () => { const resetQuery = () => {
queryFormRef.value?.resetFields(); queryFormRef.value?.resetFields();
queryParams.value = {
...data.queryParams,
pageNum: 1,
pageSize: 10,
projectId: currentProject.value?.id
};
handleQuery(); handleQuery();
}; };
/** 多选框选中数据(当前模板未使用,保留原逻辑) */ /** 多选框选中数据处理 */
const handleSelectionChange = (selection: MaterialsUseRecordVO[]) => { const handleSelectionChange = (selection: MaterialsUseRecordVO[]) => {
ids.value = selection.map((item) => item.id); ids.value = selection.map((item) => item.id);
single.value = selection.length !== 1; single.value = selection.length !== 1;
@ -252,76 +384,87 @@ const handleSelectionChange = (selection: MaterialsUseRecordVO[]) => {
const handleAdd = async (row: any) => { const handleAdd = async (row: any) => {
reset(); reset();
form.value.inventoryId = row.id; form.value.inventoryId = row.id;
currentOperateInventoryId.value = row.id;
dialog.visible = true; dialog.visible = true;
dialog.title = '添加材料使用登记'; dialog.title = '添加材料使用登记';
}; };
/** 修改按钮操作(当前模板未使用,保留原逻辑) */ /** 修改按钮操作 */
const handleUpdate = async (row?: MaterialsUseRecordVO) => { const handleUpdate = async (row?: MaterialsUseRecordVO) => {
if (!row && ids.value.length === 0) return;
reset(); reset();
const _id = row?.id || ids.value[0]; const _id = row?.id || ids.value[0];
const res = await getMaterialsUseRecord(_id); const res = await getMaterialsUseRecord(_id);
Object.assign(form.value, res.data); Object.assign(form.value, res.data);
currentOperateInventoryId.value = form.value.inventoryId;
dialog.visible = true; dialog.visible = true;
dialog.title = '修改材料使用登记'; dialog.title = '修改材料使用登记';
}; };
/** 提交按钮:核心优化逻辑(刷新外层列表+保留展开状态) */ /** 提交表单操作 */
const submitForm = () => { const submitForm = () => {
materialsUseRecordFormRef.value?.validate(async (valid: boolean) => { materialsUseRecordFormRef.value?.validate(async (valid: boolean) => {
if (valid) { if (!valid || !currentOperateInventoryId.value) return;
buttonLoading.value = true; buttonLoading.value = true;
try { try {
// 1. 执行添加/修改接口请求 // 执行添加修改操作
await addMaterialsUseRecord(form.value); const apiFn = form.value.id ? updateMaterialsUseRecord : addMaterialsUseRecord;
// 2. 刷新外层列表(确保外层数据同步,如剩余量) await apiFn(form.value);
// 刷新外层列表和当前操作行的子列表
await getList(); await getList();
// 3. 刷新当前展开行的子列表 await getListChild(currentOperateInventoryId.value);
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('操作成功'); proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false; dialog.visible = false;
} finally { } finally {
buttonLoading.value = false; buttonLoading.value = false;
} }
}
}); });
}; };
/** 删除按钮操作 */ /** 删除操作 */
const handleDelete = async (row?: MaterialsUseRecordVO) => { const handleDelete = async (row: MaterialsUseRecordVO, inventoryId: number) => {
const _ids = row?.id || ids.value;
try { try {
await proxy?.$modal.confirm(`是否确认删除材料使用登记编号为"${_ids}"的数据项`); await proxy?.$modal.confirm(`是否确认删除该记录`);
await delMaterialsUseRecord(_ids); await delMaterialsUseRecord(row.id);
await getList(); await getList();
await getListChild(); await getListChild(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('删除成功'); proxy?.$modal.msgSuccess('删除成功');
await getListChild(); } catch (error) {
} finally { // 取消删除时不做处理
loading.value = false;
} }
}; };
/** 导出按钮操作(保留原逻辑) */ /** 导出操作 */
const handleExport = () => { const handleExport = () => {
proxy?.download('materials/materialsUseRecord/export', { ...queryParams.value }, `materialsUseRecord_${new Date().getTime()}.xlsx`); proxy?.download('materials/materialsUseRecord/export', { ...queryParams.value }, `materialsUseRecord_${new Date().getTime()}.xlsx`);
}; };
/** 页面挂载时加载外层列表 */ // 监听项目ID变化刷新数据
onMounted(() => { 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(); getList();
}
);
// 页面卸载时清理监听
onUnmounted(() => {
listeningProject();
});
// 页面挂载时加载数据
onMounted(() => {
// getList();
getMaterialsListData();
}); });
</script> </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> <template>
<div class="overall-plan-material-supply"> <div class="overall-plan-material-supply">
<!-- tabPosition="left" -->
<el-card shadow="always"> <el-card shadow="always">
<template #header> <template #header>
<el-row :gutter="10" class="mb8"> <el-row :gutter="10" class="mb8">
@ -30,19 +29,28 @@
>一键全部保存</el-button >一键全部保存</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> </el-form>
<right-toolbar @queryTable="getMasterDataList"></right-toolbar> <right-toolbar @queryTable="getMasterDataList"></right-toolbar>
</el-row> </el-row>
</template> </template>
</el-card> </el-card>
<!-- 本地数据懒加载表格 -->
<el-table <el-table
:data="state.tableData" :data="state.tableData"
v-loading="state.loading.list" v-loading="state.loading.list"
ref="tableRef"
stripe stripe
style="width: 100%; margin-bottom: 20px; height: calc(100vh - 230px)" style="width: 100%; margin-bottom: 20px; height: calc(100vh - 230px)"
row-key="id" row-key="id"
border border
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }" :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:lazy="true"
:load="loadLocalChildNodes"
@expand-change="handleExpandChange"
> >
<el-table-column align="center" prop="num" label="编号" /> <el-table-column align="center" prop="num" label="编号" />
<el-table-column prop="name" label="工程或费用名称" width="180" /> <el-table-column prop="name" label="工程或费用名称" width="180" />
@ -50,87 +58,95 @@
<el-table-column prop="specification" label="规格型号" width="80" /> <el-table-column prop="specification" label="规格型号" width="80" />
<el-table-column prop="quantity" label="数量" width="100" /> <el-table-column prop="quantity" label="数量" width="100" />
<el-table-column prop="batchNumber" label="批次号" width="200" /> <el-table-column prop="batchNumber" label="批次号" width="200" />
<!-- 优化的输入框列 -->
<el-table-column prop="brand" label="品牌"> <el-table-column prop="brand" label="品牌">
<template #default="{ row }"> <template #default="{ row }">
<el-input <el-input
v-model="row.brand" v-model.lazy="row.brand"
:disabled="state.masterData.status != 'draft'" :disabled="state.masterData.status != 'draft'"
v-if="!(row.children && row.children.length > 0)" v-if="!row.hasChildren"
placeholder="" placeholder=""
clearable clearable
:key="`brand-${row.id}`"
@change="handleInputChange(row, 'brand')"
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="texture" label="材质"> <el-table-column prop="texture" label="材质">
<template #default="{ row }"> <template #default="{ row }">
<el-input <el-input
v-model="row.texture" v-model.lazy="row.texture"
:disabled="state.masterData.status != 'draft'" :disabled="state.masterData.status != 'draft'"
v-if="!(row.children && row.children.length > 0)" v-if="!row.hasChildren"
placeholder="" placeholder=""
clearable clearable
:key="`texture-${row.id}`"
@change="handleInputChange(row, 'texture')"
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="qualityStandard" label="质量标准"> <el-table-column prop="qualityStandard" label="质量标准">
<template #default="{ row }"> <template #default="{ row }">
<el-input <el-input
:disabled="state.masterData.status != 'draft'" :disabled="state.masterData.status != 'draft'"
v-model="row.qualityStandard" v-model.lazy="row.qualityStandard"
v-if="!(row.children && row.children.length > 0)" v-if="!row.hasChildren"
placeholder="" placeholder=""
clearable clearable
:key="`qualityStandard-${row.id}`"
@change="handleInputChange(row, 'qualityStandard')"
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="partUsed" label="使用部位"> <el-table-column prop="partUsed" label="使用部位">
<template #default="{ row }"> <template #default="{ row }">
<el-input <el-input
:disabled="state.masterData.status != 'draft'" :disabled="state.masterData.status != 'draft'"
v-model="row.partUsed" v-model.lazy="row.partUsed"
v-if="!(row.children && row.children.length > 0)" v-if="!row.hasChildren"
placeholder="" placeholder=""
clearable clearable
:key="`partUsed-${row.id}`"
@change="handleInputChange(row, 'partUsed')"
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="deliveryPoints" label="交货地点"> <el-table-column prop="deliveryPoints" label="交货地点">
<template #default="{ row }"> <template #default="{ row }">
<el-input <el-input
:disabled="state.masterData.status != 'draft'" :disabled="state.masterData.status != 'draft'"
v-model="row.deliveryPoints" v-model.lazy="row.deliveryPoints"
v-if="!(row.children && row.children.length > 0)" v-if="!row.hasChildren"
placeholder="" placeholder=""
clearable clearable
:key="`deliveryPoints-${row.id}`"
@change="handleInputChange(row, 'deliveryPoints')"
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="预计使用日期" width="150"> <el-table-column label="预计使用日期" width="150">
<template #default="{ row }"> <template #default="{ row }">
<el-date-picker <el-date-picker
v-model="row.dateService" v-model="row.dateService"
v-if="!(row.children && row.children.length > 0)" v-if="!row.hasChildren"
type="date" type="date"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
:disabled="state.masterData.status != 'draft'" :disabled="state.masterData.status != 'draft'"
placeholder="请选择日期" placeholder="请选择日期"
:key="`dateService-${row.id}`"
@change="handleInputChange(row, 'dateService')"
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="remark" label="备注" /> <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-table>
<!-- 编辑 --> <!-- 编辑弹窗 -->
<el-dialog v-model="visible" title="修改物料信息" :width="800" :close-on-click-modal="false" @close="handleClose"> <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-form ref="formRef" :model="formData" :rules="formRules" label-width="120px" class="space-y-4">
<el-row :gutter="20"> <el-row :gutter="20">
@ -145,7 +161,6 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<!-- 物料属性区域 -->
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="材质" prop="texture"> <el-form-item label="材质" prop="texture">
@ -158,7 +173,6 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<!-- 日期与状态区域 -->
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="使用部位" prop="partUsed"> <el-form-item label="使用部位" prop="partUsed">
@ -171,7 +185,6 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<!-- 其他信息区域 -->
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="预计使用日期" prop="dateService"> <el-form-item label="预计使用日期" prop="dateService">
@ -196,7 +209,8 @@
</template> </template>
<script setup name="billofQuantities"> <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 { import {
obtainMasterDataList, obtainMasterDataList,
totalsupplyplan, totalsupplyplan,
@ -205,13 +219,26 @@ import {
totalSupplyplanBatchEdit totalSupplyplanBatchEdit
} from '@/api/materials/overallPlanMaterialSupply/index'; } from '@/api/materials/overallPlanMaterialSupply/index';
import { useUserStoreHook } from '@/store/modules/user'; import { useUserStoreHook } from '@/store/modules/user';
// 全局状态与实例获取
const userStore = useUserStoreHook(); const userStore = useUserStoreHook();
const currentProject = computed(() => userStore.selectedProject); const currentProject = computed(() => userStore.selectedProject);
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
// 基础状态管理
const visible = ref(false); const visible = ref(false);
const formRef = ref(null); 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({ const state = reactive({
tableData: [], tableData: [], // 初始只存放根节点
queryForm: { queryForm: {
projectId: currentProject.value?.id, projectId: currentProject.value?.id,
versions: '', versions: '',
@ -224,9 +251,9 @@ const state = reactive({
sheets: false, sheets: false,
list: false list: false
}, },
// 主id
masterData: {} masterData: {}
}); });
// 表单数据 // 表单数据
const formData = reactive({ const formData = reactive({
batchNumber: '', batchNumber: '',
@ -248,6 +275,7 @@ const formData = reactive({
texture: '', texture: '',
unit: '' unit: ''
}); });
// 表单验证规则 // 表单验证规则
const formRules = reactive({ const formRules = reactive({
name: [ name: [
@ -264,71 +292,190 @@ const formRules = reactive({
], ],
compileDate: [{ required: true, message: '请选择编制日期', trigger: 'change' }] 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.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() { async function getMasterDataList() {
try { try {
// 获取主数据列表
state.loading.list = true; state.loading.list = true;
// 获取主数据
const masterDataRes = await obtainMasterDataList({ const masterDataRes = await obtainMasterDataList({
projectId: currentProject.value?.id projectId: currentProject.value?.id
}); });
const { data: masterData } = masterDataRes; const { data: masterData } = masterDataRes;
console.log('masterData', masterData);
if (!masterData[0].id) { if (!masterData[0]?.id) {
console.warn('未获取到有效的主数据ID');
state.tableData = []; state.tableData = [];
fullTreeData.value = [];
return; return;
} }
state.masterData = masterData[0]; state.masterData = masterData[0];
// 获取完整供应计划数据
// 获取供应计划 const supplyPlanRes = await totalsupplyplan({ id: masterData[0].id, projectId: currentProject.value?.id });
const supplyPlanRes = await totalsupplyplan({ id: masterData[0].id }); const allData = supplyPlanRes.rows || [];
// 存储完整数据到本地
// 处理结果 fullTreeData.value = [...allData];
if (supplyPlanRes.list == null) { // 初始只加载根节点pid为空或0的节点
// state.tableData = supplyPlanRes.rows || []; const rootNodes = allData.filter((item) => !item.pid || item.pid == 0);
state.tableData = proxy?.handleTree(supplyPlanRes.rows, 'sid', 'pid'); // 标记根节点是否有子节点
console.log('state.tableData', state.tableData); state.tableData = rootNodes.map((node) => ({
} else { ...node,
// 根据实际业务逻辑处理有list的情况 hasChildren: allData.some((child) => child.pid === node.sid)
state.tableData = []; }));
// 初始化可编辑数据映射
editDataMap.value.clear();
allData.forEach((item) => {
if (!item.hasChildren) {
// 只关注叶子节点
editDataMap.value.set(item.id, { ...item });
} }
});
} catch (error) { } catch (error) {
console.error('获取主数据列表失败:', error);
// 错误情况下给默认值,避免页面出错
state.tableData = []; state.tableData = [];
fullTreeData.value = [];
} finally { } finally {
state.loading.list = false; state.loading.list = false;
} }
} }
// 获取详情
// 修改获取详情的方法 /**
* 获取详情
*/
async function totalSupplyplanDetail(id) { async function totalSupplyplanDetail(id) {
try { try {
const result = await totalSupplyplanDetails(id); const result = await totalSupplyplanDetails(id);
if (result?.code === 200) { if (result?.code === 200) {
const detailData = result.data || {}; const detailData = result.data || {};
// 1. 清空原有表单数据
// 清空表单并赋值
Object.keys(formData).forEach((key) => { Object.keys(formData).forEach((key) => {
formData[key] = undefined; formData[key] = undefined;
}); });
// 2. 处理日期格式假设接口返回的是Date对象或ISO字符串
// 处理日期格式
const formatDate = (date) => { const formatDate = (date) => {
if (!date) return ''; if (!date) return '';
// 若为字符串先转为Date对象
const d = typeof date === 'string' ? new Date(date) : 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')}`; return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}; };
// 3. 合并数据到formData响应式赋值
Object.assign(formData, { Object.assign(formData, {
...detailData, ...detailData,
// 单独处理日期字段,转为表单可识别的字符串格式
compileDate: formatDate(detailData.compileDate), compileDate: formatDate(detailData.compileDate),
dateService: formatDate(detailData.dateService) dateService: formatDate(detailData.dateService)
}); });
console.log('表单数据已更新:', formData);
} else { } else {
ElMessage.error(`获取详情失败: ${result?.msg || '未知错误'}`); ElMessage.error(`获取详情失败: ${result?.msg || '未知错误'}`);
} }
@ -339,87 +486,113 @@ async function totalSupplyplanDetail(id) {
state.loading.list = false; state.loading.list = false;
} }
} }
// 修改
/**
* 一键保存修改
*/
const editApprovalSheet = async () => { const editApprovalSheet = async () => {
state.loading.list = true; state.loading.list = true;
await totalSupplyplanBatchEdit(state.tableData);
proxy.$modal.msgSuccess('修改成功');
state.loading.list = false;
};
// 提交表单
const handleSubmit = async () => {
try { try {
// 表单验证 // 只提交修改过的数据从Map中获取
await formRef.value.validate(); const modifiedData = Array.from(editDataMap.value.values());
// 触发提交事件 if (modifiedData.length === 0) {
editMaterialSupply(formData); ElMessage.info('没有数据需要修改');
handleClose();
} catch (error) {
// 验证失败不提交
console.error('表单验证失败:', error);
return; return;
} }
};
// 修改物资 await totalSupplyplanBatchEdit(modifiedData);
function editMaterialSupply(formData) {
materialChangeSupplyplan(formData).then((res) => {
if (res.code === 200) {
ElMessage.success('修改成功'); ElMessage.success('修改成功');
getMasterDataList(); getMasterDataList(); // 重新加载数据
} else { } catch (error) {
ElMessage.error('修改失败'); ElMessage.error(`保存失败:${error.message}`);
} finally {
state.loading.list = false;
} }
});
}
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 = () => { const handleClose = () => {
visible.value = false; visible.value = false;
// 清空表单数据 formRef.value?.resetFields();
Object.keys(formData).forEach((key) => { Object.keys(formData).forEach((key) => {
formData[key] = undefined; formData[key] = undefined;
}); });
// 重置表单验证状态
formRef.value?.resetFields();
}; };
// 审批 /**
* 审批
*/
function clickApprovalSheet1() { function clickApprovalSheet1() {
proxy.$tab.closePage(proxy.$route); proxy.$tab.closePage(proxy.$route);
proxy.$router.push({ proxy.$router.push({
path: `/approval/overallPlanMaterialSupply/indexEdit`, path: `/approval/overallPlanMaterialSupply/indexEdit`,
query: { query: { id: state.masterData.id, type: 'update' }
id: state.masterData.id,
type: 'update'
}
}); });
} }
// 审核流程
/**
* 查看流程
*/
function lookApprovalFlow() { function lookApprovalFlow() {
proxy.$router.push({ proxy.$router.push({
path: `/approval/overallPlanMaterialSupply/indexEdit`, path: `/approval/overallPlanMaterialSupply/indexEdit`,
query: { query: { id: state.masterData.id, type: 'view' }
id: state.masterData.id,
type: 'view'
}
}); });
} }
onMounted(() => {
/**
* 监听项目变化
*/
const listeningProject = watch(
() => currentProject.value?.id,
(nid) => {
if (nid) getMasterDataList();
}
);
// 导入成功处理
const handleSuccess = () => {
ElMessage.success('导入成功');
getMasterDataList(); getMasterDataList();
};
// 生命周期
onMounted(() => {
if (currentProject.value?.id) {
getMasterDataList();
}
});
onUnmounted(() => {
listeningProject();
loadingInstance.value?.close();
}); });
</script> </script>
<style> <style>
.overall-plan-material-supply { .overall-plan-material-supply {
padding: 20px; padding: 20px;

View File

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

View File

@ -30,9 +30,7 @@
:row-class-name="tableRowClassName" :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="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"> <el-table-column prop="issuanceTime" label="联系单下达时间" min-width="160" align="center">
<template #default="scope"> <template #default="scope">

View File

@ -14,6 +14,7 @@
</el-form-item> </el-form-item>
<el-form-item label="类型" prop="type"> <el-form-item label="类型" prop="type">
<el-select v-model="queryParams.type" placeholder="请选择类型"> <el-select v-model="queryParams.type" placeholder="请选择类型">
<el-option label="全部" value="0" />
<el-option label="对甲" value="1" /> <el-option label="对甲" value="1" />
<el-option label="对乙" value="2" /> <el-option label="对乙" value="2" />
</el-select> </el-select>
@ -36,7 +37,6 @@
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row> </el-row>
</template> </template>
<el-table v-loading="loading" :data="monthPlanList"> <el-table v-loading="loading" :data="monthPlanList">
<el-table-column type="index" label="序号" width="55" align="center" /> <el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column label="计划月份" align="center" prop="planMonth" /> <el-table-column label="计划月份" align="center" prop="planMonth" />
@ -82,7 +82,7 @@
<el-form-item label="计划产值(元)" prop="planValue"> <el-form-item label="计划产值(元)" prop="planValue">
<el-input v-model="form.planValue" placeholder="请输入计划产值" type="number" /> <el-input v-model="form.planValue" placeholder="请输入计划产值" type="number" />
</el-form-item> </el-form-item>
<el-form-item label="计划月份(元)" prop="planMonth"> <el-form-item label="计划月份" prop="planMonth">
<el-date-picker v-model="form.planMonth" type="month" value-format="YYYY-MM" placeholder="请选择计划月份" /> <el-date-picker v-model="form.planMonth" type="month" value-format="YYYY-MM" placeholder="请选择计划月份" />
</el-form-item> </el-form-item>
<el-form-item label="产值类型" prop="valueType"> <el-form-item label="产值类型" prop="valueType">
@ -103,7 +103,7 @@
<script setup name="MonthPlan" lang="ts"> <script setup name="MonthPlan" lang="ts">
import { listMonthPlan, getMonthPlan, delMonthPlan, addMonthPlan, updateMonthPlan } from '@/api/out/monthPlan'; 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 { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { out_value_type } = toRefs<any>(proxy?.useDict('out_value_type')); const { out_value_type } = toRefs<any>(proxy?.useDict('out_value_type'));
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status')); const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
@ -164,7 +164,7 @@ const data = reactive({
valueType: undefined, valueType: undefined,
planAuditStatus: undefined, planAuditStatus: undefined,
completeAuditStatus: undefined, completeAuditStatus: undefined,
type: '1', type: '0',
params: {} params: {}
}, },
rules: { rules: {
@ -181,7 +181,11 @@ const { queryParams, form, rules } = toRefs(data);
/** 查询月度产值计划列表 */ /** 查询月度产值计划列表 */
const getList = async () => { const getList = async () => {
loading.value = true; 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; monthPlanList.value = res.rows;
total.value = res.total; total.value = res.total;
loading.value = false; loading.value = false;

View File

@ -175,7 +175,7 @@ const getInfo = () => {
form.value = res.data as any; form.value = res.data as any;
console.log('🚀 ~ getInfo ~ form.value:', form.value[0].projectId); console.log('🚀 ~ getInfo ~ form.value:', form.value[0].projectId);
form.value[0].mid = form.value[0].id;
form.value[0].id = form.value[0].projectId + '_' + form.value[0].planMonth; form.value[0].id = form.value[0].projectId + '_' + form.value[0].planMonth;
loading.value = false; loading.value = false;
buttonLoading.value = false; buttonLoading.value = false;
@ -231,13 +231,15 @@ const approvalVerifyOpen = async () => {
// 图纸上传成功之后 开始提交 // 图纸上传成功之后 开始提交
const submit = async (status, data) => { const submit = async (status, data) => {
form.value = data; form.value = data;
console.log(form.value);
if (status === 'draft') { if (status === 'draft') {
buttonLoading.value = false; buttonLoading.value = false;
proxy?.$modal.msgSuccess('暂存成功'); proxy?.$modal.msgSuccess('暂存成功');
proxy.$tab.closePage(proxy.$route); proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1); proxy.$router.go(-1);
} else { } else {
const res = await isSubmit(data[1]?.id); const res = await isSubmit(data[0]?.mid);
if (!res.data) { if (!res.data) {
proxy?.$modal.msgError('三种计划产值必须填写'); proxy?.$modal.msgError('三种计划产值必须填写');

View File

@ -86,13 +86,14 @@ const handleTabChange = (tab) => {
activeTab.value = tab; activeTab.value = tab;
data.queryParams.valueType = '1'; data.queryParams.valueType = '1';
// data.queryParams.month = ''; // data.queryParams.month = '';
getList();
}; };
/** 查询项目总产值分配列表 */ /** 查询项目总产值分配列表 */
const getList = async () => { const getList = async () => {
loading.value = true; loading.value = true;
const res = await listOutTable(queryParams.value); const res = await listOutTable({ ...queryParams.value, type: activeTab.value });
valueAllocationList.value = res.rows; valueAllocationList.value = res.rows;
total.value = res.total; total.value = res.total;
loading.value = false; loading.value = false;

View File

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

View File

@ -298,6 +298,11 @@ const getList = async () => {
const res = await getProjectSquare(currentProject.value?.id); const res = await getProjectSquare(currentProject.value?.id);
if (res.data.length === 0) { if (res.data.length === 0) {
proxy?.$modal.msgWarning('当前项目下没有方阵,请先创建方阵'); proxy?.$modal.msgWarning('当前项目下没有方阵,请先创建方阵');
matrixOptions.value = [];
queryParams.value.projectId = '';
form.value.projectId = '';
tabList.value = [];
activeTab.value = '';
} else { } else {
let matrixList = res.data.map((item) => { let matrixList = res.data.map((item) => {
return { return {
@ -472,6 +477,7 @@ const matrixValue = ref<number | undefined>(matrixOptions.value.length > 0 ? mat
const resetMatrix = () => { const resetMatrix = () => {
matrixValue.value = undefined; matrixValue.value = undefined;
queryParams.value.matrixId = undefined; queryParams.value.matrixId = undefined;
queryParams.value.projectId = undefined;
matrixOptions.value = []; matrixOptions.value = [];
}; };
@ -491,8 +497,6 @@ onMounted(() => {
const listeningProject = watch( const listeningProject = watch(
() => currentProject.value?.id, () => currentProject.value?.id,
(nid, oid) => { (nid, oid) => {
queryParams.value.projectId = nid;
form.value.projectId = nid;
resetMatrix(); resetMatrix();
getList(); getList();
} }

View File

@ -27,9 +27,9 @@
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['land:enterRoad:add']">新增</el-button> <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['land:enterRoad:add']">新增</el-button>
</el-col> </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-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-col :span="1.5">
<el-upload <el-upload
ref="uploadRef" ref="uploadRef"

View File

@ -36,9 +36,9 @@
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['land:landBlock:add']">新增</el-button> <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['land:landBlock:add']">新增</el-button>
</el-col> </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-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-col :span="1.5">
<el-upload ref="uploadRef" class="upload-demo" :http-request="handleImport" :show-file-list="false"> <el-upload ref="uploadRef" class="upload-demo" :http-request="handleImport" :show-file-list="false">
<template #trigger> <template #trigger>
@ -78,6 +78,7 @@
</el-table> </el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" /> <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card> </el-card>
<!-- 地块表单弹窗 --> <!-- 地块表单弹窗 -->
<el-dialog draggable :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body> <el-dialog draggable :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="landBlockFormRef" :model="form" :rules="rules" label-width="100px"> <el-form ref="landBlockFormRef" :model="form" :rules="rules" label-width="100px">
@ -107,20 +108,22 @@
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
<!-- 关联方阵弹窗核心修改区域 -->
<!-- 关联方阵弹窗核心修复区域 -->
<el-dialog draggable :title="dialogMatrix.title" v-model="dialogMatrix.visible" width="900px" append-to-body> <el-dialog draggable :title="dialogMatrix.title" v-model="dialogMatrix.visible" width="900px" append-to-body>
<el-button type="primary" plain icon="Plus" @click="addUnitBoItem" style="margin-bottom: 15px">添加</el-button> <el-button type="primary" plain icon="Plus" @click="addUnitBoItem" style="margin-bottom: 15px">添加</el-button>
<!-- 方阵表单绑定unitBoList的索引实现动态校验 --> <!-- 修复1表单模型绑定formM根模型确保嵌套字段校验生效 -->
<el-form ref="landBlockFormMatrixRef" :model="formM" label-width="100px"> <el-form ref="landBlockFormMatrixRef" :model="formM" label-width="100px">
<el-row v-for="(item, i) of unitBoList" :key="i" class="mb-4"> <!-- 修复2循环formM.unitBoList而非独立ref保证数据双向绑定 -->
<!-- 方阵选择必填校验 --> <el-row v-for="(item, i) of formM.unitBoList" :key="i" class="mb-4">
<!-- 方阵选择修复校验规则移除min:2改为min:1适配单层级选择 -->
<el-col :span="8"> <el-col :span="8">
<el-form-item <el-form-item
label="方阵" label="方阵"
:prop="`unitBoList[${i}].unitProjectId`" :prop="`unitBoList[${i}].unitProjectId`"
:rules="[ :rules="[
{ required: true, message: '请选择方阵', trigger: 'change' }, { required: true, message: '请选择方阵', trigger: 'change' },
{ type: 'array', min: 2, message: '请选择完整的方阵层级', trigger: 'change' } { type: 'array', min: 1, message: '请选择完整的方阵', trigger: 'change' }
]" ]"
> >
<el-cascader <el-cascader
@ -133,7 +136,7 @@
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- 所属工区必填校验 --> <!-- 所属工区保留原有规则 -->
<el-col :span="8"> <el-col :span="8">
<el-form-item <el-form-item
label="所属工区" label="所属工区"
@ -143,7 +146,7 @@
<el-input v-model="item.unitProjectArea" placeholder="请输入所属工区" /> <el-input v-model="item.unitProjectArea" placeholder="请输入所属工区" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- 方阵状态必填校验 --> <!-- 方阵状态保留原有规则 -->
<el-col :span="6"> <el-col :span="6">
<el-form-item <el-form-item
label="方阵状态" label="方阵状态"
@ -153,8 +156,15 @@
<el-input v-model="item.unitProjectStatus" placeholder="请输入方阵状态" /> <el-input v-model="item.unitProjectStatus" placeholder="请输入方阵状态" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- 删除按钮禁用逻辑优化至少保留1项 -->
<el-col :span="1" style="margin-left: 15px; display: flex; align-items: flex-end"> <el-col :span="1" style="margin-left: 15px; display: flex; align-items: flex-end">
<el-button style="margin-bottom: 18px" type="danger" icon="Delete" @click="removeUnitBoItem(i)"></el-button> <el-button
style="margin-bottom: 18px"
type="danger"
icon="Delete"
@click="removeUnitBoItem(i)"
:disabled="formM.unitBoList.length <= 1"
></el-button>
</el-col> </el-col>
</el-row> </el-row>
</el-form> </el-form>
@ -181,21 +191,35 @@ import {
} from '@/api/system/landTransfer/landBlock'; } from '@/api/system/landTransfer/landBlock';
import { LandBlockVO, LandBlockQuery, LandBlockForm } from '@/api/system/landTransfer/landBlock/types'; import { LandBlockVO, LandBlockQuery, LandBlockForm } from '@/api/system/landTransfer/landBlock/types';
import { useUserStoreHook } from '@/store/modules/user'; import { useUserStoreHook } from '@/store/modules/user';
import { getCurrentInstance, ComponentInternalInstance, onMounted, onUnmounted, watch } from 'vue'; import { getCurrentInstance, ComponentInternalInstance, onMounted, onUnmounted, watch, reactive, ref, toRefs, computed } from 'vue';
import { ElFormInstance, ElMessage } from 'element-plus'; import { ElFormInstance } from 'element-plus';
// 类型定义补充避免any // 类型定义补充
interface DialogOption { interface DialogOption {
visible: boolean; visible: boolean;
title: string; title: string;
} }
// 方阵级联选择器选项类型
interface FangzhenOption {
matrixId: string | number;
name: string;
children?: FangzhenOption[];
}
// 方阵表单项类型
interface UnitBoItem { interface UnitBoItem {
unitProjectArea: string; unitProjectArea: string;
unitProjectStatus: string; unitProjectStatus: string;
unitProjectId: (string | number)[]; // 级联选择值(数组) unitProjectId: (string | number)[]; // 级联选择值(数组)
} }
// 方阵表单根模型类型关键显式声明unitBoList
interface MatrixForm {
landId?: string | number; // 关联的地块ID
unitBoList: UnitBoItem[]; // 方阵列表
}
// 基础实例与Store // 基础实例与Store
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const userStore = useUserStoreHook(); const userStore = useUserStoreHook();
@ -203,14 +227,17 @@ const currentProject = computed(() => userStore.selectedProject);
// 响应式数据 // 响应式数据
const landBlockList = ref<LandBlockVO[]>([]); const landBlockList = ref<LandBlockVO[]>([]);
const fangzhenList = ref<any[]>([]); // 方阵列表(实际项目建议定义具体类型) const fangzhenList = ref<FangzhenOption[]>([]);
const buttonLoading = ref(false); const buttonLoading = ref(false);
const loading = ref(true); const loading = ref(true);
const showSearch = ref(true); const showSearch = ref(true);
const uploadRef = ref<any>(null); // upload组件ref const uploadRef = ref<any>(null);
// 方阵表单数据(核心修改:初始值为空,避免默认填充无效数据 // 方阵表单模型(核心修使用reactive并显式声明结构
const unitBoList = ref<UnitBoItem[]>([{ unitProjectArea: '', unitProjectStatus: '', unitProjectId: [] }]); const formM = ref<MatrixForm>({
landId: undefined,
unitBoList: [{ unitProjectArea: '', unitProjectStatus: '', unitProjectId: [] }]
});
// 表格选择相关 // 表格选择相关
const ids = ref<Array<string | number>>([]); const ids = ref<Array<string | number>>([]);
@ -243,7 +270,6 @@ const initFormData: LandBlockForm = {
// 核心数据(含表单规则) // 核心数据(含表单规则)
const data = reactive({ const data = reactive({
form: { ...initFormData }, form: { ...initFormData },
formM: { landId: undefined }, // 方阵关联表单仅存地块ID
queryParams: { queryParams: {
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
@ -256,7 +282,7 @@ const data = reactive({
farmerCount: undefined, farmerCount: undefined,
params: {} params: {}
}, },
// 地块表单规则(原有规则保留) // 地块表单规则
rules: { rules: {
id: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }], id: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
projectId: [{ required: true, message: '项目ID不能为空', trigger: 'blur' }], projectId: [{ required: true, message: '项目ID不能为空', trigger: 'blur' }],
@ -265,7 +291,7 @@ const data = reactive({
} }
}); });
const { queryParams, form, rules, formM } = toRefs(data); const { queryParams, form, rules } = toRefs(data);
/** 查询地块列表 */ /** 查询地块列表 */
const getList = async () => { const getList = async () => {
@ -381,10 +407,10 @@ const getfangzhenList = async () => {
loading.value = true; loading.value = true;
try { try {
const res = await subMatrix(currentProject.value.id); const res = await subMatrix(currentProject.value.id);
// 处理方阵数据(级联选择需父子结构,此处保留原有逻辑 // 处理方阵数据(级联选择需父子结构)
res.data.forEach((item: any) => { res.data.forEach((item: any) => {
item.children?.forEach((item2: any) => { item.children?.forEach((item2: any) => {
item2.matrixId = `${item2.name}_${item2.matrixId}`; // 拼接名称+ID便于后续拆分 item2.matrixId = `${item2.name}_${item2.matrixId}`;
}); });
}); });
fangzhenList.value = res.data; fangzhenList.value = res.data;
@ -395,55 +421,62 @@ const getfangzhenList = async () => {
} }
}; };
/** 关联方阵(核心修改:打开弹窗前强制重置表单) */ /** 关联方阵 */
const handleView = async (row: LandBlockVO) => { const handleView = async (row: LandBlockVO) => {
if (!row?.id) return proxy?.$modal.msgWarning('请选择有效的地块'); if (!row?.id) return proxy?.$modal.msgWarning('请选择有效的地块');
console.log('🚀 ~ handleView ~ row:', row);
// 1. 重置方阵表单(清空历史数据) // 重置方阵表单
resetMatrix(); resetMatrix();
// 2. 绑定当前地块ID // 绑定当前地块ID
formM.value.landId = row.id; formM.value.landId = row.id;
// 3. 打开弹窗 // 打开弹窗
dialogMatrix.visible = true; dialogMatrix.visible = true;
dialogMatrix.title = `关联方阵(地块:${row.landName || row.landCode}`; dialogMatrix.title = `关联方阵(地块:${row.landName || row.landCode}`;
}; };
/** 新增方阵表单项 */ /** 新增方阵表单项 */
const addUnitBoItem = () => { const addUnitBoItem = () => {
unitBoList.value.push({ formM.value.unitBoList.push({
unitProjectArea: '', unitProjectArea: '',
unitProjectStatus: '', unitProjectStatus: '',
unitProjectId: [] // 级联选择初始为空数组 unitProjectId: []
}); });
// 重置校验状态
landBlockFormMatrixRef.value?.clearValidate();
}; };
/** 删除方阵表单项 */ /** 删除方阵表单项 */
const removeUnitBoItem = (index: number) => { const removeUnitBoItem = (index: number) => {
if (unitBoList.value.length <= 1) { if (formM.value.unitBoList.length <= 1) {
return proxy?.$modal.msgWarning('至少保留一项方阵配置'); return proxy?.$modal.msgWarning('至少保留一项方阵配置');
} }
unitBoList.value.splice(index, 1); formM.value.unitBoList.splice(index, 1);
// 重置表单校验状态(避免删除后残留校验提示)
landBlockFormMatrixRef.value?.clearValidate(); landBlockFormMatrixRef.value?.clearValidate();
}; };
/** 提交方阵关联表单 */ /** 提交方阵关联表单(核心修复:数据处理逻辑) */
const submitFormMatrix = () => { const submitFormMatrix = () => {
landBlockFormMatrixRef.value?.validate(async (valid: boolean) => { landBlockFormMatrixRef.value?.validate(async (valid: boolean) => {
if (!valid) return; if (!valid) return;
if (!formM.value.landId) return proxy?.$modal.msgWarning('地块ID异常请重新选择地块'); if (!formM.value.landId) return proxy?.$modal.msgWarning('地块ID异常请重新选择地块');
try { try {
// 处理方阵数据(拆分名称+ID // 处理方阵数据(修复ID拆分逻辑
const unitBoListParams = unitBoList.value.map((item) => { const unitBoListParams = formM.value.unitBoList.map((item) => {
const [unitProjectName, unitProjectId] = item.unitProjectId[1]?.split('_') || []; // 取级联选择的最后一层值
if (!unitProjectId) throw new Error('方阵ID解析失败请重新选择方阵'); const lastLevelValue = item.unitProjectId[item.unitProjectId.length - 1];
if (!lastLevelValue) throw new Error('请选择完整的方阵');
// 拆分名称和ID
const [unitProjectName, unitProjectId] = String(lastLevelValue).split('_');
if (!unitProjectId) throw new Error('方阵ID解析失败请重新选择');
return { return {
unitProjectArea: item.unitProjectArea.trim(), unitProjectArea: item.unitProjectArea.trim(),
unitProjectStatus: item.unitProjectStatus.trim(), unitProjectStatus: item.unitProjectStatus.trim(),
unitProjectId: unitProjectId, // 纯ID后端需要 unitProjectId: unitProjectId,
unitProjectName: unitProjectName // 名称(可选,用于显示) unitProjectName: unitProjectName
}; };
}); });
@ -456,7 +489,7 @@ const submitFormMatrix = () => {
if (res.code === 200) { if (res.code === 200) {
proxy?.$modal.msgSuccess('关联方阵成功'); proxy?.$modal.msgSuccess('关联方阵成功');
dialogMatrix.visible = false; dialogMatrix.visible = false;
await getList(); // 刷新地块列表 await getList();
} else { } else {
proxy?.$modal.msgError(res.msg || '关联失败'); proxy?.$modal.msgError(res.msg || '关联失败');
} }
@ -472,17 +505,14 @@ const cancelMatrix = () => {
dialogMatrix.visible = false; dialogMatrix.visible = false;
}; };
/** 方阵表单重置(核心修改:清空所有数据+重置校验) */ /** 方阵表单重置 */
const resetMatrix = () => { const resetMatrix = () => {
// 1. 清空地块ID
formM.value.landId = undefined;
// 2. 重置方阵列表(仅保留一个空项)
unitBoList.value = [{ unitProjectArea: '', unitProjectStatus: '', unitProjectId: [] }];
// 3. 重置表单校验状态
if (landBlockFormMatrixRef.value) { if (landBlockFormMatrixRef.value) {
landBlockFormMatrixRef.value.resetFields(); landBlockFormMatrixRef.value.resetFields();
landBlockFormMatrixRef.value.clearValidate(); landBlockFormMatrixRef.value.clearValidate();
} }
formM.value.landId = undefined;
formM.value.unitBoList = [{ unitProjectArea: '', unitProjectStatus: '', unitProjectId: [] }];
}; };
/** 监听项目变化,刷新数据 */ /** 监听项目变化,刷新数据 */
@ -491,11 +521,11 @@ const listeningProject = watch(
(newId) => { (newId) => {
if (newId) { if (newId) {
queryParams.value.projectId = newId; queryParams.value.projectId = newId;
getfangzhenList(); // 刷新方阵列表 getfangzhenList();
getList(); // 刷新地块列表 getList();
} }
}, },
{ immediate: true } // 初始加载时执行一次 { immediate: true }
); );
/** 导入Excel */ /** 导入Excel */
@ -524,36 +554,34 @@ const handleImport = (options: { file: File }) => {
loading.value = false; loading.value = false;
}); });
}; };
/** 导出模板 */
const exportFile = () => { const exportFile = () => {
// 导出模版文件
try { try {
// 创建a标签
const link = document.createElement('a'); const link = document.createElement('a');
// 设置PDF文件路径 - 相对于public目录
link.href = '/dikuai.xlsx'; link.href = '/dikuai.xlsx';
// 设置下载后的文件名
link.download = '地块信息导入模版.xlsx'; link.download = '地块信息导入模版.xlsx';
// 触发点击
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
// 清理
document.body.removeChild(link); document.body.removeChild(link);
} catch (error) { } catch (error) {
alert('下载失败,请重试'); alert('下载失败,请重试');
} }
}; };
/** 下载导入模板 */ /** 下载导入模板 */
const downloadTemplate = () => { const downloadTemplate = () => {
try { try {
const link = document.createElement('a'); const link = document.createElement('a');
link.href = '/landBlock.xlsx'; // 模板路径需确保public目录下存在该文件 link.href = '/landBlock.xlsx';
link.download = '地块信息导入模板.xlsx'; // 下载后文件名 link.download = '地块信息导入模板.xlsx';
document.body.appendChild(link); document.body.appendChild(link);
link.click(); // 触发下载 link.click();
} catch (err) { } catch (err) {
proxy?.$modal.msgError('模板下载失败,请重试'); proxy?.$modal.msgError('模板下载失败,请重试');
} finally { } finally {
document.body.removeChild(link); // 清理DOM const link = document.querySelector('a[download="地块信息导入模板.xlsx"]');
if (link) document.body.removeChild(link);
} }
}; };

View File

@ -144,13 +144,16 @@
</el-table-column> --> </el-table-column> -->
<el-table-column label="备注" align="center" prop="remark" /> <el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180" /> <el-table-column label="创建时间" align="center" prop="createTime" width="180" />
<el-table-column fixed="right" label="操作" align="center" class-name="small-padding fixed-width" width="400"> <el-table-column fixed="right" label="操作" align="center" width="500">
<template #default="scope"> <template #default="scope">
<el-space> <el-space>
<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="FolderOpened" @click="handleShowUpload(scope.row)" v-hasPermi="['project:project:edit']" <el-button link type="primary" icon="FolderOpened" @click="handleShowUpload(scope.row)" v-hasPermi="['project:project:edit']"
>导入安全协议书 >导入安全协议书
</el-button> </el-button>
<el-button link type="success" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['project:project:edit']">修改 </el-button> <el-button link type="success" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['project:project:edit']">修改 </el-button>
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['project:project:remove']">删除 </el-button> <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['project:project:remove']">删除 </el-button>
<el-button link type="primary" icon="upload" @click="handleUpload(scope.row)" v-hasPermi="['project:project:saveTenderFile']" <el-button link type="primary" icon="upload" @click="handleUpload(scope.row)" v-hasPermi="['project:project:saveTenderFile']"
@ -160,7 +163,6 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" /> <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card> </el-card>
<!-- 添加或修改项目对话框 --> <!-- 添加或修改项目对话框 -->
@ -253,78 +255,6 @@
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
<div class="block-box">
<div class="">打卡设置</div>
<el-row :gutter="20">
<el-col :span="12" :offset="0">
<el-form-item label="打卡开始时间" prop="playCardStart" label-width="110px">
<!-- <el-time-picker value-format="HH:mm" v-model="form.playCardStart" placeholder="请输入打卡开始时间" /> -->
<el-time-select
v-model="form.playCardStart"
style="width: 100%"
class="mr-4"
placeholder="请输入打卡开始时间"
value-format="HH:mm"
start="00:00"
step="00:15"
end="23:59"
/>
</el-form-item>
</el-col>
<el-col :span="12" :offset="0">
<el-form-item label="打卡结束时间" prop="playCardEnd" label-width="110px">
<!-- <el-time-picker value-format="HH:mm" v-model="form.playCardEnd" placeholder="请输入打卡结束时间" /> -->
<el-time-select
v-model="form.playCardEnd"
style="width: 100%"
:min-time="form.playCardStart"
class="mr-4"
placeholder="请输入打卡结束时间"
value-format="HH:mm"
start="00:00"
step="00:15"
end="23:59"
/>
</el-form-item>
</el-col>
<el-col :span="12" :offset="0">
<el-form-item label="打卡类型" prop="playCardStart" label-width="110px">
<!-- <el-time-picker value-format="HH:mm" v-model="form.playCardStart" placeholder="请输入打卡开始时间" /> -->
<el-time-select
v-model="form.playCardStart"
style="width: 100%"
class="mr-4"
placeholder="请输入打卡开始时间"
value-format="HH:mm"
start="00:00"
step="00:15"
end="23:59"
/>
</el-form-item>
</el-col>
<el-col :span="12" :offset="0">
<el-form-item label="工作日" prop="playCardEnd" label-width="110px">
<!-- <el-time-picker value-format="HH:mm" v-model="form.playCardEnd" placeholder="请输入打卡结束时间" /> -->
<el-time-select
v-model="form.playCardEnd"
style="width: 100%"
:min-time="form.playCardStart"
class="mr-4"
placeholder="请输入打卡结束时间"
value-format="HH:mm"
start="00:00"
step="00:15"
end="23:59"
/>
</el-form-item>
</el-col>
<!-- <el-col :span="24" :offset="0">
<el-form-item label="安全协议书" prop="securityAgreement">
<file-upload v-model="form.securityAgreement" :limit="1" :file-type="['pdf']" :file-size="50" />
</el-form-item>
</el-col> -->
</el-row>
</div>
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
@ -342,13 +272,12 @@
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
<!-- //选取项目地址弹窗 --> <!-- //选取项目地址弹窗 -->
<el-dialog v-model="amapStatus" :title="form.projectName + '-获取经纬度'" width="80%"> <el-dialog draggable v-model="amapStatus" :title="form.projectName + '-获取经纬度'" width="80%">
<amap height="620px" @setLocation="setPoi"></amap> <amap height="620px" @setLocation="setPoi"></amap>
</el-dialog> </el-dialog>
<!-- 选取方阵地址 --> <!-- 选取方阵地址 -->
<el-dialog title="设置方阵" v-model="polygonStatus" width="1400px" :close-on-click-modal="false"> <el-dialog draggable title="设置方阵" v-model="polygonStatus" width="1400px" :close-on-click-modal="false">
<open-layers-map <open-layers-map
:project-id="projectId" :project-id="projectId"
:design-id="designId" :design-id="designId"
@ -356,7 +285,7 @@
@close="polygonStatus = false" @close="polygonStatus = false"
></open-layers-map> ></open-layers-map>
</el-dialog> </el-dialog>
<el-dialog title="添加子项目" v-model="childProjectStatus" width="400"> <el-dialog draggable title="添加子项目" v-model="childProjectStatus" width="400">
<span>填写子项目名称</span> <span>填写子项目名称</span>
<el-input v-model="childProjectForm.projectName"></el-input> <el-input v-model="childProjectForm.projectName"></el-input>
<template #footer> <template #footer>
@ -366,7 +295,7 @@
</span> </span>
</template> </template>
</el-dialog> </el-dialog>
<el-dialog title="上传文件" v-model="fileVisble" width="400"> <el-dialog draggable title="上传文件" v-model="fileVisble" width="400">
<file-upload v-model="fileForm.tenderFiles" :limit="10" :file-type="['pdf']" :file-size="50" /> <file-upload v-model="fileForm.tenderFiles" :limit="10" :file-type="['pdf']" :file-size="50" />
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
@ -375,6 +304,91 @@
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
<el-dialog draggable title="打卡规则" v-model="ruleFlag" width="800">
<template #footer>
<el-form ref="projectFormRef" :model="form" :rules="rules" label-width="100px">
<el-row :gutter="20">
<el-col :span="12" :offset="0">
<el-form-item label="上班时间" prop="clockInTime">
<el-time-select
v-model="form.clockInTime"
style="width: 100%"
class="mr-4"
placeholder="请输入打卡开始时间"
value-format="HH:mm"
start="00:00"
step="00:15"
end="23:59"
/>
</el-form-item>
</el-col>
<el-col :span="12" :offset="0">
<el-form-item label="下班时间" prop="clockOutTime">
<el-time-select
v-model="form.clockOutTime"
style="width: 100%"
:min-time="form.clockInTime"
class="mr-4"
placeholder="请输入打卡结束时间"
value-format="HH:mm"
start="00:00"
step="00:15"
end="23:59"
/>
</el-form-item>
</el-col>
<el-col :span="24" :offset="0">
<el-form-item label="打卡类型" prop="type">
<el-radio-group v-model="form.type">
<el-radio value="1" size="large">无限制</el-radio>
<el-radio value="2" size="large">范围内打卡</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="打卡类型" prop="weekday">
<el-checkbox-group v-model="form.weekday" size="small">
<el-checkbox label="星期一" value="1" />
<el-checkbox label="星期二" value="2" />
<el-checkbox label="星期三" value="3" />
<el-checkbox label="星期四" value="4" />
<el-checkbox label="星期五" value="5" />
<el-checkbox label="星期六" value="6" />
<el-checkbox label="星期日" value="7" />
</el-checkbox-group>
</el-form-item>
</el-col> </el-row
></el-form>
<div class="dialog-footer">
<el-button type="primary" @click="ruleSubmit"> 提交</el-button>
<el-button @click="ruleFlag = false">取消</el-button>
</div>
</template>
</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 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 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 @click="ScopeFlag = false">取消</el-button>
</div>
</template>
</el-dialog>
<CesiumEarthDialog ref="earthDialog" @send-data="handleEarthData"
:position="position"
:data="earthData"
@close="handleEarthClose"></CesiumEarthDialog>
</div> </div>
</template> </template>
@ -382,17 +396,24 @@
import { import {
addChildProject, addChildProject,
addProject, addProject,
addProjectFacilities,
addProjectPilePoint,
addProjectSquare,
delProject, delProject,
uploadProjectFile, uploadProjectFile,
getProject, getProject,
listProject, listProject,
updateProject updateProject,
attendanceRuleAdd,
attendanceRuleEdit,
byProjectIdDetail,
getAttendanceRangeList,
updateAttendanceRange,
delAttendanceRange
} from '@/api/project/project'; } from '@/api/project/project';
import { ProjectForm, ProjectQuery, ProjectVO, childProjectQuery, locationType } from '@/api/project/project/types'; import { ProjectForm, ProjectQuery, ProjectVO, childProjectQuery, locationType } from '@/api/project/project/types';
import amap from '@/components/amap/index.vue'; 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 { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable, project_category_type, project_type, project_stage } = toRefs<any>( 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') proxy?.useDict('sys_normal_disable', 'project_category_type', 'project_type', 'project_stage')
@ -413,6 +434,22 @@ const polygonStatus = ref(false);
const dxfFile = ref(null); const dxfFile = ref(null);
const projectId = ref<string>(''); const projectId = ref<string>('');
const designId = 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: '',
punchColor: '#1983ff'
}
]);
const childProjectForm = reactive<childProjectQuery>({ const childProjectForm = reactive<childProjectQuery>({
projectName: '', projectName: '',
pid: '', pid: '',
@ -432,7 +469,7 @@ const fileForm = ref({
const jsonData = ref(null); const jsonData = ref(null);
const fullscreenLoading = ref(false); const fullscreenLoading = ref(false);
const initFormData: ProjectForm = { const initFormData = {
id: undefined, id: undefined,
projectName: undefined, projectName: undefined,
shortName: undefined, shortName: undefined,
@ -451,13 +488,15 @@ const initFormData: ProjectForm = {
lat: undefined, lat: undefined,
plan: undefined, plan: undefined,
onStreamTime: undefined, onStreamTime: undefined,
playCardStart: undefined, clockInTime: undefined,
playCardEnd: undefined, clockOutTime: undefined,
designTotal: undefined, designTotal: undefined,
securityAgreement: undefined, securityAgreement: undefined,
sort: 0, sort: 0,
showHidden: undefined, showHidden: undefined,
isDelete: undefined isDelete: undefined,
type: '1',
weekday: []
}; };
const data = reactive<PageData<ProjectForm, ProjectQuery>>({ const data = reactive<PageData<ProjectForm, ProjectQuery>>({
form: { ...initFormData }, form: { ...initFormData },
@ -480,8 +519,6 @@ const data = reactive<PageData<ProjectForm, ProjectQuery>>({
lat: undefined, lat: undefined,
plan: undefined, plan: undefined,
onStreamTime: undefined, onStreamTime: undefined,
playCardStart: undefined,
playCardEnd: undefined,
designTotal: undefined, designTotal: undefined,
securityAgreement: undefined, securityAgreement: undefined,
sort: undefined, sort: undefined,
@ -490,8 +527,8 @@ const data = reactive<PageData<ProjectForm, ProjectQuery>>({
params: {} params: {}
}, },
rules: { rules: {
playCardStart: [{ required: true, message: '打卡开始时间不能为空', trigger: 'blur' }], clockInTime: [{ required: true, message: '打卡开始时间不能为空', trigger: 'blur' }],
playCardEnd: [{ required: true, message: '打卡结束时间不能为空', trigger: 'blur' }], clockOutTime: [{ required: true, message: '打卡结束时间不能为空', trigger: 'blur' }],
projectName: [{ required: true, message: '项目名称不能为空', trigger: 'blur' }], projectName: [{ required: true, message: '项目名称不能为空', trigger: 'blur' }],
shortName: [{ required: true, message: '项目简称不能为空', trigger: 'blur' }], shortName: [{ required: true, message: '项目简称不能为空', trigger: 'blur' }],
principalPhone: [{ required: true, message: '负责人电话不能为空', trigger: 'blur' }], principalPhone: [{ required: true, message: '负责人电话不能为空', trigger: 'blur' }],
@ -692,7 +729,86 @@ 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 = () => {
// 提交打卡范围
};
// 添加规则
const handleCheckRules = async (row?: ProjectVO) => {
reset();
const _id = row?.id || ids.value[0];
const res = await byProjectIdDetail(_id);
if (res.data) {
res.data.weekday = res.data.weekday.split(',');
Object.assign(form.value, res.data);
}
projectId.value = row.id;
ruleFlag.value = true;
};
const ruleSubmit = async () => {
console.log(form.value);
projectFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
let obj = {
weekday: form.value.weekday.join(','),
projectId: projectId.value,
id: projectId.value,
clockInTime: form.value.clockInTime,
clockOutTime: form.value.clockOutTime,
type: form.value.type
};
if (form.value.id) {
await attendanceRuleEdit(obj);
} else {
await attendanceRuleAdd(obj);
}
ruleFlag.value = false;
await getList();
}
});
};
/** 导出按钮操作 */ /** 导出按钮操作 */
const handleExport = () => { const handleExport = () => {
proxy?.download( proxy?.download(
@ -703,7 +819,36 @@ const handleExport = () => {
`project_${new Date().getTime()}.xlsx` `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(() => { onMounted(() => {
getList(); 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-option v-for="item in team_clock_type" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</el-form-item> </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-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" /> <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item> </el-form-item>
@ -98,13 +103,15 @@
</template> </template>
<script setup name="ProjectTeam" lang="ts"> <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 { ProjectTeamForm, ProjectTeamQuery, ProjectTeamVO } from '@/api/project/projectTeam/types';
import { useUserStoreHook } from '@/store/modules/user'; import { useUserStoreHook } from '@/store/modules/user';
import UserListDialog from '@/views/project/projectTeam/component/UserListDialog.vue'; import UserListDialog from '@/views/project/projectTeam/component/UserListDialog.vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { team_clock_type } = toRefs<any>(proxy?.useDict('team_clock_type')); const { team_clock_type } = toRefs<any>(proxy?.useDict('team_clock_type'));
console.log(team_clock_type);
// 获取用户 store // 获取用户 store
const userStore = useUserStoreHook(); const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目 // 从 store 中获取项目列表和当前选中的项目
@ -117,6 +124,7 @@ const ids = ref<Array<string | number>>([]);
const single = ref(true); const single = ref(true);
const multiple = ref(true); const multiple = ref(true);
const total = ref(0); const total = ref(0);
const projectTeamRangeList = ref([]);
const currentRow = ref<ProjectTeamVO>({ const currentRow = ref<ProjectTeamVO>({
id: undefined, id: undefined,
projectId: undefined, projectId: undefined,
@ -140,7 +148,8 @@ const initFormData: ProjectTeamForm = {
teamName: undefined, teamName: undefined,
isClockIn: undefined, isClockIn: undefined,
remark: undefined, remark: undefined,
peopleNumber: undefined peopleNumber: undefined,
punchRangeList: undefined
}; };
const data = reactive<PageData<ProjectTeamForm, ProjectTeamQuery>>({ const data = reactive<PageData<ProjectTeamForm, ProjectTeamQuery>>({
form: { ...initFormData }, form: { ...initFormData },
@ -171,6 +180,14 @@ const getList = async () => {
total.value = res.total; total.value = res.total;
loading.value = false; loading.value = false;
}; };
/** 获取该项目的打开范围 "*/
const getClockIn = async () => {
if(currentProject.value?.id){
const res = await getProjectTeamClockIn({projectId:currentProject.value?.id});
projectTeamRangeList.value = res.rows
}
};
/** 取消按钮 */ /** 取消按钮 */
const cancel = () => { const cancel = () => {
@ -280,5 +297,6 @@ onUnmounted(() => {
onMounted(() => { onMounted(() => {
getList(); getList();
getClockIn();
}); });
</script> </script>

View File

@ -5,6 +5,11 @@
<div v-show="showSearch" class="mb-[10px]"> <div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover"> <el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true"> <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-form-item label="人员姓名" prop="userName">
<el-input v-model="queryParams.userName" placeholder="请输入人员姓名" clearable @keyup.enter="handleQuery" /> <el-input v-model="queryParams.userName" placeholder="请输入人员姓名" clearable @keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
@ -156,6 +161,7 @@
</el-button> </el-button>
<!-- <el-button link type="primary" icon="Switch" @click="handleToggle(scope.row)"> 切换人脸 </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="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="primary" icon="ChatLineSquare" @click="handleExit(scope.row)"> 入退场记录 </el-button>
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)"
v-hasPermi="['contractor:constructionUser:remove']"> v-hasPermi="['contractor:constructionUser:remove']">
@ -443,6 +449,30 @@
</template> </template>
</el-calendar> </el-calendar>
</el-dialog> </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> </div>
</template> </template>
@ -462,7 +492,10 @@ import {
getConstructionUserExit, getConstructionUserExit,
dowloadConstructionUserTemplate, dowloadConstructionUserTemplate,
importConstructionUserInfo, importConstructionUserInfo,
listConstructionMonth listConstructionMonth,
ProjectList,
TeamList,
TeamDistribution
} from '@/api/project/constructionUser'; } from '@/api/project/constructionUser';
import { import {
ConstructionUserForm, ConstructionUserForm,
@ -494,8 +527,8 @@ import { parseTime } from '@/utils/ruoyi';
const calendar = ref<CalendarInstance>(); const calendar = ref<CalendarInstance>();
const { proxy } = getCurrentInstance() as ComponentInternalInstance; 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>( 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') 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 // 获取用户 store
const userStore = useUserStoreHook(); const userStore = useUserStoreHook();
@ -511,6 +544,7 @@ const single = ref(true);
const multiple = ref(true); const multiple = ref(true);
const total = ref(0); const total = ref(0);
const skip = ref(false); const skip = ref(false);
const personnelAllocation = ref(false);
const fileStatus = ref(false); const fileStatus = ref(false);
const showFaceDrawer = ref(false); const showFaceDrawer = ref(false);
const statusDialog = ref(false); const statusDialog = ref(false);
@ -531,6 +565,10 @@ const queryFormRef = ref<ElFormInstance>();
const constructionUserFormRef = ref<ElFormInstance>(); const constructionUserFormRef = ref<ElFormInstance>();
const skipName = ref(''); const skipName = ref('');
const calendarList = ref<Array<AttendanceMonthVO>>([]); const calendarList = ref<Array<AttendanceMonthVO>>([]);
// 项目列表
const projectList = ref([]);
// 班组列表
const teamList = ref([]);
const dialog = reactive<DialogOption>({ const dialog = reactive<DialogOption>({
visible: false, visible: false,
title: '', title: '',
@ -543,6 +581,14 @@ const skipObject: skipType = reactive({
projectId: '', projectId: '',
contractorId: '' contractorId: ''
}); });
// 人员分配
const personnelAllocationObject = reactive({
memberId: null,
projectId: '',
teamId: '',
postId: '',
});
const contractorList = ref<Array<skipTeamType>>([]); const contractorList = ref<Array<skipTeamType>>([]);
//项目列表 //项目列表
const skipOptions = ref<Array<skipOptionType>>([]); const skipOptions = ref<Array<skipOptionType>>([]);
@ -661,6 +707,13 @@ const uploadPath = computed(() => {
return list; return list;
}); });
// 获取项目列表
const getProjectList = async () => {
const res = await ProjectList({});
projectList.value = res.rows;
projectList.value.unshift({ id: '', projectName: '全部' });
};
/** 返回文件上传状态 */ /** 返回文件上传状态 */
const uploadStatusColor = computed(() => (str: string) => { const uploadStatusColor = computed(() => (str: string) => {
switch (str) { switch (str) {
@ -825,7 +878,7 @@ const reset = () => {
/** 搜索按钮操作 */ /** 搜索按钮操作 */
const handleQuery = () => { const handleQuery = () => {
queryParams.value.pageNum = 1; 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(); getList();
}; };
@ -1053,6 +1106,36 @@ const listeningProject = watch(
getContractorList(); 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(() => { onUnmounted(() => {
listeningProject(); listeningProject();
@ -1060,6 +1143,7 @@ onUnmounted(() => {
onMounted(() => { onMounted(() => {
getContractorList(); getContractorList();
getProjectList();
}); });
</script> </script>
<style scoped lang="scss"> <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> </el-row>
</template> </template>
<!-- 关键修改开启lazy模式使用本地数据筛选子节点 -->
<el-table <el-table
ref="menuTableRef" ref="menuTableRef"
v-loading="loading" v-loading="loading"
@ -41,6 +42,9 @@
row-key="menuId" row-key="menuId"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }" :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:default-expand-all="isExpandAll" :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="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
<el-table-column prop="icon" label="图标" align="center" width="100"> <el-table-column prop="icon" label="图标" align="center" width="100">
@ -77,7 +81,7 @@
</el-table> </el-table>
</el-card> </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-form ref="menuFormRef" :model="form" :rules="rules" label-width="100px">
<el-row> <el-row>
<el-col :span="24"> <el-col :span="24">
@ -103,7 +107,6 @@
</el-col> </el-col>
<el-col v-if="form.menuType !== 'F'" :span="24"> <el-col v-if="form.menuType !== 'F'" :span="24">
<el-form-item label="菜单图标" prop="icon"> <el-form-item label="菜单图标" prop="icon">
<!-- 图标选择器 -->
<icon-select v-model="form.icon" /> <icon-select v-model="form.icon" />
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -122,10 +125,9 @@
<template #label> <template #label>
<span> <span>
<el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top"> <el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top">
<el-icon> <el-icon><question-filled /></el-icon>
<question-filled /> </el-tooltip>
</el-icon> </el-tooltip 是否外链
>是否外链
</span> </span>
</template> </template>
<el-radio-group v-model="form.isFrame"> <el-radio-group v-model="form.isFrame">
@ -139,9 +141,7 @@
<template #label> <template #label>
<span> <span>
<el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top"> <el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top">
<el-icon> <el-icon><question-filled /></el-icon>
<question-filled />
</el-icon>
</el-tooltip> </el-tooltip>
路由地址 路由地址
</span> </span>
@ -154,9 +154,7 @@
<template #label> <template #label>
<span> <span>
<el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top"> <el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
<el-icon> <el-icon><question-filled /></el-icon>
<question-filled />
</el-icon>
</el-tooltip> </el-tooltip>
组件路径 组件路径
</span> </span>
@ -170,9 +168,7 @@
<template #label> <template #label>
<span> <span>
<el-tooltip content="控制器中定义的权限字符,如:@SaCheckPermission('system:user:list')" placement="top"> <el-tooltip content="控制器中定义的权限字符,如:@SaCheckPermission('system:user:list')" placement="top">
<el-icon> <el-icon><question-filled /></el-icon>
<question-filled />
</el-icon>
</el-tooltip> </el-tooltip>
权限字符 权限字符
</span> </span>
@ -185,9 +181,7 @@
<template #label> <template #label>
<span> <span>
<el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top"> <el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top">
<el-icon> <el-icon><question-filled /></el-icon>
<question-filled />
</el-icon>
</el-tooltip> </el-tooltip>
路由参数 路由参数
</span> </span>
@ -199,9 +193,7 @@
<template #label> <template #label>
<span> <span>
<el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top"> <el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top">
<el-icon> <el-icon><question-filled /></el-icon>
<question-filled />
</el-icon>
</el-tooltip> </el-tooltip>
是否缓存 是否缓存
</span> </span>
@ -217,9 +209,7 @@
<template #label> <template #label>
<span> <span>
<el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top"> <el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top">
<el-icon> <el-icon><question-filled /></el-icon>
<question-filled />
</el-icon>
</el-tooltip> </el-tooltip>
显示状态 显示状态
</span> </span>
@ -234,9 +224,7 @@
<template #label> <template #label>
<span> <span>
<el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top"> <el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top">
<el-icon> <el-icon><question-filled /></el-icon>
<question-filled />
</el-icon>
</el-tooltip> </el-tooltip>
菜单状态 菜单状态
</span> </span>
@ -264,6 +252,20 @@
import { addMenu, delMenu, getMenu, listMenu, updateMenu } from '@/api/system/menu'; import { addMenu, delMenu, getMenu, listMenu, updateMenu } from '@/api/system/menu';
import { MenuForm, MenuQuery, MenuVO } from '@/api/system/menu/types'; import { MenuForm, MenuQuery, MenuVO } from '@/api/system/menu/types';
import { MenuTypeEnum } from '@/enums/MenuTypeEnum'; 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 { interface MenuOptionsType {
menuId: number; menuId: number;
@ -271,20 +273,29 @@ interface MenuOptionsType {
children: MenuOptionsType[] | undefined; children: MenuOptionsType[] | undefined;
} }
// 全局变量
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_show_hide, sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_show_hide', 'sys_normal_disable')); 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 menuList = ref<MenuVO[]>([]);
const loading = ref(true); const loading = ref(true);
const showSearch = ref(true); const showSearch = ref(true);
const menuOptions = ref<MenuOptionsType[]>([]); const menuOptions = ref<MenuOptionsType[]>([]);
const isExpandAll = ref(false); const isExpandAll = ref(false);
// 弹窗状态
const dialog = reactive<DialogOption>({ const dialog = reactive<DialogOption>({
visible: false, visible: false,
title: '' title: ''
}); });
// 表单相关
const queryFormRef = ref<ElFormInstance>(); const queryFormRef = ref<ElFormInstance>();
const menuFormRef = ref<ElFormInstance>(); const menuFormRef = ref<ElFormInstance>();
const initFormData = { const initFormData = {
@ -298,35 +309,114 @@ const initFormData = {
isFrame: '1', isFrame: '1',
isCache: '0', isCache: '0',
visible: '0', visible: '0',
status: '0' status: '0',
menuSource: 1
}; };
const data = reactive<PageData<MenuForm, MenuQuery>>({ const data = reactive({
form: { ...initFormData }, form: { ...initFormData },
queryParams: { queryParams: {
menuName: undefined, menuName: undefined,
status: undefined status: undefined,
menuSource: 1
}, },
rules: { rules: {
menuName: [{ required: true, message: '菜单名称不能为空', trigger: 'blur' }], menuName: [{ required: true, message: '菜单名称不能为空', trigger: 'blur' }],
orderNum: [{ 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 menuTableRef = ref<ElTableInstance>();
const { queryParams, form, rules } = toRefs<PageData<MenuForm, MenuQuery>>(data); const { queryParams, form, rules } = toRefs<PageData<MenuForm, MenuQuery>>(data);
/** 查询菜单列表 */
/**
* 初始化加载:一次性获取所有数据并处理
* 仅在表格展示根节点,子节点通过懒加载从本地数据中筛选
*/
const getList = async () => { const getList = async () => {
loading.value = true; loading.value = true;
try {
// 1. 一次性获取所有数据
const res = await listMenu(queryParams.value); const res = await listMenu(queryParams.value);
const data = proxy?.handleTree<MenuVO>(res.data, 'menuId'); fullMenuList.value = res.data || [];
if (data) {
menuList.value = data;
}
// 2. 构建缓存(提前计算所有节点的子节点)
buildChildrenCache();
console.log(11111);
// 3. 只展示根节点parentId=0
menuList.value = filterChildren(0);
} catch (err) {
proxy?.$modal.msgError('加载菜单失败');
} finally {
loading.value = false; 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 () => { const getTreeselect = async () => {
menuOptions.value = []; menuOptions.value = [];
@ -335,11 +425,13 @@ const getTreeselect = async () => {
menu.children = proxy?.handleTree<MenuOptionsType>(response.data, 'menuId'); menu.children = proxy?.handleTree<MenuOptionsType>(response.data, 'menuId');
menuOptions.value.push(menu); menuOptions.value.push(menu);
}; };
/** 取消按钮 */ /** 取消按钮 */
const cancel = () => { const cancel = () => {
reset(); reset();
dialog.visible = false; dialog.visible = false;
}; };
/** 表单重置 */ /** 表单重置 */
const reset = () => { const reset = () => {
form.value = { ...initFormData }; form.value = { ...initFormData };
@ -350,11 +442,13 @@ const reset = () => {
const handleQuery = () => { const handleQuery = () => {
getList(); getList();
}; };
/** 重置按钮操作 */ /** 重置按钮操作 */
const resetQuery = () => { const resetQuery = () => {
queryFormRef.value?.resetFields(); queryFormRef.value?.resetFields();
handleQuery(); handleQuery();
}; };
/** 新增按钮操作 */ /** 新增按钮操作 */
const handleAdd = (row?: MenuVO) => { const handleAdd = (row?: MenuVO) => {
reset(); reset();
@ -363,18 +457,24 @@ const handleAdd = (row?: MenuVO) => {
dialog.visible = true; dialog.visible = true;
dialog.title = '添加菜单'; dialog.title = '添加菜单';
}; };
/** 展开/折叠操作 */ /** 展开/折叠操作 */
const handleToggleExpandAll = () => { const handleToggleExpandAll = () => {
isExpandAll.value = !isExpandAll.value; isExpandAll.value = !isExpandAll.value;
toggleExpandAll(menuList.value, isExpandAll.value); toggleExpandAll(menuList.value, isExpandAll.value);
}; };
/** 展开/折叠所有 */
/** 展开/折叠所有节点 */
const toggleExpandAll = (data: MenuVO[], status: boolean) => { const toggleExpandAll = (data: MenuVO[], status: boolean) => {
data.forEach((item: MenuVO) => { data.forEach((item: MenuVO) => {
menuTableRef.value?.toggleRowExpansion(item, status); 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) => { const handleUpdate = async (row: MenuVO) => {
reset(); reset();
@ -386,25 +486,38 @@ const handleUpdate = async (row: MenuVO) => {
dialog.visible = true; dialog.visible = true;
dialog.title = '修改菜单'; dialog.title = '修改菜单';
}; };
/** 提交按钮 */ /** 提交按钮 */
const submitForm = () => { const submitForm = () => {
menuFormRef.value?.validate(async (valid: boolean) => { menuFormRef.value?.validate(async (valid: boolean) => {
if (valid) { 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('操作成功'); proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false; dialog.visible = false;
// 重新加载所有数据并重建缓存
await getList(); await getList();
} }
}); });
}; };
/** 删除按钮操作 */ /** 删除按钮操作 */
const handleDelete = async (row: MenuVO) => { const handleDelete = async (row: MenuVO) => {
await proxy?.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?'); await proxy?.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?');
await delMenu(row.menuId); await delMenu(row.menuId);
// 重新加载所有数据并重建缓存
await getList(); await getList();
proxy?.$modal.msgSuccess('删除成功'); proxy?.$modal.msgSuccess('删除成功');
}; };
// 初始化加载所有数据
onMounted(() => { onMounted(() => {
getList(); getList();
}); });

View File

@ -1,7 +1,6 @@
<template> <template>
<div class="p-2"> <div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
:leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]"> <div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover"> <el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true"> <el-form ref="queryFormRef" :model="queryParams" :inline="true">
@ -13,19 +12,11 @@
</el-form-item> </el-form-item>
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="角色状态" clearable> <el-select v-model="queryParams.status" placeholder="角色状态" clearable>
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
:value="dict.value" />
</el-select> </el-select>
</el-form-item> </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-form-item>
<el-button type="primary" icon="Search" @click="handleQuery" <el-button type="primary" icon="Search" @click="handleQuery" v-hasPermi="['system:role:query']">搜索</el-button>
v-hasPermi="['system:role:query']">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button> <el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -37,9 +28,18 @@
<el-col :lg="4" :xs="24" style=""> <el-col :lg="4" :xs="24" style="">
<el-card shadow="hover"> <el-card shadow="hover">
<el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable /> <el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable />
<el-tree ref="deptTreeRef" class="mt-2" node-key="id" :data="deptOptions" <el-tree
:props="{ label: 'label', children: 'children' }" :expand-on-click-node="false" ref="deptTreeRef"
:filter-node-method="filterNode" highlight-current default-expand-all @node-click="handleNodeClick" /> 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-card>
</el-col> </el-col>
<el-col :lg="20" :xs="24"> <el-col :lg="20" :xs="24">
@ -47,25 +47,21 @@
<template #header> <template #header>
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="1.5"> <el-col :span="1.5">
<el-button v-hasPermi="['system:role:add']" type="primary" plain icon="Plus" <el-button v-hasPermi="['system:role:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
@click="handleAdd()">新增</el-button>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button v-hasPermi="['system:role:edit']" type="success" plain :disabled="single" icon="Edit" <el-button v-hasPermi="['system:role:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()"
@click="handleUpdate()">修改</el-button> >修改</el-button
>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button v-hasPermi="['system:role:delete']" type="danger" plain :disabled="ids.length === 0" <el-button v-hasPermi="['system:role:delete']" type="danger" plain :disabled="ids.length === 0" @click="handleDelete()"
@click="handleDelete()">删除</el-button> >删除</el-button
</el-col> >
<el-col :span="1.5">
<el-button v-hasPermi="['system:role:export']" type="warning" plain icon="Download"
@click="handleExport">导出</el-button>
</el-col> </el-col>
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar> <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
</el-row> </el-row>
</template> </template>
<el-table ref="roleTableRef" v-loading="loading" :data="roleList" @selection-change="handleSelectionChange"> <el-table ref="roleTableRef" v-loading="loading" :data="roleList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" /> <el-table-column type="selection" width="55" align="center" />
<el-table-column v-if="false" label="角色编号" prop="roleId" width="120" /> <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="显示顺序" prop="roleSort" width="100" />
<el-table-column label="状态" align="center" width="100"> <el-table-column label="状态" align="center" width="100">
<template #default="scope"> <template #default="scope">
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
@change="handleStatusChange(scope.row)"></el-switch>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime"> <el-table-column label="创建时间" align="center" prop="createTime">
@ -87,27 +82,28 @@
<el-table-column fixed="right" label="操作" width="180"> <el-table-column fixed="right" label="操作" width="180">
<template #default="scope"> <template #default="scope">
<el-tooltip v-if="scope.row.roleId !== 1" content="修改" placement="top"> <el-tooltip v-if="scope.row.roleId !== 1" content="修改" placement="top">
<el-button v-hasPermi="['system:role:edit']" link type="primary" icon="Edit" <el-button v-hasPermi="['system:role:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
@click="handleUpdate(scope.row)"></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip v-if="scope.row.roleId !== 1" content="删除" placement="top"> <el-tooltip v-if="scope.row.roleId !== 1" content="删除" placement="top">
<el-button v-hasPermi="['system:role:remove']" link type="primary" icon="Delete" <el-button v-hasPermi="['system:role:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
@click="handleDelete(scope.row)"></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip v-if="scope.row.roleId !== 1" content="数据权限" placement="top"> <el-tooltip v-if="scope.row.roleId !== 1" content="数据权限" placement="top">
<el-button v-hasPermi="['system:role:edit']" link type="primary" icon="CircleCheck" <el-button v-hasPermi="['system:role:edit']" link type="primary" icon="CircleCheck" @click="handleDataScope(scope.row)"></el-button>
@click="handleDataScope(scope.row)"></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip v-if="scope.row.roleId !== 1" content="分配用户" placement="top"> <el-tooltip v-if="scope.row.roleId !== 1" content="分配用户" placement="top">
<el-button v-hasPermi="['system:role:edit']" link type="primary" icon="User" <el-button v-hasPermi="['system:role:edit']" link type="primary" icon="User" @click="handleAuthUser(scope.row)"></el-button>
@click="handleAuthUser(scope.row)"></el-button>
</el-tooltip> </el-tooltip>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<pagination v-if="total > 0" v-model:total="total" v-model:page="queryParams.pageNum" <pagination
v-model:limit="queryParams.pageSize" @pagination="getList" /> v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
@ -115,8 +111,15 @@
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body> <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 ref="roleFormRef" :model="form" :rules="rules" label-width="110px">
<el-form-item label="所属部门" prop="deptId"> <el-form-item label="所属部门" prop="deptId">
<el-cascader :options="deptOptions" v-model="form.deptId" placeholder="请选择所属部门" clearable filterable <el-cascader
:show-all-levels="false" :props="{ value: 'id', emitPath: false, checkStrictly: true }" @change=""> :options="deptOptions"
v-model="form.deptId"
placeholder="请选择所属部门"
clearable
filterable
:show-all-levels="false"
:props="{ value: 'id', emitPath: false, checkStrictly: true }"
>
</el-cascader> </el-cascader>
</el-form-item> </el-form-item>
<el-form-item label="角色名称" prop="roleName"> <el-form-item label="角色名称" prop="roleName">
@ -138,23 +141,26 @@
</el-form-item> </el-form-item>
<el-form-item label="状态"> <el-form-item label="状态">
<el-radio-group v-model="form.status"> <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 v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
}}</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="菜单权限"> <el-form-item label="菜单权限">
<el-checkbox v-model="menuExpand" <el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand(Boolean($event), 'menu')">展开/折叠</el-checkbox>
@change="handleCheckedTreeExpand(Boolean($event), 'menu')">展开/折叠</el-checkbox>
<el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event, 'menu')">全选/全不选</el-checkbox> <el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event, 'menu')">全选/全不选</el-checkbox>
<el-checkbox v-model="form.menuCheckStrictly" <el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event, 'menu')">父子联动</el-checkbox>
@change="handleCheckedTreeConnect($event, 'menu')">父子联动</el-checkbox> <el-tree
<el-tree ref="menuRef" class="tree-border" :data="menuOptions" show-checkbox node-key="id" ref="menuRef"
:check-strictly="!form.menuCheckStrictly" empty-text="加载中请稍候" class="tree-border"
:props="{ label: 'label', children: 'children' }"></el-tree> :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>
<el-form-item label="是否为特殊角色"> <el-form-item label="是否为特殊角色">
<el-switch v-model="form.isSpecial" active-value="1" inactive-value="0" active-text="是" inactive-text="否"> <el-switch v-model="form.isSpecial" active-value="1" inactive-value="0" active-text="是" inactive-text="否"> </el-switch>
</el-switch>
</el-form-item> </el-form-item>
<el-form-item label="备注"> <el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input> <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
@ -179,19 +185,24 @@
</el-form-item> </el-form-item>
<el-form-item label="权限范围"> <el-form-item label="权限范围">
<el-select v-model="form.dataScope" @change="dataScopeSelectChange"> <el-select v-model="form.dataScope" @change="dataScopeSelectChange">
<el-option v-for="item in dataScopeOptions" :key="item.value" :label="item.label" <el-option v-for="item in dataScopeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
:value="item.value"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item v-show="form.dataScope === '2'" label="数据权限"> <el-form-item v-show="form.dataScope === '2'" label="数据权限">
<el-checkbox v-model="deptExpand" <el-checkbox v-model="deptExpand" @change="handleCheckedTreeExpand(Boolean($event), 'dept')">展开/折叠</el-checkbox>
@change="handleCheckedTreeExpand(Boolean($event), 'dept')">展开/折叠</el-checkbox>
<el-checkbox v-model="deptNodeAll" @change="handleCheckedTreeNodeAll($event, 'dept')">全选/全不选</el-checkbox> <el-checkbox v-model="deptNodeAll" @change="handleCheckedTreeNodeAll($event, 'dept')">全选/全不选</el-checkbox>
<el-checkbox v-model="form.deptCheckStrictly" <el-checkbox v-model="form.deptCheckStrictly" @change="handleCheckedTreeConnect($event, 'dept')">父子联动</el-checkbox>
@change="handleCheckedTreeConnect($event, 'dept')">父子联动</el-checkbox> <el-tree
<el-tree ref="deptRef" class="tree-border" :data="deptOptions" show-checkbox default-expand-all node-key="id" ref="deptRef"
:check-strictly="!form.deptCheckStrictly" empty-text="加载中请稍候" class="tree-border"
:props="{ label: 'label', children: 'children' }"></el-tree> :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-item>
</el-form> </el-form>
<template #footer> <template #footer>
@ -252,7 +263,7 @@ const menuRef = ref<ElTreeInstance>();
const deptRef = ref<ElTreeInstance>(); const deptRef = ref<ElTreeInstance>();
const deptTreeRef = ref<ElTreeInstance>(); const deptTreeRef = ref<ElTreeInstance>();
const initForm: RoleForm = { const initForm = {
roleId: undefined, roleId: undefined,
roleSort: 1, roleSort: 1,
status: '0', status: '0',
@ -265,10 +276,11 @@ const initForm: RoleForm = {
menuIds: [], menuIds: [],
deptId: '', deptId: '',
isSpecial: null, isSpecial: null,
deptIds: [] deptIds: [],
roleSource: '1'
}; };
const data = reactive<PageData<RoleForm, RoleQuery>>({ const data = reactive({
form: { ...initForm }, form: { ...initForm },
queryParams: { queryParams: {
pageNum: 1, pageNum: 1,
@ -276,7 +288,8 @@ const data = reactive<PageData<RoleForm, RoleQuery>>({
roleName: '', roleName: '',
roleKey: '', roleKey: '',
deptId: '', deptId: '',
status: '' status: '',
roleSource: '1'
}, },
rules: { rules: {
roleName: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }], roleName: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }],
@ -377,7 +390,7 @@ const handleAuthUser = (row: RoleVO) => {
/** 查询菜单树结构 */ /** 查询菜单树结构 */
const getMenuTreeselect = async () => { const getMenuTreeselect = async () => {
const res = await menuTreeselect(); const res = await menuTreeselect({ menuSource: '1' });
menuOptions.value = res.data; menuOptions.value = res.data;
}; };
/** 所有部门节点数据 */ /** 所有部门节点数据 */
@ -427,14 +440,14 @@ const handleUpdate = async (row?: RoleVO) => {
}; };
/** 根据角色ID查询菜单树结构 */ /** 根据角色ID查询菜单树结构 */
const getRoleMenuTreeselect = (roleId: string | number) => { const getRoleMenuTreeselect = (roleId: string | number) => {
return roleMenuTreeselect(roleId).then((res): RoleMenuTree => { return roleMenuTreeselect(roleId, { menuSource: '1' }).then((res): RoleMenuTree => {
menuOptions.value = res.data.menus; menuOptions.value = res.data.menus;
return res.data; return res.data;
}); });
}; };
/** 根据角色ID查询部门树结构 */ /** 根据角色ID查询部门树结构 */
const getRoleDeptTreeSelect = async (roleId: string | number) => { const getRoleDeptTreeSelect = async (roleId: string | number) => {
const res = await deptTreeSelect(roleId); const res = await deptTreeSelect(roleId, { roleSource: '1' });
deptOptions.value = res.data.depts; deptOptions.value = res.data.depts;
return res.data; return res.data;
}; };

View File

@ -0,0 +1,258 @@
<template>
<div class="p-2 editInfo">
<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">
<el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="30" />
</el-form-item>
</el-col>
<el-col :span="2"></el-col>
<el-col :span="10">
<el-form-item label="归属部门" prop="deptId">
<el-tree-select
v-model="form.deptId"
:data="enabledDeptOptions"
:props="{ value: 'id', label: 'label', children: 'children' }"
value-key="id"
placeholder="请选择归属部门"
check-strictly
@change="handleDeptChange"
/>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" />
</el-form-item> </el-col
><el-col :span="2"></el-col>
<el-col :span="10">
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item v-if="form.userId == undefined" label="用户名称" prop="userName">
<el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" />
</el-form-item> </el-col
><el-col :span="2"></el-col>
<el-col :span="10">
<el-form-item v-if="form.userId == undefined" label="用户密码" prop="password">
<el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" show-password />
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="用户性别">
<el-select v-model="form.sex" placeholder="请选择">
<el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
</el-select>
</el-form-item> </el-col
><el-col :span="2"></el-col>
<el-col :span="10">
<el-form-item label="岗位">
<el-select v-model="form.postIds" multiple placeholder="请选择">
<el-option
v-for="item in postOptions"
:key="item.postId"
:label="item.postName"
:value="item.postId"
:disabled="item.status == '1'"
></el-option>
</el-select>
</el-form-item> </el-col
><el-col :span="2"></el-col>
<el-col :span="10">
<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-col>
<el-col :span="24">
<el-form-item label="备注">
<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 size="large" @click="cancel()"> </el-button>
<el-button size="large" type="primary" @click="submitForm"> </el-button>
</div>
</div>
</template>
<script setup name="Profile" lang="ts">
import api from '@/api/system/user';
import { listProject } from '@/api/project/project';
import { getProjectByDeptId, getRoleList, optionselect } from '@/api/system/post';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable, sys_user_sex } = toRefs<any>(proxy?.useDict('sys_normal_disable', 'sys_user_sex'));
const enabledDeptOptions = ref([]);
const deptOptions = ref([]);
const projectOptions = ref<any[]>([]);
const postOptions = ref([]);
const roleOptions = ref([]);
const userFormRef = ref<ElFormInstance>();
const initFormData = {
userId: undefined,
deptId: undefined,
userName: '',
nickName: undefined,
password: '',
phonenumber: undefined,
email: undefined,
sex: undefined,
projectRoles: [
{
projectId: '',
roleIds: []
}
],
status: '0',
remark: '',
postIds: [],
filePath: undefined
};
const initData = {
form: { ...initFormData },
rules: {
userName: [
{ required: true, message: '用户名称不能为空', trigger: 'blur' },
{
min: 2,
max: 20,
message: '用户名称长度必须介于 2 和 20 之间',
trigger: 'blur'
}
],
nickName: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }],
password: [
{ required: true, message: '用户密码不能为空', trigger: 'blur' },
{
min: 5,
max: 20,
message: '用户密码长度必须介于 5 和 20 之间',
trigger: 'blur'
},
{ pattern: /^[^<>"'|\\]+$/, message: '不能包含非法字符:< > " \' \\ |', trigger: 'blur' }
],
email: [
{
type: 'email',
message: '请输入正确的邮箱地址',
trigger: ['blur', 'change']
}
],
phonenumber: [
{ required: true, message: '请输入手机号码', trigger: 'blur' },
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: '请输入正确的手机号码',
trigger: 'blur'
}
]
}
};
const data = reactive(initData);
const { form, rules } = toRefs(data);
/** 查询部门下拉树结构 */
const getDeptTree = async () => {
const res = await api.deptTreeSelect({ isShow: '1' });
deptOptions.value = res.data;
enabledDeptOptions.value = filterDisabledDept(res.data);
const projectList = await listProject();
projectOptions.value = projectList.rows;
};
/** 过滤禁用的部门 */
const filterDisabledDept = (deptList) => {
return deptList.filter((dept) => {
if (dept.disabled) {
return false;
}
if (dept.children && dept.children.length) {
dept.children = filterDisabledDept(dept.children);
}
return true;
});
};
async function handleDeptChange(value: number | string) {
proxy?.$emit('setDeptId', value);
const response = await optionselect(value);
const roleList = await getRoleList(value);
roleOptions.value = roleList.data;
postOptions.value = response.data;
form.value.postIds = [];
form.value.projectRoles = [
{
projectId: '',
roleIds: []
}
];
}
const submitForm = () => {
userFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
form.value.userId ? await api.updateUser(form.value) : await api.addUser(form.value);
proxy?.$modal.msgSuccess('操作成功');
proxy?.$emit('submit', false);
}
});
};
const cancel = () => {
proxy?.$emit('close', false);
};
/** 重置操作表单 */
const reset = () => {
form.value = { ...initFormData };
form.value.projectRoles = [
{
projectId: '',
roleIds: []
}
];
userFormRef.value?.resetFields();
};
// 打开弹框调用参数
const open = async (row?: any) => {
reset();
if (row) {
// 编辑
const { data } = await api.getUser(row.userId);
Object.assign(form.value, data.user);
postOptions.value = data.posts;
roleOptions.value = data.roles;
form.value.postIds = data.postIds;
form.value.projectRoles = data.projectRoles;
form.value.password = '';
const roleList = await getRoleList(form.value.deptId);
roleOptions.value = roleList.data;
} else {
// 新增
const { data } = await api.getUser();
postOptions.value = data.posts;
}
};
const getInfoForm = () => {
return form.value;
};
onMounted(() => {
getDeptTree();
});
defineExpose({ open, getInfoForm });
</script>
<style lang="scss" scoped>
.editInfo {
display: flex;
flex-direction: column;
.box_submit {
align-self: flex-end;
display: flex;
gap: 10px;
}
}
</style>

View File

@ -0,0 +1,486 @@
<template>
<div class="roleInfo">
<div class="title_detail">
<span>当前选定角色信息预览</span>
<div style="margin-top: 10px">
<el-table :data="roleList" border height="150">
<el-table-column label="所属部门" align="center" prop="deptName" />
<el-table-column label="关联项目" align="center" prop="projectName" />
<el-table-column label="web端担任角色" align="center" prop="webRoles" />
<el-table-column label="APP端担任角色" align="center" prop="appRoles" />
</el-table>
</div>
</div>
<div class="title_detail" style="margin-top: 10px">
<span>选择或修改当前角色信息</span>
<div style="margin-top: 10px" class="box_detail">
<!-- 项目列表选择区 -->
<div class="project_list">
<span>关联项目模块</span>
<div class="project-items">
<div
v-for="item in projectOptions"
:key="item.id"
class="project-item"
:class="{ 'project-item-selected': isProjectSelected(item.id) }"
@click="toggleProjectSelection(item)"
>
<div class="project-item-content">
<el-checkbox v-model="item.checked" :value="item.id" class="project-checkbox" @change="handleProjectCheck(item, $event)" />
<span class="project-name">{{ item.projectName }}</span>
</div>
</div>
</div>
</div>
<!-- 角色分配区 -->
<div class="post_list">
<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">
<span class="project-title">{{ project.projectName }}</span>
<el-button type="text" class="remove-project" @click="removeProject(project.id)"> 移除 </el-button>
</div>
<div class="role-assignment">
<div class="role-group">
<label class="role-label">web端角色</label>
<el-checkbox-group v-model="project.webRoles" @change="updateRoleList">
<el-checkbox v-for="role in allRoles" :key="role.roleId" :value="role.roleId">
{{ role.roleName }}
</el-checkbox>
</el-checkbox-group>
</div>
<div class="role-group">
<label class="role-label">APP端角色</label>
<el-checkbox-group v-model="project.appRoles" @change="updateRoleList">
<el-checkbox v-for="role in AppRoles" :key="role.roleId" :value="role.roleId">
{{ role.roleName }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</div>
</div>
</div>
</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>
<script setup name="RoleProjectManagement" lang="ts">
import { ref, reactive, toRefs, getCurrentInstance, ComponentInternalInstance, defineExpose, watch } from 'vue';
import { ElFormInstance } from 'element-plus';
import api from '@/api/system/user';
import { listProject } from '@/api/project/project';
import { getRoleList } from '@/api/system/post';
// 类型定义
interface Project {
id: number | string;
projectName: string;
checked: boolean;
webRoles: string[];
appRoles: string[];
}
interface Role {
roleId: number | string;
roleName: string;
roleSort?: number;
}
interface RoleInfo {
userNick: string;
deptName: string;
postName: string;
projectName: string;
webRoles: string;
appRoles: string;
}
// 组件实例
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 响应式数据
const projectOptions = ref<Project[]>([]);
const selectedProjects = ref<Project[]>([]);
const allRoles = ref<Role[]>([]); // web端角色
const AppRoles = ref<Role[]>([]); // APP端角色
const roleList = ref<RoleInfo[]>([]);
// 表单初始数据
const initFormData = {
userId: undefined,
deptId: undefined,
userName: '',
nickName: undefined,
password: '',
phonenumber: undefined,
email: undefined,
sex: undefined,
projectRoles: [] as Array<{ projectId: number | string; webRoles: string[]; appRoles: string[] }>,
status: '0',
remark: '',
postIds: [],
filePath: undefined,
deptName: '' // 新增部门名称字段
};
const data = reactive({
form: { ...initFormData }
});
const deptName = ref('');
const { form } = toRefs(data);
// 核心方法:更新预览列表
const updateRoleList = () => {
if (selectedProjects.value.length === 0) {
roleList.value = [];
return;
}
roleList.value = selectedProjects.value.map((project) => {
// 处理web端角色名称
var webRoleNames = project.webRoles
.map((roleId) => {
const role = allRoles.value.find((r) => r.roleId === roleId);
return role ? role.roleName : '';
})
.filter(Boolean);
// 处理APP端角色名称
var appRoleNames = project.appRoles
.map((roleId) => {
const role = AppRoles.value.find((r) => r.roleId === roleId);
return role ? role.roleName : '';
})
.filter(Boolean);
webRoleNames = [...new Set(webRoleNames)];
appRoleNames = [...new Set(appRoleNames)];
return {
deptName: deptName.value,
projectName: project.projectName,
webRoles: webRoleNames.length > 0 ? webRoleNames.join('') : '无',
appRoles: appRoleNames.length > 0 ? appRoleNames.join('') : '无'
};
});
};
// 监听已选项目变化,自动更新预览列表
watch(
selectedProjects,
() => {
updateRoleList();
},
{ deep: true }
);
// 检查项目是否被选中
const isProjectSelected = (projectId: number | string) => {
return selectedProjects.value.some((p) => p.id === projectId);
};
// 切换项目选择状态
const toggleProjectSelection = (project: Project) => {
// handleProjectCheck(project, !project.checked);
};
// 处理项目勾选状态变化
const handleProjectCheck = (project: Project, checked: boolean) => {
// project.checked = checked;
const index = selectedProjects.value.findIndex((p) => p.id === project.id);
if (checked && index === -1) {
// 添加选中的项目
selectedProjects.value.push({
...project,
webRoles: [],
appRoles: []
});
} else if (!checked && index !== -1) {
// 移除取消选中的项目
selectedProjects.value.splice(index, 1);
}
};
// 移除项目
const removeProject = (projectId: number | string) => {
// 更新选中项目列表
selectedProjects.value = selectedProjects.value.filter((p) => p.id !== projectId);
// 更新项目选项的勾选状态
const project = projectOptions.value.find((p) => p.id === projectId);
if (project) {
project.checked = false;
}
};
// 提交表单
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)]
}));
// 提交数据
try {
if (form.value.userId) {
await api.updateUser(form.value);
} else {
await api.addUser(form.value);
}
proxy?.$modal.msgSuccess('操作成功');
proxy?.$emit('submit', form.value);
cancel();
} catch (error) {
proxy?.$modal.msgError('操作失败,请重试');
console.error('提交失败:', error);
}
};
// 取消操作
const cancel = () => {
resetForm();
proxy?.$emit('close');
};
// 重置表单
const resetForm = () => {
data.form = { ...initFormData };
projectOptions.value.forEach((p) => (p.checked = false));
selectedProjects.value = [];
roleList.value = [];
};
// 初始化数据
const initData = async () => {
try {
// 获取项目列表
const projectRes = await listProject();
projectOptions.value = projectRes.rows.map((item: any) => ({
id: item.id,
projectName: item.projectName,
checked: false
}));
} catch (error) {
proxy?.$modal.msgError('数据加载失败');
console.error('初始化数据失败:', error);
}
};
// 打开弹窗时调用
const open = async (row?: any, deptId?: any) => {
resetForm();
await initData();
deptName.value = row.deptName;
if (row) {
try {
if (!row.createTime) {
form.value = { ...row };
// 获取角色列表
if (form.value.deptId) {
deptId = form.value.deptId;
}
const roleRes = await getRoleList(deptId);
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);
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.roleSource == '1');
AppRoles.value = roleRes.data.filter((item: Role) => item.roleSource != '1');
// 加载项目角色数据
if (data.projectRoles && data.projectRoles.length) {
data.projectRoles.forEach((pr: any) => {
const project = projectOptions.value.find((p) => p.id === pr.projectId);
if (project) {
project.checked = true;
selectedProjects.value.push({
...project,
webRoles: pr.roleIds || [],
appRoles: pr.roleIds || []
});
}
});
}
}
} catch (error) {
proxy?.$modal.msgError('加载用户数据失败');
console.error('加载用户数据失败:', error);
}
}
};
// 暴露方法
defineExpose({ open });
</script>
<style lang="scss">
.roleInfo {
height: 100%;
padding-bottom: 60px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 12px;
.title_detail {
> span {
font-size: 16px;
font-weight: 600;
color: #333;
display: inline-block;
padding-bottom: 5px;
border-bottom: 2px solid #409eff;
}
.box_detail {
display: flex;
gap: 15px;
margin-top: 15px;
> div {
height: 350px;
padding: 15px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow-y: auto;
box-sizing: border-box;
}
.project_list {
width: 320px;
border: 1px solid #eee;
.project-items {
margin-top: 10px;
}
.project-item {
padding: 10px 12px;
margin-bottom: 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid #eee;
&:hover {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.project-item-content {
display: flex;
align-items: center;
}
.project-checkbox {
margin-right: 8px;
}
.project-name {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.project-item-selected {
background-color: #ecf5ff;
border-color: #c6e2ff;
}
}
.post_list {
flex: 1;
border: 1px solid #eee;
.list_title {
margin-bottom: 10px;
}
.no-selection {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-size: 14px;
}
.project-role-container {
padding: 15px;
margin-bottom: 15px;
// background-color: #f9f9f9;
border-radius: 4px;
border: 1px solid rgb(229 231 235);
.project-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
.project-title {
font-weight: 500;
color: #333;
font-size: 14px;
}
.remove-project {
color: #f56c6c;
padding: 0 5px;
&:hover {
color: #e4393c;
}
}
}
.role-assignment {
.role-group {
display: flex;
flex-direction: column;
margin-bottom: 10px;
.role-label {
display: inline-block;
width: 110px;
color: #606266;
font-size: 14px;
}
.el-checkbox-group {
display: inline-block;
margin-left: 10px;
.el-checkbox {
margin-right: 15px;
margin-bottom: 8px;
}
}
}
}
}
}
}
}
.box_submit {
align-self: flex-end;
display: flex;
gap: 10px;
}
}
</style>

View File

@ -0,0 +1,788 @@
<template>
<div class="p-2">
<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">
<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="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 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 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-has-permi="['system:user:query']">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="hover">
<template #header>
<el-row :gutter="10">
<el-col :span="1.5">
<el-button v-has-permi="['system:user:add']" type="primary" plain icon="Plus" @click="handleAdd()"> 新增 </el-button>
</el-col>
<el-col :span="1.5">
<el-button v-has-permi="['system:user:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">
修改
</el-button>
</el-col>
<el-col :span="1.5">
<el-button v-has-permi="['system:user:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">
删除
</el-button>
</el-col>
<el-col :span="1.5">
<el-dropdown class="mt-[1px]">
<el-button plain type="info">
更多
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item icon="Download" @click="importTemplate">下载模板</el-dropdown-item>
<el-dropdown-item v-has-permi="['system:user:import']" icon="Top" @click="handleImport">导入数据 </el-dropdown-item>
<el-dropdown-item v-has-permi="['system:user:export']" icon="Download" @click="handleExport"> 导出数据 </el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-col>
<right-toolbar v-model:show-search="showSearch" :columns="columns" :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="50" align="center" />
<el-table-column v-if="columns[0].visible" key="userId" label="用户编号" align="center" prop="userId" />
<el-table-column v-if="columns[1].visible" key="userName" label="用户名称" align="center" prop="userName" :show-overflow-tooltip="true" />
<el-table-column v-if="columns[2].visible" key="nickName" label="用户昵称" align="center" prop="nickName" :show-overflow-tooltip="true" />
<el-table-column v-if="columns[3].visible" key="deptName" label="部门" align="center" prop="deptName" :show-overflow-tooltip="true" />
<el-table-column v-if="columns[4].visible" key="phonenumber" label="手机号码" align="center" prop="phonenumber" width="120" />
<el-table-column v-if="columns[5].visible" key="status" label="状态" align="center">
<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 v-if="columns[6].visible" label="创建时间" align="center" prop="createTime" width="160">
<template #default="scope">
<span>{{ scope.row.createTime }}</span>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="230" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip v-if="scope.row.userId !== 1" content="修改" placement="top">
<el-button v-hasPermi="['system:user:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.userId !== 1" content="删除" placement="top">
<el-button v-hasPermi="['system:user:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.userId !== 1" content="重置密码" placement="top">
<el-button v-hasPermi="['system:user:resetPwd']" link type="primary" icon="Key" @click="handleResetPwd(scope.row)"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.userId !== 1" content="分配角色" placement="top">
<el-button v-hasPermi="['system:user:edit']" link type="primary" icon="CircleCheck" @click="handleAuthRole(scope.row)"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.userId !== 1" content="编辑关联项目" placement="top">
<el-button v-hasPermi="['system:user:edit']" link type="primary" icon="Edit" @click="handleUpdateProject(scope.row)"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.userId !== 1" content="上传证书目录" placement="top">
<el-button v-hasPermi="['system:user:edit']" link type="primary" icon="Upload" @click="handleUploadCert(scope.row)"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="shuttleVisible" title="编辑关联项目" width="auto" destroy-on-close>
<shuttle-frame :userId="selectedUserId" @close="shuttleVisible = false" />
</el-dialog>
<pagination
v-show="total > 0"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
</el-card>
</el-col>
</el-row>
<!-- 添加或修改用户配置对话框 -->
<el-dialog draggable ref="formDialogRef" v-model="dialog.visible" :title="dialog.title" width="600px" append-to-body @close="closeDialog">
<el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px">
<el-row>
<el-col :span="12">
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="30" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="归属部门" prop="deptId">
<el-tree-select
v-model="form.deptId"
:data="enabledDeptOptions"
:props="{ value: 'id', label: 'label', children: 'children' }"
value-key="id"
placeholder="请选择归属部门"
check-strictly
@change="handleDeptChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.userId == undefined" label="用户名称" prop="userName">
<el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.userId == undefined" label="用户密码" prop="password">
<el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" show-password />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="用户性别">
<el-select v-model="form.sex" placeholder="请选择">
<el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="岗位">
<el-select v-model="form.postIds" multiple placeholder="请选择">
<el-option
v-for="item in postOptions"
:key="item.postId"
:label="item.postName"
:value="item.postId"
:disabled="item.status == '1'"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-row :gutter="20" v-for="(item, index) in form.projectRoles">
<el-col :span="11" :offset="0">
<el-form-item label="项目列表">
<el-select v-model="item.projectId" placeholder="请选择">
<el-option v-for="dict in projectOptions" :key="dict.id" :label="dict.shortName" :value="dict.id"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="11" :offset="0">
<el-form-item label="角色">
<el-select v-model="item.roleIds" filterable multiple placeholder="请选择">
<el-option
v-for="item in roleOptions"
:key="item.roleId"
:label="item.roleName"
:value="item.roleId"
:disabled="item.status == '1'"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="1" :offset="0">
<el-button type="primary" circle icon="Plus" @click="handleAddProject" v-if="index == 0"></el-button>
<el-button type="danger" circle icon="Delete" @click="delProject(index)" v-else></el-button>
</el-col>
</el-row>
</el-col>
<el-col :span="12">
<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-col>
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
</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>
<!-- 用户导入对话框 -->
<el-dialog draggable v-model="upload.open" :title="upload.title" width="400px" append-to-body>
<el-upload
ref="uploadRef"
:limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="upload.url + '?updateSupport=' + upload.updateSupport"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:auto-upload="false"
drag
>
<el-icon class="el-icon--upload">
<i-ep-upload-filled />
</el-icon>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="text-center el-upload__tip">
<div class="el-upload__tip">
<el-checkbox v-model="upload.updateSupport" />
是否更新已经存在的用户数据
</div>
<span>仅允许导入xlsxlsx格式文件</span>
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板 </el-link>
</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitFileForm"> </el-button>
<el-button @click="upload.open = false"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog title="上传证书目录" v-model="certDialog" width="30%" destroy-on-close>
<!-- <File-upload v-model="fileUpload" :limit="5"></File-upload> -->
<ImageUpload v-model="fileUpload" :limit="5"></ImageUpload>
<template #footer>
<span>
<el-button @click="certDialog = false">取消</el-button>
<el-button type="primary" @click="uploadCert">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup name="User" lang="ts">
import api, { uploadCertList } from '@/api/system/user';
import { UserForm, UserQuery, UserVO } from '@/api/system/user/types';
import { DeptTreeVO, DeptVO } from '@/api/system/dept/types';
import { RoleVO } from '@/api/system/role/types';
import { PostVO } from '@/api/system/post/types';
import { globalHeaders } from '@/utils/request';
import { to } from 'await-to-js';
import { getProjectByDeptId, getRoleList, optionselect } from '@/api/system/post';
import ShuttleFrame from '../../project/projectRelevancy/component/ShuttleFrame.vue';
import { listProject } from '@/api/project/project';
import editInfo from './comm/editInfo.vue';
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'));
const userList = ref<UserVO[]>();
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<number | string>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
const deptName = ref('');
const deptOptions = ref<DeptTreeVO[]>([]);
const enabledDeptOptions = ref<DeptTreeVO[]>([]);
const initPassword = ref<string>('');
const postOptions = ref<PostVO[]>([]);
const roleOptions = ref<RoleVO[]>([]);
const projectOptions = ref<any[]>([]);
/*** 用户导入参数 */
const upload = reactive<ImportOption>({
// 是否显示弹出层(用户导入)
open: false,
// 弹出层标题(用户导入)
title: '',
// 是否禁用上传
isUploading: false,
// 是否更新已经存在的用户数据
updateSupport: 0,
// 设置上传的请求头部
headers: globalHeaders(),
// 上传的地址
url: import.meta.env.VITE_APP_BASE_API + '/system/user/importData'
});
// 列显隐信息
const columns = ref<FieldOption[]>([
{ key: 0, label: `用户编号`, visible: false, children: [] },
{ key: 1, label: `用户名称`, visible: true, children: [] },
{ key: 2, label: `用户昵称`, visible: true, children: [] },
{ key: 3, label: `部门`, visible: true, children: [] },
{ key: 4, label: `手机号码`, visible: true, children: [] },
{ key: 5, label: `状态`, visible: true, children: [] },
{ key: 6, label: `创建时间`, visible: true, children: [] }
]);
const deptTreeRef = ref<ElTreeInstance>();
const queryFormRef = ref<ElFormInstance>();
const userFormRef = ref<ElFormInstance>();
const uploadRef = ref<ElUploadInstance>();
const formDialogRef = ref<ElDialogInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: UserForm = {
userId: undefined,
deptId: undefined,
userName: '',
nickName: undefined,
password: '',
phonenumber: undefined,
email: undefined,
sex: undefined,
projectRoles: [
{
projectId: '',
roleIds: []
}
],
status: '0',
remark: '',
postIds: [],
filePath: undefined
};
const initData: PageData<UserForm, UserQuery> = {
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
userName: '',
phonenumber: '',
status: '',
deptId: '',
roleId: ''
},
rules: {
userName: [
{ required: true, message: '用户名称不能为空', trigger: 'blur' },
{
min: 2,
max: 20,
message: '用户名称长度必须介于 2 和 20 之间',
trigger: 'blur'
}
],
nickName: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }],
password: [
{ required: true, message: '用户密码不能为空', trigger: 'blur' },
{
min: 5,
max: 20,
message: '用户密码长度必须介于 5 和 20 之间',
trigger: 'blur'
},
{ pattern: /^[^<>"'|\\]+$/, message: '不能包含非法字符:< > " \' \\ |', trigger: 'blur' }
],
email: [
{
type: 'email',
message: '请输入正确的邮箱地址',
trigger: ['blur', 'change']
}
],
phonenumber: [
{ required: true, message: '请输入手机号码', trigger: 'blur' },
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: '请输入正确的手机号码',
trigger: 'blur'
}
]
}
};
const data = reactive<PageData<UserForm, UserQuery>>(initData);
const { queryParams, form, rules } = toRefs<PageData<UserForm, UserQuery>>(data);
/** 通过条件过滤节点 */
const filterNode = (value: string, data: any) => {
if (!value) return true;
return data.label.indexOf(value) !== -1;
};
/** 根据名称筛选部门树 */
watchEffect(
() => {
deptTreeRef.value?.filter(deptName.value);
},
{
flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发此属性控制在DOM元素更新后运行
}
);
/** 查询用户列表 */
const getList = async () => {
loading.value = true;
const res = await api.listUser(proxy?.addDateRange(queryParams.value, dateRange.value));
loading.value = false;
userList.value = res.rows;
total.value = res.total;
};
/** 查询部门下拉树结构 */
const getDeptTree = async () => {
const res = await api.deptTreeSelect({ isShow: '1' });
deptOptions.value = res.data;
enabledDeptOptions.value = filterDisabledDept(res.data);
const projectList = await listProject();
projectOptions.value = projectList.rows;
};
/** 过滤禁用的部门 */
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;
});
};
/** 节点单击事件 */
const handleNodeClick = (data: DeptVO) => {
queryParams.value.deptId = data.id;
handleQuery();
};
/** 部门选择变化 */
const handleAddProject = () => {
form.value.projectRoles.push({
projectId: '',
roleIds: []
});
};
/** 删除项目 */
const delProject = (index: number) => {
form.value.projectRoles.splice(index, 1);
};
/** 搜索按钮操作 */
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?: UserVO) => {
const userIds = row?.userId || ids.value;
const [err] = await to(proxy?.$modal.confirm('是否确认删除用户编号为"' + userIds + '"的数据项?') as any);
if (!err) {
await api.delUser(userIds);
await getList();
proxy?.$modal.msgSuccess('删除成功');
}
};
/** 用户状态修改 */
const handleStatusChange = async (row: UserVO) => {
let text = row.status === '0' ? '启用' : '停用';
try {
await proxy?.$modal.confirm('确认要"' + text + '""' + row.userName + '"用户吗?');
await api.changeUserStatus(row.userId, row.status);
proxy?.$modal.msgSuccess(text + '成功');
} catch (err) {
row.status = row.status === '0' ? '1' : '0';
}
};
/** 跳转角色分配 */
const handleAuthRole = (row: UserVO) => {
const userId = row.userId;
router.push('/system/user-auth/role/' + userId);
};
/** 重置密码按钮操作 */
const handleResetPwd = async (row: UserVO) => {
const [err, res] = await to(
ElMessageBox.prompt('请输入"' + row.userName + '"的新密码', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
closeOnClickModal: false,
inputPattern: /^.{5,20}$/,
inputErrorMessage: '用户密码长度必须介于 5 和 20 之间',
inputValidator: (value) => {
if (/<|>|"|'|\||\\/.test(value)) {
return '不能包含非法字符:< > " \' \\ |';
}
}
})
);
if (!err && res) {
await api.resetUserPwd(row.userId, res.value);
proxy?.$modal.msgSuccess('修改成功,新密码是:' + res.value);
}
};
/** 选择条数 */
const handleSelectionChange = (selection: UserVO[]) => {
ids.value = selection.map((item) => item.userId);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 导入按钮操作 */
const handleImport = () => {
upload.title = '用户导入';
upload.open = true;
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'system/user/export',
{
...queryParams.value
},
`user_${new Date().getTime()}.xlsx`
);
};
/** 下载模板操作 */
const importTemplate = () => {
proxy?.download('system/user/importTemplate', {}, `user_template_${new Date().getTime()}.xlsx`);
};
/**文件上传中处理 */
const handleFileUploadProgress = () => {
upload.isUploading = true;
};
/** 文件上传成功处理 */
const handleFileSuccess = (response: any, file: UploadFile) => {
upload.open = false;
upload.isUploading = false;
uploadRef.value?.handleRemove(file);
ElMessageBox.alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + '</div>', '导入结果', {
dangerouslyUseHTMLString: true
});
getList();
};
/** 提交上传文件 */
function submitFileForm() {
uploadRef.value?.submit();
}
/** 重置操作表单 */
const reset = () => {
form.value = { ...initFormData };
form.value.projectRoles = [
{
projectId: '',
roleIds: []
}
];
userFormRef.value?.resetFields();
};
/** 取消按钮 */
const cancel = () => {
dialog.visible = false;
reset();
};
/** 新增按钮操作 */
const handleAdd = async () => {
reset();
const { data } = await api.getUser();
dialog.visible = true;
dialog.title = '新增用户';
postOptions.value = data.posts;
form.value.password = initPassword.value.toString();
};
/** 修改按钮操作 */
const handleUpdate = async (row?: UserForm) => {
reset();
const userId = row?.userId || ids.value[0];
const { data } = await api.getUser(userId);
dialog.visible = true;
dialog.title = '修改用户';
Object.assign(form.value, data.user);
postOptions.value = data.posts;
roleOptions.value = data.roles;
form.value.postIds = data.postIds;
form.value.projectRoles = data.projectRoles;
form.value.password = '';
const roleList = await getRoleList(form.value.deptId);
roleOptions.value = roleList.data;
};
const validate = () => {
for (let i = 0; i < form.value.projectRoles.length; i++) {
const item = form.value.projectRoles[i];
if (!item.projectId || item.projectId.length === 0) {
proxy?.$modal.msgError(`${i + 1} 行“项目列表”未填写`);
return false; // 阻止提交
}
if (!item.roleIds || item.roleIds.length === 0) {
proxy?.$modal.msgError(`${i + 1} 行“角色”未填写`);
return false; // 阻止提交
}
}
return true;
};
/** 提交按钮 */
const submitForm = () => {
const isValid = validate();
if (!isValid) return;
userFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
form.value.userId ? await api.updateUser(form.value) : await api.addUser(form.value);
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
}
});
};
/**
* 关闭用户弹窗
*/
const closeDialog = () => {
dialog.visible = false;
resetForm();
};
/**
* 重置表单
*/
const resetForm = () => {
userFormRef.value?.resetFields();
userFormRef.value?.clearValidate();
form.value.id = undefined;
form.value.status = '1';
};
onMounted(() => {
getDeptTree(); // 初始化部门数据
getList(); // 初始化列表数据
proxy?.getConfigKey('sys.user.initPassword').then((response) => {
initPassword.value = response.data;
});
});
async function handleDeptChange(value: number | string) {
const response = await optionselect(value);
const roleList = await getRoleList(value);
roleOptions.value = roleList.data;
postOptions.value = response.data;
form.value.postIds = [];
form.value.projectRoles = [
{
projectId: [],
roleIds: []
}
];
}
const shuttleVisible = ref(false);
const selectedUserId = ref<number>();
const handleUpdateProject = (row) => {
if (row) {
selectedUserId.value = row.userId;
shuttleVisible.value = true;
}
};
const certDialog = ref(false);
const certId = ref<string | number>(undefined);
const fileUpload = ref<string>();
//上传证书
const handleUploadCert = (row: UserVO) => {
certId.value = row.userId;
fileUpload.value = row.filePath;
certDialog.value = true;
};
const uploadCert = async () => {
if (!fileUpload.value) {
proxy?.$modal.msgError('请上传证书目录');
return;
}
let res = await uploadCertList({
userId: certId.value,
fileId: fileUpload.value
});
console.log(res);
certDialog.value = false;
proxy?.$modal.msgSuccess('上传证书目录成功');
};
</script>
<style lang="scss" scoped></style>

View File

@ -5,14 +5,22 @@
<el-col :lg="4" :xs="24" style=""> <el-col :lg="4" :xs="24" style="">
<el-card shadow="hover"> <el-card shadow="hover">
<el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable /> <el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable />
<el-tree ref="deptTreeRef" class="mt-2" node-key="id" :data="deptOptions" <el-tree
:props="{ label: 'label', children: 'children' }" :expand-on-click-node="false" ref="deptTreeRef"
:filter-node-method="filterNode" highlight-current default-expand-all @node-click="handleNodeClick" /> 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-card>
</el-col> </el-col>
<el-col :lg="20" :xs="24"> <el-col :lg="20" :xs="24">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
:leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]"> <div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover"> <el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true"> <el-form ref="queryFormRef" :model="queryParams" :inline="true">
@ -20,24 +28,27 @@
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" /> <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="手机号码" prop="phonenumber"> <el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable <el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable @keyup.enter="handleQuery" />
@keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="用户状态" clearable> <el-select v-model="queryParams.status" placeholder="用户状态" clearable>
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
:value="dict.value" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="创建时间" style="width: 308px"> <el-form-item label="创建时间" style="width: 308px">
<el-date-picker v-model="dateRange" value-format="YYYY-MM-DD HH:mm:ss" type="daterange" <el-date-picker
range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" v-model="dateRange"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"></el-date-picker> 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-form-item> <el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery" <el-button type="primary" icon="Search" @click="handleQuery" @v-has-permi="['system:user:query']">搜索</el-button>
@v-has-permi="['system:user:query']">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button> <el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item> </el-form-item>
@ -50,18 +61,15 @@
<template #header> <template #header>
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="1.5"> <el-col :span="1.5">
<el-button v-has-permi="['system:user:add']" type="primary" plain icon="Plus" @click="handleAdd()"> 新增 <el-button v-has-permi="['system:user:add']" type="primary" plain icon="Plus" @click="handleAdd()"> 新增 </el-button>
</el-button>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button v-has-permi="['system:user:edit']" type="success" plain :disabled="single" icon="Edit" <el-button v-has-permi="['system:user:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">
@click="handleUpdate()">
修改 修改
</el-button> </el-button>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button v-has-permi="['system:user:remove']" type="danger" plain :disabled="multiple" icon="Delete" <el-button v-has-permi="['system:user:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">
@click="handleDelete()">
删除 删除
</el-button> </el-button>
</el-col> </el-col>
@ -76,34 +84,26 @@
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item icon="Download" @click="importTemplate">下载模板</el-dropdown-item> <el-dropdown-item icon="Download" @click="importTemplate">下载模板</el-dropdown-item>
<el-dropdown-item v-has-permi="['system:user:import']" icon="Top" @click="handleImport">导入数据 <el-dropdown-item v-has-permi="['system:user:import']" icon="Top" @click="handleImport">导入数据 </el-dropdown-item>
</el-dropdown-item> <el-dropdown-item v-has-permi="['system:user:export']" icon="Download" @click="handleExport"> 导出数据 </el-dropdown-item>
<el-dropdown-item v-has-permi="['system:user:export']" icon="Download" @click="handleExport"> 导出数据
</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
</el-col> </el-col>
<right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" <right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
@query-table="getList"></right-toolbar>
</el-row> </el-row>
</template> </template>
<el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange"> <el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" /> <el-table-column type="selection" width="50" align="center" />
<el-table-column v-if="columns[0].visible" key="userId" label="用户编号" align="center" prop="userId" /> <el-table-column v-if="columns[0].visible" key="userId" label="用户编号" align="center" prop="userId" />
<el-table-column v-if="columns[1].visible" key="userName" label="用户名称" align="center" prop="userName" <el-table-column v-if="columns[1].visible" key="userName" label="用户名称" align="center" prop="userName" :show-overflow-tooltip="true" />
:show-overflow-tooltip="true" /> <el-table-column v-if="columns[2].visible" key="nickName" label="用户昵称" align="center" prop="nickName" :show-overflow-tooltip="true" />
<el-table-column v-if="columns[2].visible" key="nickName" label="用户昵称" align="center" prop="nickName" <el-table-column v-if="columns[3].visible" key="deptName" label="部门" align="center" prop="deptName" :show-overflow-tooltip="true" />
:show-overflow-tooltip="true" /> <el-table-column v-if="columns[4].visible" key="phonenumber" label="手机号码" align="center" prop="phonenumber" width="120" />
<el-table-column v-if="columns[3].visible" key="deptName" label="部门" align="center" prop="deptName"
:show-overflow-tooltip="true" />
<el-table-column v-if="columns[4].visible" key="phonenumber" label="手机号码" align="center" prop="phonenumber"
width="120" />
<el-table-column v-if="columns[5].visible" key="status" label="状态" align="center"> <el-table-column v-if="columns[5].visible" key="status" label="状态" align="center">
<template #default="scope"> <template #default="scope">
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
@change="handleStatusChange(scope.row)"></el-switch>
</template> </template>
</el-table-column> </el-table-column>
@ -116,30 +116,24 @@
<el-table-column label="操作" fixed="right" width="230" class-name="small-padding fixed-width"> <el-table-column label="操作" fixed="right" width="230" class-name="small-padding fixed-width">
<template #default="scope"> <template #default="scope">
<el-tooltip v-if="scope.row.userId !== 1" content="修改" placement="top"> <el-tooltip v-if="scope.row.userId !== 1" content="修改" placement="top">
<el-button v-hasPermi="['system:user:edit']" link type="primary" icon="Edit" <el-button v-hasPermi="['system:user:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
@click="handleUpdate(scope.row)"></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip v-if="scope.row.userId !== 1" content="删除" placement="top"> <el-tooltip v-if="scope.row.userId !== 1" content="删除" placement="top">
<el-button v-hasPermi="['system:user:remove']" link type="primary" icon="Delete" <el-button v-hasPermi="['system:user:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
@click="handleDelete(scope.row)"></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip v-if="scope.row.userId !== 1" content="重置密码" placement="top"> <el-tooltip v-if="scope.row.userId !== 1" content="重置密码" placement="top">
<el-button v-hasPermi="['system:user:resetPwd']" link type="primary" icon="Key" <el-button v-hasPermi="['system:user:resetPwd']" link type="primary" icon="Key" @click="handleResetPwd(scope.row)"></el-button>
@click="handleResetPwd(scope.row)"></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip v-if="scope.row.userId !== 1" content="分配角色" placement="top"> <el-tooltip v-if="scope.row.userId !== 1" content="分配角色" placement="top">
<el-button v-hasPermi="['system:user:edit']" link type="primary" icon="CircleCheck" <el-button v-hasPermi="['system:user:edit']" link type="primary" icon="CircleCheck" @click="handleAuthRole(scope.row)"></el-button>
@click="handleAuthRole(scope.row)"></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip v-if="scope.row.userId !== 1" content="编辑关联项目" placement="top"> <el-tooltip v-if="scope.row.userId !== 1" content="编辑关联项目" placement="top">
<el-button v-hasPermi="['system:user:edit']" link type="primary" icon="Edit" <el-button v-hasPermi="['system:user:edit']" link type="primary" icon="Edit" @click="handleUpdateProject(scope.row)"></el-button>
@click="handleUpdateProject(scope.row)"></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip v-if="scope.row.userId !== 1" content="上传证书目录" placement="top"> <el-tooltip v-if="scope.row.userId !== 1" content="上传证书目录" placement="top">
<el-button v-hasPermi="['system:user:edit']" link type="primary" icon="Upload" <el-button v-hasPermi="['system:user:edit']" link type="primary" icon="Upload" @click="handleUploadCert(scope.row)"></el-button>
@click="handleUploadCert(scope.row)"></el-button>
</el-tooltip> </el-tooltip>
</template> </template>
</el-table-column> </el-table-column>
@ -148,117 +142,53 @@
<shuttle-frame :userId="selectedUserId" @close="shuttleVisible = false" /> <shuttle-frame :userId="selectedUserId" @close="shuttleVisible = false" />
</el-dialog> </el-dialog>
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" <pagination
:total="total" @pagination="getList" /> v-show="total > 0"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
<!-- 添加或修改用户配置对话框 --> <!-- 添加或修改用户配置对话框 -->
<el-dialog ref="formDialogRef" v-model="dialog.visible" :title="dialog.title" width="600px" append-to-body <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">
@close="closeDialog"> <div class="boxDetial">
<el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px"> <div class="tab_info">
<el-row> <div class="tab_item" @click="onTab(1)" :class="{ active: type == 1 }">
<el-col :span="12"> <Avatar style="width: 1em; height: 1em; margin-right: 8px" :style="{color: type == 1 ? '#1890ff' : '#000'}" />
<el-form-item label="用户昵称" prop="nickName"> <span>基本资料</span>
<el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="30" /> </div>
</el-form-item> <div class="tab_item" @click="onTab(2)" :class="{ active: type == 2 }">
</el-col> <Key style="width: 1em; height: 1em; margin-right: 8px" :style="{color: type == 2 ? '#1890ff' : '#000'}" />
<el-col :span="12"> <span>角色信息</span>
<el-form-item label="归属部门" prop="deptId"> </div>
<el-tree-select v-model="form.deptId" :data="enabledDeptOptions" </div>
:props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" placeholder="请选择归属部门" <div class="tab_content" v-show="type == 1">
check-strictly @change="handleDeptChange" /> <editInfo ref="editInfoRef" @close="dialog.visible = false" @submit="getList" @setDeptId="setDeptId"></editInfo>
</el-form-item> </div>
</el-col> <div class="tab_content" v-show="type == 2">
<el-col :span="12"> <roleInfo ref="roleInfoRef" @close="dialog.visible = false" @submit="getList"></roleInfo>
<el-form-item label="手机号码" prop="phonenumber"> </div>
<el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.userId == undefined" label="用户名称" prop="userName">
<el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.userId == undefined" label="用户密码" prop="password">
<el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" show-password />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="用户性别">
<el-select v-model="form.sex" placeholder="请选择">
<el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label"
:value="dict.value"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="岗位">
<el-select v-model="form.postIds" multiple placeholder="请选择">
<el-option v-for="item in postOptions" :key="item.postId" :label="item.postName" :value="item.postId"
:disabled="item.status == '1'"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-row :gutter="20" v-for="(item, index) in form.projectRoles">
<el-col :span="11" :offset="0">
<el-form-item label="项目列表">
<el-select v-model="item.projectId" placeholder="请选择">
<el-option v-for="dict in projectOptions" :key="dict.id" :label="dict.shortName"
:value="dict.id"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="11" :offset="0">
<el-form-item label="角色">
<el-select v-model="item.roleIds" filterable multiple placeholder="请选择">
<el-option v-for="item in roleOptions" :key="item.roleId" :label="item.roleName"
:value="item.roleId" :disabled="item.status == '1'"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="1" :offset="0">
<el-button type="primary" circle icon="Plus" @click="handleAddProject" v-if="index == 0"></el-button>
<el-button type="danger" circle icon="Delete" @click="delProject(index)" v-else></el-button>
</el-col>
</el-row>
</el-col>
<el-col :span="12">
<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-col>
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
</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> </div>
</template>
</el-dialog> </el-dialog>
<!-- 用户导入对话框 --> <!-- 用户导入对话框 -->
<el-dialog draggable v-model="upload.open" :title="upload.title" width="400px" append-to-body> <el-dialog draggable v-model="upload.open" :title="upload.title" width="400px" append-to-body>
<el-upload ref="uploadRef" :limit="1" accept=".xlsx, .xls" :headers="upload.headers" <el-upload
:action="upload.url + '?updateSupport=' + upload.updateSupport" :disabled="upload.isUploading" ref="uploadRef"
:on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag> :limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="upload.url + '?updateSupport=' + upload.updateSupport"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:auto-upload="false"
drag
>
<el-icon class="el-icon--upload"> <el-icon class="el-icon--upload">
<i-ep-upload-filled /> <i-ep-upload-filled />
</el-icon> </el-icon>
@ -270,8 +200,7 @@
是否更新已经存在的用户数据 是否更新已经存在的用户数据
</div> </div>
<span>仅允许导入xlsxlsx格式文件</span> <span>仅允许导入xlsxlsx格式文件</span>
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板 </el-link>
@click="importTemplate">下载模板 </el-link>
</div> </div>
</template> </template>
</el-upload> </el-upload>
@ -307,7 +236,9 @@ import { to } from 'await-to-js';
import { getProjectByDeptId, getRoleList, optionselect } from '@/api/system/post'; import { getProjectByDeptId, getRoleList, optionselect } from '@/api/system/post';
import ShuttleFrame from '../../project/projectRelevancy/component/ShuttleFrame.vue'; import ShuttleFrame from '../../project/projectRelevancy/component/ShuttleFrame.vue';
import { listProject } from '@/api/project/project'; 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 router = useRouter();
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable, sys_user_sex } = toRefs<any>(proxy?.useDict('sys_normal_disable', 'sys_user_sex')); const { sys_normal_disable, sys_user_sex } = toRefs<any>(proxy?.useDict('sys_normal_disable', 'sys_user_sex'));
@ -326,7 +257,8 @@ const initPassword = ref<string>('');
const postOptions = ref<PostVO[]>([]); const postOptions = ref<PostVO[]>([]);
const roleOptions = ref<RoleVO[]>([]); const roleOptions = ref<RoleVO[]>([]);
const projectOptions = ref<any[]>([]); const projectOptions = ref<any[]>([]);
const editInfoRef = ref<InstanceType<typeof editInfo> | null>(null);
const roleInfoRef = ref<InstanceType<typeof roleInfo> | null>(null);
/*** 用户导入参数 */ /*** 用户导入参数 */
const upload = reactive<ImportOption>({ const upload = reactive<ImportOption>({
// 是否显示弹出层(用户导入) // 是否显示弹出层(用户导入)
@ -358,7 +290,8 @@ const queryFormRef = ref<ElFormInstance>();
const userFormRef = ref<ElFormInstance>(); const userFormRef = ref<ElFormInstance>();
const uploadRef = ref<ElUploadInstance>(); const uploadRef = ref<ElUploadInstance>();
const formDialogRef = ref<ElDialogInstance>(); const formDialogRef = ref<ElDialogInstance>();
const deptIdRole = ref<number>();
const type = ref(1);
const dialog = reactive<DialogOption>({ const dialog = reactive<DialogOption>({
visible: false, visible: false,
title: '' title: ''
@ -452,7 +385,9 @@ watchEffect(
flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发此属性控制在DOM元素更新后运行 flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发此属性控制在DOM元素更新后运行
} }
); );
const setDeptId = (deptId: number) => {
deptIdRole.value = deptId;
};
/** 查询用户列表 */ /** 查询用户列表 */
const getList = async () => { const getList = async () => {
loading.value = true; loading.value = true;
@ -637,8 +572,12 @@ const cancel = () => {
const handleAdd = async () => { const handleAdd = async () => {
reset(); reset();
const { data } = await api.getUser(); const { data } = await api.getUser();
type.value = 1;
dialog.visible = true; dialog.visible = true;
dialog.title = '新增用户'; dialog.title = '新增用户';
nextTick(() => {
editInfoRef.value?.open();
});
postOptions.value = data.posts; postOptions.value = data.posts;
form.value.password = initPassword.value.toString(); form.value.password = initPassword.value.toString();
}; };
@ -646,19 +585,13 @@ const handleAdd = async () => {
/** 修改按钮操作 */ /** 修改按钮操作 */
const handleUpdate = async (row?: UserForm) => { const handleUpdate = async (row?: UserForm) => {
reset(); reset();
const userId = row?.userId || ids.value[0];
const { data } = await api.getUser(userId);
dialog.visible = true; dialog.visible = true;
dialog.title = '修改用户'; dialog.title = '修改用户';
Object.assign(form.value, data.user); type.value = 1;
postOptions.value = data.posts; form.value = row;
roleOptions.value = data.roles; nextTick(() => {
form.value.postIds = data.postIds; editInfoRef.value?.open(row);
form.value.projectRoles = data.projectRoles; });
form.value.password = '';
const roleList = await getRoleList(form.value.deptId);
roleOptions.value = roleList.data;
}; };
const validate = () => { const validate = () => {
@ -764,6 +697,58 @@ const uploadCert = async () => {
certDialog.value = false; certDialog.value = false;
proxy?.$modal.msgSuccess('上传证书目录成功'); proxy?.$modal.msgSuccess('上传证书目录成功');
}; };
const onTab = (val: number) => {
type.value = val;
if (val == 2) {
let obj = editInfoRef.value?.getInfoForm();
form.value = obj;
nextTick(() => {
roleInfoRef.value?.open(form.value, deptIdRole.value);
});
}
};
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped>
.boxDetial {
display: flex;
justify-content: space-between;
align-items: start;
height: 680px;
gap: 20px;
.tab_info {
height: 40%;
width: 200px;
background: #fff;
padding: 30px 10px;
text-align: center;
display: flex;
flex-direction: column;
border-radius: 0.5rem;
font-size: 18px;
.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;
}
}
.tab_content {
height: 100%;
width: calc(100% - 200px);
padding: 20px 10px;
background: #fff;
border-radius: 0.5rem;
}
}
</style>

View File

@ -10,7 +10,7 @@
<el-option v-for="item in options" :key="item.versions" :label="item.versions" :value="item.versions" /> <el-option v-for="item in options" :key="item.versions" :label="item.versions" :value="item.versions" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="表名" prop="sheet"> <el-form-item label="表名" prop="sheet" v-if="activeTab != '3'">
<el-select v-model="queryForm.sheet" placeholder="选择表名" @change="changeSheet"> <el-select v-model="queryForm.sheet" placeholder="选择表名" @change="changeSheet">
<el-option v-for="item in sheets" :key="item" :label="item" :value="item" /> <el-option v-for="item in sheets" :key="item" :label="item" :value="item" />
</el-select> </el-select>
@ -58,11 +58,13 @@
</el-card> </el-card>
</transition> </transition>
<el-card shadow="never" class="mb8"> <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 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="num" label="编号" />
<el-table-column prop="name" label="工程或费用名称" /> <el-table-column prop="name" label="工程或费用名称" />
<el-table-column prop="unit" label="单位" /> <el-table-column prop="unit" label="单位" align="center" />
<el-table-column prop="quantity" label="数量">
<el-table-column prop="specification" label="规格" align="center" />
<el-table-column prop="quantity" label="数量" align="center">
<template #default="scope"> <template #default="scope">
{{ scope.row.children.length > 0 ? '' : scope.row.quantity }} {{ scope.row.children.length > 0 ? '' : scope.row.quantity }}
</template> </template>
@ -90,6 +92,11 @@
{{ scope.row.price != 0 ? Number(scope.row.price).toFixed(2) : null }} {{ scope.row.price != 0 ? Number(scope.row.price).toFixed(2) : null }}
</template> </template>
</el-table-column> </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"> <el-table-column prop="operate" label="操作" align="center">
<template #default="scope"> <template #default="scope">
<el-button <el-button
@ -111,64 +118,141 @@
</template> </template>
<script setup lang="ts"> <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 { useUserStoreHook } from '@/store/modules/user';
import { obtainAllVersionNumbers, sheetList, getTableList, updatePrice, importExcelFile } from '@/api/tender/index'; 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 { proxy } = getCurrentInstance() as ComponentInternalInstance;
const userStore = useUserStoreHook(); const userStore = useUserStoreHook();
const currentProject = computed(() => userStore.selectedProject); const currentProject = computed(() => userStore.selectedProject);
// 标签页配置
const tabList = [ const tabList = [
{ { label: '招采工程量清单', value: '2' },
label: '招采工程量清单', { label: '物资设备清单', value: '3' }
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 activeTab = ref('2');
const sheets = ref([]); const sheets = ref<string[]>([]);
const options = ref([]); const options = ref<VersionItem[]>([]);
const tableData = ref([]); const tableData = ref<TableRow[]>([]);
const tableRef = ref(); const tableAllRef = ref<any>(null);
const isExpandAll = ref(false); const uploadRef = ref<any>(null);
const isExpandAll = ref(true);
const loading = ref(false); 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) => { const handleTabChange = (tab: string) => {
activeTab.value = tab; activeTab.value = tab;
tableData.value = []; tableData.value = [];
versionsData.value = {}; versionsData.value = {};
isExpandAll.value = true;
expandRowKeys.value = [];
getVersionNums(); 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(); getSheetName();
}; };
//切换表格
const changeSheet = (val: any) => { // 切换表名
const changeSheet = (val: string) => {
getTableData(); getTableData();
}; };
//展开树
const toggleExpandAll = () => { // 一键展开/收起
const toggleExpandAll = async () => {
isExpandAll.value = !isExpandAll.value; isExpandAll.value = !isExpandAll.value;
console.log(isExpandAll.value); if (!tableData.value.length || !tableAllRef.value) return;
tableData.value.forEach((row) => {
tableRef.value.toggleRowExpansion(row, isExpandAll.value); if (isExpandAll.value) {
// 收集所有已加载节点的ID
const allExpandIds = collectAllNodeIds(tableData.value);
expandRowKeys.value = allExpandIds;
// 处理懒加载节点
await loadAndExpandAllLazyNodes();
} else {
// 全部收起
expandRowKeys.value = [];
}
};
// 辅助函数递归收集所有节点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 () => { const getVersionNums = async () => {
try { try {
const params = { const params = {
@ -179,93 +263,126 @@ const getVersionNums = async () => {
}; };
const res = await obtainAllVersionNumbers(params); const res = await obtainAllVersionNumbers(params);
if (res.code == 200) { if (res.code === 200) {
options.value = res.data; options.value = res.data;
if (res.data.length > 0) { versionMap.clear();
res.data.forEach((item: any) => { options.value.forEach((item) => versionMap.set(item.versions, item));
versionMap.set(item.versions, item);
}); if (options.value.length > 0) {
queryForm.value.versions = res.data[0].versions; queryForm.value.versions = options.value[0].versions;
versionsData.value = options.value.find((item) => item.versions == queryForm.value.versions); versionsData.value = options.value[0];
getSheetName(); getSheetName();
} else { } else {
queryForm.value.versions = ''; queryForm.value.versions = '';
// getSheetName(); sheets.value = [];
tableData.value = [];
expandRowKeys.value = [];
} }
} }
} catch (error) { } catch (error) {
console.log(error); console.error('获取版本号失败:', error);
ElMessage.error('获取版本号失败,请刷新重试');
} }
}; };
//获取表名
// 获取表名列表
const getSheetName = async () => { const getSheetName = async () => {
try { try {
const params = { const params = {
projectId: currentProject.value?.id, projectId: currentProject.value?.id,
versions: queryForm.value.versions versions: queryForm.value.versions
}; };
const res = await sheetList(params); const res = await sheetList(params);
if (res.code == 200) { if (res.code === 200) {
sheets.value = res.data; sheets.value = res.data;
if (res.data.length > 0) { if (sheets.value.length > 0) {
queryForm.value.sheet = res.data[0]; queryForm.value.sheet = sheets.value[0];
} else { } else {
queryForm.value.sheet = ''; queryForm.value.sheet = '';
tableData.value = [];
expandRowKeys.value = [];
} }
getTableData(); getTableData();
} }
} catch (error) { } catch (error) {
console.log(error); console.error('获取表名失败:', error);
ElMessage.error('获取表名失败,请刷新重试');
} }
}; };
// 获取表格数据 // 获取表格数据
const getTableData = async () => { const getTableData = async () => {
try { try {
loading.value = true; loading.value = true;
const params = { const params = {
projectId: currentProject.value?.id, projectId: currentProject.value?.id,
versions: queryForm.value.versions, versions: queryForm.value.versions,
sheet: queryForm.value.sheet, sheet: queryForm.value.sheet,
type: activeTab.value type: activeTab.value
}; };
const res = await getTableList(params); const res = await getTableList(params);
if (res.code == 200) { if (res.code === 200) {
tableData.value = res.data; tableData.value = res.data;
if (isExpandAll.value) {
// 展开全部
isExpandAll.value = false;
toggleExpandAll();
}
modifyPrice.clear();
} }
} catch (error) { } catch (error) {
console.log(error); console.error('获取表格数据失败:', error);
ElMessage.error('获取表格数据失败,请刷新重试');
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; };
//导入
const importExcel = (options: any): any => { // 导入Excel
let formData = new FormData(); 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); formData.append('file', options.file);
loading.value = true; loading.value = true;
importExcelFile( 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 formData
) )
.then((res) => { .then((res) => {
const { code } = res; if (res.code === 200) {
if (code == 200) { proxy?.$modal.msgSuccess(res.msg || '导入成功');
proxy.$modal.msgSuccess(res.msg || '导入成功');
getTableData(); getTableData();
} else { } else {
proxy.$modal.msgError(res.msg || '导入失败'); proxy?.$modal.msgError(res.msg || '导入失败');
} }
}) })
.catch((err) => { .catch((err) => {
proxy.$modal.msgError(err.msg || '导入失败'); proxy?.$modal.msgError(err.msg || '导入失败');
}) })
.finally(() => { .finally(() => {
loading.value = false; loading.value = false;
}); });
}; };
//导出
// 导出Excel
const handleExport = () => { const handleExport = () => {
if (!queryForm.value.versions || (activeTab.value !== '3' && !queryForm.value.sheet)) {
ElMessage.warning('请先选择版本号和表名(物资设备清单无需选择表名)');
return;
}
proxy?.download( proxy?.download(
'/tender/tenderPlanLimitList/export', '/tender/tenderPlanLimitList/export',
{ {
@ -274,90 +391,108 @@ const handleExport = () => {
versions: queryForm.value.versions, versions: queryForm.value.versions,
type: activeTab.value 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) => { // 记录待修改价格的行
const changePrice = (row: TableRow) => {
if (row.id && row.unitPrice !== undefined) {
modifyPrice.set(row.id, row); modifyPrice.set(row.id, row);
// if (!row.unitPrice) { } else if (row.id) {
// modifyPrice.delete(row.id); modifyPrice.delete(row.id);
// } }
}; };
//修改单价
const handleSave = (row?: any, type?: any) => { // 保存价格修改
try { const handleSave = (row?: TableRow, type?: 'single' | 'all') => {
if (type == 'single') { 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; loading.value = true;
const list = [{ ...row, type: activeTab.value }]; updatePrice(updateList)
updatePrice(list).then((res) => { .then((res) => {
if (res.code == 200) { if (res.code === 200) {
ElMessage({ ElMessage.success('修改成功');
message: '修改成功',
type: 'success'
});
getTableData(); getTableData();
modifyPrice.clear();
} else {
ElMessage.error(res.msg || '修改失败');
} }
}); })
} .catch((err) => {
if (type == 'all') { ElMessage.error(err.msg || '修改失败');
loading.value = true; })
const list = []; .finally(() => {
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; loading.value = false;
} });
}; };
/** 审核按钮操作 */ // 审核/查看流程
const handleAudit = async () => { const handleAudit = () => {
let id = versionMap.get(queryForm.value.versions).id; const versionItem = versionMap.get(queryForm.value.versions);
console.log(id); if (!versionItem?.id) {
if (activeTab.value == '2') { ElMessage.warning('请先选择有效的版本号');
return;
}
if (activeTab.value === '2') {
proxy?.$tab.openPage('/approval/tenderBidd/indexEdit', '招采工程量清单审核', { proxy?.$tab.openPage('/approval/tenderBidd/indexEdit', '招采工程量清单审核', {
id: id, id: versionItem.id,
type: 'update' type: 'update'
}); });
} } else if (activeTab.value === '3') {
if (activeTab.value == '3') {
proxy?.$tab.openPage('/approval/tenderBidd/indexEdit2', '物资设备清单审核', { proxy?.$tab.openPage('/approval/tenderBidd/indexEdit2', '物资设备清单审核', {
id: id, id: versionItem.id,
type: 'update' type: 'update'
}); });
} }
}; };
//监听项目id刷新数据 // 监听项目切换
const listeningProject = watch( const listeningProject = watch(
() => currentProject.value?.id, () => currentProject.value?.id,
(nid, oid) => { (newId, oldId) => {
if (newId && newId !== oldId) {
getVersionNums(); getVersionNums();
} else {
tableData.value = [];
options.value = [];
sheets.value = [];
queryForm.value = { versions: '', sheet: '' };
versionsData.value = {};
expandRowKeys.value = [];
versionMap.clear();
modifyPrice.clear();
}
} }
); );
onUnmounted(() => {
listeningProject(); // 生命周期钩子
});
onMounted(() => { onMounted(() => {
getVersionNums(); getVersionNums();
}); });
onUnmounted(() => {
listeningProject();
});
</script> </script>
<style scoped lang="scss"></style> <style scoped lang="scss">
.mb8 {
margin-bottom: 8px;
}
</style>

View File

@ -191,9 +191,16 @@
<el-table-column prop="useQuantity" label="剩余量" align="center"> <el-table-column prop="useQuantity" label="剩余量" align="center">
<template #default="scope"> <template #default="scope">
{{ {{
(scope.row.quantity ? Number(scope.row.quantity) : 0) - (scope.row.useQuantity ? Number(scope.row.useQuantity) : 0) == 0 (scope.row.quantity ? Number(scope.row.quantity) : 0) -
? '' (scope.row.useQuantity ? Number(scope.row.useQuantity) : 0) -
: (scope.row.quantity ? Number(scope.row.quantity) : 0) - (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)
}} }}
</template> </template>
</el-table-column> </el-table-column>
@ -219,12 +226,16 @@
<el-table-column prop="price" label="总价" align="center"> <el-table-column prop="price" label="总价" align="center">
<template #default="scope"> <template #default="scope">
{{ {{
((scope.row.quantity ? Number(scope.row.quantity) : 0) - (scope.row.useQuantity ? Number(scope.row.useQuantity) : 0)) * ((scope.row.quantity ? Number(scope.row.quantity) : 0) -
(scope.row.useQuantity ? Number(scope.row.useQuantity) : 0) -
(scope.row.selectNum ? Number(scope.row.selectNum) : 0)) *
Number(scope.row.unitPrice) == Number(scope.row.unitPrice) ==
0 0
? '' ? ''
: ( : (
((scope.row.quantity ? Number(scope.row.quantity) : 0) - (scope.row.useQuantity ? Number(scope.row.useQuantity) : 0)) * ((scope.row.quantity ? Number(scope.row.quantity) : 0) -
(scope.row.useQuantity ? Number(scope.row.useQuantity) : 0) -
(scope.row.selectNum ? Number(scope.row.selectNum) : 0)) *
Number(scope.row.unitPrice) Number(scope.row.unitPrice)
).toFixed(2) ).toFixed(2)
}} }}
@ -459,19 +470,11 @@ const getSheetName = async () => {
treeForm.value.sheet = res.data[0]; treeForm.value.sheet = res.data[0];
} else { } else {
treeForm.value.sheet = ''; treeForm.value.sheet = '';
ElMessage({
message: '获取表名失败',
type: 'warning'
});
} }
getTreeList(); getTreeList();
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
ElMessage({
message: '获取表名失败',
type: 'warning'
});
} }
}; };
const handleSelection = (selection: any) => { const handleSelection = (selection: any) => {
@ -612,6 +615,7 @@ const changeBiddingTime = (value: any, row: any) => {
}; };
//修改合同金额 //修改合同金额
const changeContractPrice = (value: any, row: any) => { const changeContractPrice = (value: any, row: any) => {
if (value <= Number(row.price)) {
updateTenderPlan({ updateTenderPlan({
...row ...row
}).then((res) => { }).then((res) => {
@ -623,6 +627,9 @@ const changeContractPrice = (value: any, row: any) => {
getList(); getList();
} }
}); });
} else {
ElMessage.error('合同金额不能大于限价金额');
}
}; };
//上传投标文件 //上传投标文件

View File

@ -234,17 +234,17 @@
<!-- 第十一行注册人员数量仅劳务类型显示 --> <!-- 第十一行注册人员数量仅劳务类型显示 -->
<el-row :gutter="20" class="mb-4" v-if="form.supplierType === '劳务'"> <el-row :gutter="20" class="mb-4" v-if="form.supplierType === '劳务'">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="一建建造师" prop="build1"> <el-form-item label="一建建造师" prop="firstBuildingNumber">
<el-input v-model="form.build1" placeholder="请输入一建建造师数量" clearable /> <el-input v-model="form.firstBuildingNumber" placeholder="请输入一建建造师数量" clearable />
</el-form-item> </el-form-item>
<el-form-item label="二建建造师" prop="build2"> <el-form-item label="二建建造师" prop="secondBuildingNumber">
<el-input v-model="form.build2" placeholder="请输入二建建造师数量" clearable /> <el-input v-model="form.secondBuildingNumber" placeholder="请输入二建建造师数量" clearable />
</el-form-item> </el-form-item>
<el-form-item label="注册造价工程师" prop="build3"> <el-form-item label="注册造价工程师" prop="registeredEngineerNumber">
<el-input v-model="form.build3" placeholder="请输入注册造价工程师数量" clearable /> <el-input v-model="form.registeredEngineerNumber" placeholder="请输入注册造价工程师数量" clearable />
</el-form-item> </el-form-item>
<el-form-item label="其他(分别写)" prop="build4"> <el-form-item label="其他(分别写)" prop="otherBuildingNumber">
<el-input v-model="form.build4" placeholder="请输入其他人员数量" clearable /> <el-input v-model="form.otherBuildingNumber" placeholder="请输入其他人员数量" clearable />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
@ -254,17 +254,17 @@
<!-- 第十二行职称人员数量仅劳务类型显示 --> <!-- 第十二行职称人员数量仅劳务类型显示 -->
<el-row :gutter="20" class="mb-4" v-if="form.supplierType === '劳务'"> <el-row :gutter="20" class="mb-4" v-if="form.supplierType === '劳务'">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="高级工程师人数" prop="personnelNumber1"> <el-form-item label="高级工程师人数" prop="seniorEngineerNumber">
<el-input v-model="form.personnelNumber1" placeholder="请输高级工程师数量" clearable /> <el-input v-model="form.seniorEngineerNumber" placeholder="请输高级工程师数量" clearable />
</el-form-item> </el-form-item>
<el-form-item label="工程师数量" prop="personnelNumber2"> <el-form-item label="工程师数量" prop="engineerNumber">
<el-input v-model="form.personnelNumber2" placeholder="请输入工程师数量" clearable /> <el-input v-model="form.engineerNumber" placeholder="请输入工程师数量" clearable />
</el-form-item> </el-form-item>
<el-form-item label="助理工程师数量" prop="personnelNumber3"> <el-form-item label="助理工程师数量" prop="assistantEngineerNumber">
<el-input v-model="form.personnelNumber3" placeholder="请输入助理工程师数量" clearable /> <el-input v-model="form.assistantEngineerNumber" placeholder="请输入助理工程师数量" clearable />
</el-form-item> </el-form-item>
<el-form-item label="其他人员数量" prop="personnelNumber4"> <el-form-item label="其他人员数量" prop="otherPersonnelNumber">
<el-input v-model="form.personnelNumber4" placeholder="请输入其他人员数量" clearable /> <el-input v-model="form.otherPersonnelNumber" placeholder="请输入其他人员数量" clearable />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
@ -285,8 +285,8 @@
:data="form" :data="form"
uploadUrl="/supplierInput/supplierInput" uploadUrl="/supplierInput/supplierInput"
:limit="1" :limit="1"
:onUploadSuccess="handleUploadSuccess" @handleChange="handleFileChange"
@handleChange="change" @handleRemove="handleFileRemove"
showFileList showFileList
> >
<div><el-button type="primary">上传文件</el-button><br /></div> <div><el-button type="primary">上传文件</el-button><br /></div>
@ -309,7 +309,7 @@
<script setup name="SupplierInput" lang="ts"> <script setup name="SupplierInput" lang="ts">
import { ComponentInternalInstance, getCurrentInstance, onMounted, ref, reactive, toRefs, computed } from 'vue'; import { ComponentInternalInstance, getCurrentInstance, onMounted, ref, reactive, toRefs, computed } from 'vue';
import { ElFormInstance } from 'element-plus'; import { ElFormInstance } from 'element-plus';
import { listSupplierInput, getSupplierInput, delSupplierInput } from '@/api/supplierInput/supplierInput/index'; import { listSupplierInput, getSupplierInput, delSupplierInput, updateSupplierInput } from '@/api/supplierInput/supplierInput/index';
import { SupplierInputVO, SupplierInputQuery, SupplierInputForm, PageData, DialogOption } from '@/api/supplierInput/supplierInput/types'; import { SupplierInputVO, SupplierInputQuery, SupplierInputForm, PageData, DialogOption } from '@/api/supplierInput/supplierInput/types';
import Pagination from '@/components/Pagination/index.vue'; import Pagination from '@/components/Pagination/index.vue';
import RightToolbar from '@/components/RightToolbar/index.vue'; import RightToolbar from '@/components/RightToolbar/index.vue';
@ -369,14 +369,14 @@ const initFormData: any = {
inputFile: undefined, inputFile: undefined,
// state: '0', // 新增默认待审核 // state: '0', // 新增默认待审核
// 新增:用于表单输入的单独字段 // 新增:用于表单输入的单独字段
build1: undefined, // 一建建造师 firstBuildingNumber: undefined, // 一建建造师
build2: undefined, // 二建建造师 secondBuildingNumber: undefined, // 二建建造师
build3: undefined, // 注册造价工程师 registeredEngineerNumber: undefined, // 注册造价工程师
build4: undefined, // 其他注册人员 otherBuildingNumber: undefined, // 其他注册人员
personnelNumber1: undefined, // 高级工程师 seniorEngineerNumber: undefined, // 高级工程师
personnelNumber2: undefined, // 工程师 engineerNumber: undefined, // 工程师
personnelNumber3: undefined, // 助理工程师 assistantEngineerNumber: undefined, // 助理工程师
personnelNumber4: undefined // 其他职称人员 otherPersonnelNumber: undefined // 其他职称人员
}; };
// 核心数据(表单+查询参数) // 核心数据(表单+查询参数)
const data = reactive<PageData<SupplierInputForm, SupplierInputQuery>>({ const data = reactive<PageData<SupplierInputForm, SupplierInputQuery>>({
@ -411,14 +411,14 @@ const rules = computed(() => {
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' } { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
], ],
id: [{ required: true, message: 'ID不能为空', trigger: 'blur' }], id: [{ required: true, message: 'ID不能为空', trigger: 'blur' }],
build1: [{ required: true, message: '请输入一建建造师数量', trigger: 'change' }], firstBuildingNumber: [{ required: true, message: '请输入一建建造师数量', trigger: 'change' }],
build2: [{ required: true, message: '请输入二建建造师数量', trigger: 'change' }], secondBuildingNumber: [{ required: true, message: '请输入二建建造师数量', trigger: 'change' }],
build3: [{ required: true, message: '请输入注册造价工程师数量', trigger: 'change' }], registeredEngineerNumber: [{ required: true, message: '请输入注册造价工程师数量', trigger: 'change' }],
build4: [{ required: true, message: '请输入其他数量', trigger: 'change' }], otherBuildingNumber: [{ required: true, message: '请输入其他数量', trigger: 'change' }],
personnelNumber1: [{ required: true, message: '请输入高级工程师数量', trigger: 'change' }], seniorEngineerNumber: [{ required: true, message: '请输入高级工程师数量', trigger: 'change' }],
personnelNumber2: [{ required: true, message: '请输入工程师数量', trigger: 'change' }], engineerNumber: [{ required: true, message: '请输入工程师数量', trigger: 'change' }],
personnelNumber3: [{ required: true, message: '请输入助理工程师数量', trigger: 'change' }], assistantEngineerNumber: [{ required: true, message: '请输入助理工程师数量', trigger: 'change' }],
personnelNumber4: [{ required: true, message: '请输入其他数量', trigger: 'change' }] otherPersonnelNumber: [{ required: true, message: '请输入其他数量', trigger: 'change' }]
}; };
// 仅当类型为"劳务"时,添加安全生产许可证+人员数量校验 // 仅当类型为"劳务"时,添加安全生产许可证+人员数量校验
@ -430,15 +430,15 @@ const rules = computed(() => {
safeCodeData: [{ required: true, message: '请选择安全生产许可证发证日期', trigger: 'change' }], safeCodeData: [{ required: true, message: '请选择安全生产许可证发证日期', trigger: 'change' }],
safeCertificateValidity: [{ required: true, message: '请选择安全生产许可证有效期', trigger: 'change' }], safeCertificateValidity: [{ required: true, message: '请选择安全生产许可证有效期', trigger: 'change' }],
// 注册人员数量校验 // 注册人员数量校验
build1: [{ required: true, message: '请输入一建建造师数量', trigger: 'blur' }], firstBuildingNumber: [{ required: true, message: '请输入一建建造师数量', trigger: 'blur' }],
build2: [{ required: true, message: '请输入二建建造师数量', trigger: 'blur' }], secondBuildingNumber: [{ required: true, message: '请输入二建建造师数量', trigger: 'blur' }],
build3: [{ required: true, message: '请输入注册造价工程师数量', trigger: 'blur' }], registeredEngineerNumber: [{ required: true, message: '请输入注册造价工程师数量', trigger: 'blur' }],
build4: [{ required: true, message: '请输入其他注册人员数量', trigger: 'blur' }], otherBuildingNumber: [{ required: true, message: '请输入其他注册人员数量', trigger: 'blur' }],
// 职称人员数量校验 // 职称人员数量校验
personnelNumber1: [{ required: true, message: '请输入高级工程师数量', trigger: 'blur' }], seniorEngineerNumber: [{ required: true, message: '请输入高级工程师数量', trigger: 'blur' }],
personnelNumber2: [{ required: true, message: '请输入工程师数量', trigger: 'blur' }], engineerNumber: [{ required: true, message: '请输入工程师数量', trigger: 'blur' }],
personnelNumber3: [{ required: true, message: '请输入助理工程师数量', trigger: 'blur' }], assistantEngineerNumber: [{ required: true, message: '请输入助理工程师数量', trigger: 'blur' }],
personnelNumber4: [{ required: true, message: '请输入其他职称人员数量', trigger: 'blur' }] otherPersonnelNumber: [{ required: true, message: '请输入其他职称人员数量', trigger: 'blur' }]
}; };
} }
@ -455,22 +455,26 @@ const handleTypeChange = () => {
form.value.registeredNumber = undefined; form.value.registeredNumber = undefined;
form.value.personnelNumber = undefined; form.value.personnelNumber = undefined;
// 清空表单单独字段 // 清空表单单独字段
form.value.build1 = form.value.build2 = form.value.build3 = form.value.build4 = undefined; form.value.firstBuildingNumber =
form.value.personnelNumber1 = form.value.personnelNumber2 = form.value.personnelNumber3 = form.value.personnelNumber4 = undefined; form.value.secondBuildingNumber =
form.value.registeredEngineerNumber =
form.value.otherBuildingNumber =
undefined;
form.value.seniorEngineerNumber = form.value.engineerNumber = form.value.assistantEngineerNumber = form.value.otherPersonnelNumber = undefined;
} }
// 重置隐藏字段的校验状态,避免错误提示残留 // 重置隐藏字段的校验状态,避免错误提示残留
supplierInputFormRef.value?.clearValidate([ supplierInputFormRef.value?.clearValidate([
'safeCode', 'safeCode',
'safeCodeData', 'safeCodeData',
'safeCertificateValidity', 'safeCertificateValidity',
'build1', 'firstBuildingNumber',
'build2', 'secondBuildingNumber',
'build3', 'registeredEngineerNumber',
'build4', 'otherBuildingNumber',
'personnelNumber1', 'seniorEngineerNumber',
'personnelNumber2', 'engineerNumber',
'personnelNumber3', 'assistantEngineerNumber',
'personnelNumber4' 'otherPersonnelNumber'
]); ]);
}; };
@ -517,18 +521,18 @@ const resetQuery = () => {
const splitBackEndStrToForm = (resData: any) => { const splitBackEndStrToForm = (resData: any) => {
if (resData.registeredNumber) { if (resData.registeredNumber) {
const registeredArr = resData.registeredNumber.split(','); const registeredArr = resData.registeredNumber.split(',');
form.value.build1 = registeredArr[0] || undefined; // 一建建造师 form.value.firstBuildingNumber = registeredArr[0] || undefined; // 一建建造师
form.value.build2 = registeredArr[1] || undefined; // 二建建造师 form.value.secondBuildingNumber = registeredArr[1] || undefined; // 二建建造师
form.value.build3 = registeredArr[2] || undefined; // 注册造价工程师 form.value.registeredEngineerNumber = registeredArr[2] || undefined; // 注册造价工程师
form.value.build4 = registeredArr[3] || undefined; // 其他注册人员 form.value.otherBuildingNumber = registeredArr[3] || undefined; // 其他注册人员
} }
if (resData.personnelNumber) { if (resData.personnelNumber) {
const personnelArr = resData.personnelNumber.split(','); const personnelArr = resData.personnelNumber.split(',');
form.value.personnelNumber1 = personnelArr[0] || undefined; // 高级工程师 form.value.seniorEngineerNumber = personnelArr[0] || undefined; // 高级工程师
form.value.personnelNumber2 = personnelArr[1] || undefined; // 工程师 form.value.engineerNumber = personnelArr[1] || undefined; // 工程师
form.value.personnelNumber3 = personnelArr[2] || undefined; // 助理工程师 form.value.assistantEngineerNumber = personnelArr[2] || undefined; // 助理工程师
form.value.personnelNumber4 = personnelArr[3] || undefined; // 其他职称人员 form.value.otherPersonnelNumber = personnelArr[3] || undefined; // 其他职称人员
} }
}; };
/** 审核过程按钮操作 */ /** 审核过程按钮操作 */
@ -572,6 +576,7 @@ const handleAdd = () => {
dialog.title = '添加供应商入库'; dialog.title = '添加供应商入库';
}; };
const editFileId = ref('');
const handleUpdate = async (row?: SupplierInputVO) => { const handleUpdate = async (row?: SupplierInputVO) => {
reset(); reset();
const _id = row?.id || ids.value[0]; const _id = row?.id || ids.value[0];
@ -579,7 +584,8 @@ const handleUpdate = async (row?: SupplierInputVO) => {
try { try {
const res = await getSupplierInput(_id); const res = await getSupplierInput(_id);
const resData = res.data || {}; const resData: any = res.data || {};
editFileId.value = resData.fileId;
// 1. 基础字段回显 // 1. 基础字段回显
form.value = { ...form.value, ...resData, inputFile: '' }; form.value = { ...form.value, ...resData, inputFile: '' };
// 2. 核心修复:拆分后端拼接字符串到表单单独字段 // 2. 核心修复:拆分后端拼接字符串到表单单独字段
@ -594,24 +600,52 @@ const handleUpdate = async (row?: SupplierInputVO) => {
} }
}; };
const fileStatus = ref(false);
const updateFileStatus = ref(true);
const isUpdateFile = ref(false); //记录是否在修改页面时是否有新上传的文件
const handleFileChange = (file, fileList) => {
if (form.value.id) {
updateFileStatus.value = true;
isUpdateFile.value = true; //记录是否在修改页面时是否有新上传的文件
}
fileStatus.value = true;
};
const handleFileRemove = (file, fileList) => {
if (form.value.id) {
updateFileStatus.value = false;
isUpdateFile.value = false; //记录是否在修改页面时是否有新上传的文件
}
fileStatus.value = false;
};
/** 提交表单 */ /** 提交表单 */
const submitForm = () => { const submitForm = () => {
supplierInputFormRef.value?.validate(async (valid: boolean) => { supplierInputFormRef.value?.validate(async (valid: boolean) => {
if (!valid) return; if (!valid) return;
if (form.value.supplierType === '劳务') { if (form.value.supplierType === '劳务') {
form.value.registeredNumber = [form.value.build1, form.value.build2, form.value.build3, form.value.build4].join(','); form.value.registeredNumber = [
form.value.firstBuildingNumber,
form.value.secondBuildingNumber,
form.value.registeredEngineerNumber,
form.value.otherBuildingNumber
].join(',');
form.value.personnelNumber = [ form.value.personnelNumber = [
form.value.personnelNumber1, form.value.seniorEngineerNumber,
form.value.personnelNumber2, form.value.engineerNumber,
form.value.personnelNumber3, form.value.assistantEngineerNumber,
form.value.personnelNumber4 form.value.otherPersonnelNumber
].join(','); ].join(',');
} }
buttonLoading.value = true; buttonLoading.value = true;
try { try {
if (fileUploadRef.value) { if (fileUploadRef.value) {
await fileUploadRef.value.submitUpload().then((res) => { if (form.value.fileId === editFileId.value && !isUpdateFile.value) {
console.log(res); console.log(1111111111);
editFormData();
} else {
fileUploadRef.value.submitUpload().then((res) => {
if (res == 'noFile') { if (res == 'noFile') {
proxy?.$modal.msgError('请上传文件'); proxy?.$modal.msgError('请上传文件');
return; return;
@ -621,6 +655,7 @@ const submitForm = () => {
getList(); getList();
}); });
} }
}
} catch (error) { } catch (error) {
proxy?.$modal.msgError('提交失败,请重试'); proxy?.$modal.msgError('提交失败,请重试');
} finally { } finally {
@ -628,7 +663,14 @@ const submitForm = () => {
} }
}); });
}; };
const editFormData = async () => {
const res = await updateSupplierInput(form.value);
if ((res.code = 200)) {
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
getList();
}
};
/** 删除操作 */ /** 删除操作 */
const handleDelete = async (row?: SupplierInputVO) => { const handleDelete = async (row?: SupplierInputVO) => {
const _ids = row?.id || ids.value; const _ids = row?.id || ids.value;

View File

@ -104,8 +104,9 @@
<el-col :span="12"> <el-col :span="12">
<el-form-item label="近三年营业额" prop="pastThreeYears"> <el-form-item label="近三年营业额" prop="pastThreeYears">
<el-input v-model="form.pastThreeYears" placeholder="请输入近三年营业额" clearable /> <el-input v-model="form.pastThreeYears" placeholder="请输入近三年营业额" clearable />
</el-form-item> </el-col </el-form-item>
><el-col :span="12"> </el-col>
<!-- <el-col :span="12">
<el-form-item label="生产许可证编号" prop="safeCode"> <el-form-item label="生产许可证编号" prop="safeCode">
<el-input v-model="form.safeCode" placeholder="请输入许可证编号" clearable /> <el-input v-model="form.safeCode" placeholder="请输入许可证编号" clearable />
</el-form-item> </el-form-item>
@ -118,45 +119,45 @@
<el-form-item label="生产许可证发证日期" prop="safeCertificateValidity"> <el-form-item label="生产许可证发证日期" prop="safeCertificateValidity">
<el-date-picker v-model="form.safeCertificateValidity" type="date" placeholder="请选择发证日期" /> <el-date-picker v-model="form.safeCertificateValidity" type="date" placeholder="请选择发证日期" />
</el-form-item> </el-form-item>
</el-col> </el-col> -->
</el-row> </el-row>
<el-row class="mb-4" v-if="form.supplierType === '劳务'"> <el-row class="mb-4" v-if="form.supplierType === '劳务'">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="一建建造师" prop="build1"> <el-form-item label="一建建造师" prop="firstBuildingNumber">
<el-input v-model="form.build1" placeholder="请输入一建建造师数量" clearable /> <el-input v-model="form.firstBuildingNumber" placeholder="请输入一建建造师数量" clearable />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="二建建造师" prop="build2"> <el-form-item label="二建建造师" prop="secondBuildingNumber">
<el-input v-model="form.build2" placeholder="请输入二建建造师数量" clearable /> <el-input v-model="form.secondBuildingNumber" placeholder="请输入二建建造师数量" clearable />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="其他(分别写)" prop="build4"> <el-form-item label="其他(分别写)" prop="otherBuildingNumber">
<el-input v-model="form.build3" placeholder="请输入其他人员数量" clearable /> <el-input v-model="form.otherBuildingNumber" placeholder="请输入其他人员数量" clearable />
</el-form-item> </el-col </el-form-item> </el-col
><el-col :span="12"> ><el-col :span="12">
<el-form-item label="注册造价工程师" prop="build3"> <el-form-item label="注册造价工程师" prop="registeredEngineerNumber">
<el-input v-model="form.build4" placeholder="请输入注册造价工程师数量" clearable /> <el-input v-model="form.registeredEngineerNumber" placeholder="请输入注册造价工程师数量" clearable />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="24" class="mb-4" v-if="form.supplierType === '劳务'"> <el-row :gutter="24" class="mb-4" v-if="form.supplierType === '劳务'">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="高级工程师人数" prop="personnelNumber1"> <el-form-item label="高级工程师人数" prop="seniorEngineerNumber">
<el-input v-model="form.personnelNumber1" placeholder="请输高级工程师数量" clearable /> <el-input v-model="form.seniorEngineerNumber" placeholder="请输高级工程师数量" clearable />
</el-form-item> </el-col </el-form-item> </el-col
><el-col :span="12"> ><el-col :span="12">
<el-form-item label="工程师数量" prop="personnelNumber2"> <el-form-item label="工程师数量" prop="engineerNumber">
<el-input v-model="form.personnelNumber2" placeholder="请输入工程师数量" clearable /> <el-input v-model="form.engineerNumber" placeholder="请输入工程师数量" clearable />
</el-form-item> </el-col </el-form-item> </el-col
><el-col :span="12"> ><el-col :span="12">
<el-form-item label="助理工程师数量" prop="personnelNumber3"> <el-form-item label="助理工程师数量" prop="assistantEngineerNumber">
<el-input v-model="form.personnelNumber3" placeholder="请输入助理工程师数量" clearable /> <el-input v-model="form.assistantEngineerNumber" placeholder="请输入助理工程师数量" clearable />
</el-form-item> </el-col </el-form-item> </el-col
><el-col :span="12"> ><el-col :span="12">
<el-form-item label="其他人员数量" prop="personnelNumber4"> <el-form-item label="其他人员数量" prop="otherPersonnelNumber">
<el-input v-model="form.personnelNumber4" placeholder="请输入其他人员数量" clearable /> <el-input v-model="form.otherPersonnelNumber" placeholder="请输入其他人员数量" clearable />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -270,14 +271,14 @@ const initFormData = {
inputFile: undefined, inputFile: undefined,
state: '0', // 新增默认待审核 state: '0', // 新增默认待审核
// 新增:用于表单输入的单独字段 // 新增:用于表单输入的单独字段
build1: undefined, // 一建建造师 firstBuildingNumber: undefined, // 一建建造师
build2: undefined, // 二建建造师 secondBuildingNumber: undefined, // 二建建造师
build3: undefined, // 注册造价工程师 registeredEngineerNumber: undefined, // 注册造价工程师
build4: undefined, // 其他注册人员 otherBuildingNumber: undefined, // 其他注册人员
personnelNumber1: undefined, // 高级工程师 seniorEngineerNumber: undefined, // 高级工程师
personnelNumber2: undefined, // 工程师 engineerNumber: undefined, // 工程师
personnelNumber3: undefined, // 助理工程师 assistantEngineerNumber: undefined, // 助理工程师
personnelNumber4: undefined // 其他职称人员 otherPersonnelNumber: undefined
}; };
const data = reactive<PageData<LeaveForm, LeaveQuery>>({ const data = reactive<PageData<LeaveForm, LeaveQuery>>({
form: { ...initFormData }, form: { ...initFormData },
@ -332,17 +333,27 @@ const getInfo = () => {
buttonLoading.value = false; buttonLoading.value = false;
nextTick(async () => { nextTick(async () => {
const res = await getSupplierInput(routeParams.value.id); const res = await getSupplierInput(routeParams.value.id);
console.log(res, '------------------res');
Object.assign(form.value, res.data); Object.assign(form.value, res.data);
form.value.registeredNumber = form.value.registeredNumber?.split(','); // form.value.firstBuildingNumber=res.data.firstBuildingNumber, // 一建建造师
form.value.build1 = form.value.registeredNumber[0] || ''; // secondBuildingNumber: undefined, // 二建建造师
form.value.build2 = form.value.registeredNumber[1] || ''; // registeredEngineerNumber: undefined, // 注册造价工程师
form.value.build3 = form.value.registeredNumber[2] || ''; // otherBuildingNumber: undefined, // 其他注册人员
form.value.build4 = form.value.registeredNumber[3] || ''; // seniorEngineerNumber: undefined, // 高级工程师
form.value.personnelNumber = form.value.personnelNumber?.split(','); // engineerNumber: undefined, // 工程师
form.value.personnelNumber1 = form.value.personnelNumber[0] || ''; // assistantEngineerNumber: undefined, // 助理工程师
form.value.personnelNumber2 = form.value.personnelNumber[1] || ''; // otherPersonnelNumber: undefined
form.value.personnelNumber3 = form.value.personnelNumber[2] || ''; // form.value.registeredNumber = form.value.registeredNumber?.split(',');
form.value.personnelNumber4 = form.value.personnelNumber[3] || ''; // form.value.build1 = form.value.registeredNumber[0] || '';
// form.value.build2 = form.value.registeredNumber[1] || '';
// form.value.build3 = form.value.registeredNumber[2] || '';
// form.value.build4 = form.value.registeredNumber[3] || '';
// form.value.personnelNumber = form.value.personnelNumber?.split(',');
// form.value.personnelNumber1 = form.value.personnelNumber[0] || '';
// form.value.personnelNumber2 = form.value.personnelNumber[1] || '';
// form.value.personnelNumber3 = form.value.personnelNumber[2] || '';
// form.value.personnelNumber4 = form.value.personnelNumber[3] || '';
loading.value = false; loading.value = false;
buttonLoading.value = false; buttonLoading.value = false;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long