This commit is contained in:
dhr
2025-09-06 14:37:46 +08:00
87 changed files with 7561 additions and 2061 deletions

View File

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

View File

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

View File

@ -91,7 +91,9 @@
<template #default="scope">
<!-- 查看子项按钮 -->
<el-tooltip content="查看子项" placement="top">
<el-button link type="primary" @click="handleViewSons(scope.row)" v-hasPermi="['land:landTransferLedger:view']"> 查看子项 </el-button>
<el-button link type="primary" @click="handleViewSons(scope.row)" v-hasPermi="['land:landTransferLedger:childrenList']">
查看子项
</el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" @click="handleDelete(scope.row)" v-hasPermi="['land:landTransferLedger:remove']">删除</el-button>
@ -147,7 +149,9 @@
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAddSon" v-hasPermi="['land:landTransferLedger:addSon']"> 新增子项 </el-button>
<el-button type="primary" plain icon="Plus" @click="handleAddSon" v-hasPermi="['land:landTransferLedger:childrenAdd']">
新增子项
</el-button>
</el-col>
</el-row>
</template>

View File

@ -144,13 +144,16 @@
</el-table-column> -->
<el-table-column label="备注" align="center" prop="remark" />
<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">
<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>
<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="primary" icon="upload" @click="handleUpload(scope.row)" v-hasPermi="['project:project:saveTenderFile']"
@ -160,7 +163,6 @@
</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>
<!-- 添加或修改项目对话框 -->
@ -253,78 +255,6 @@
</el-col>
</el-row>
</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>
<template #footer>
<div class="dialog-footer">
@ -342,13 +272,12 @@
</div>
</template>
</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>
</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
:project-id="projectId"
:design-id="designId"
@ -356,7 +285,7 @@
@close="polygonStatus = false"
></open-layers-map>
</el-dialog>
<el-dialog title="添加子项目" v-model="childProjectStatus" width="400">
<el-dialog draggable title="添加子项目" v-model="childProjectStatus" width="400">
<span>填写子项目名称</span>
<el-input v-model="childProjectForm.projectName"></el-input>
<template #footer>
@ -366,7 +295,7 @@
</span>
</template>
</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" />
<template #footer>
<div class="dialog-footer">
@ -375,6 +304,91 @@
</div>
</template>
</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>
</template>
@ -382,17 +396,24 @@
import {
addChildProject,
addProject,
addProjectFacilities,
addProjectPilePoint,
addProjectSquare,
delProject,
uploadProjectFile,
getProject,
listProject,
updateProject
updateProject,
attendanceRuleAdd,
attendanceRuleEdit,
byProjectIdDetail,
getAttendanceRangeList,
updateAttendanceRange,
delAttendanceRange
} from '@/api/project/project';
import { ProjectForm, ProjectQuery, ProjectVO, childProjectQuery, locationType } from '@/api/project/project/types';
import amap from '@/components/amap/index.vue';
import CesiumEarthDialog from "./map.vue"
import { useUserStoreHook } from '@/store/modules/user';
const userStore = useUserStoreHook();
const currentProject = computed(() => userStore.selectedProject);
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable, project_category_type, project_type, project_stage } = toRefs<any>(
proxy?.useDict('sys_normal_disable', 'project_category_type', 'project_type', 'project_stage')
@ -413,6 +434,22 @@ const polygonStatus = ref(false);
const dxfFile = ref(null);
const projectId = ref<string>('');
const designId = ref<string>('');
const ruleFlag = ref(false);
const ScopeFlag = ref(false);
const earthDialog = ref(null);
const position = ref(''); //预览
let earthData = reactive({
projectId: '',
punchName: '',
punchColor: '#1983ff',
punchRange: '',
}); //传值给地图组件
const punchRangeList = ref<any>([
{
punchName: '',
punchColor: '#1983ff'
}
]);
const childProjectForm = reactive<childProjectQuery>({
projectName: '',
pid: '',
@ -432,7 +469,7 @@ const fileForm = ref({
const jsonData = ref(null);
const fullscreenLoading = ref(false);
const initFormData: ProjectForm = {
const initFormData = {
id: undefined,
projectName: undefined,
shortName: undefined,
@ -451,13 +488,15 @@ const initFormData: ProjectForm = {
lat: undefined,
plan: undefined,
onStreamTime: undefined,
playCardStart: undefined,
playCardEnd: undefined,
clockInTime: undefined,
clockOutTime: undefined,
designTotal: undefined,
securityAgreement: undefined,
sort: 0,
showHidden: undefined,
isDelete: undefined
isDelete: undefined,
type: '1',
weekday: []
};
const data = reactive<PageData<ProjectForm, ProjectQuery>>({
form: { ...initFormData },
@ -480,8 +519,6 @@ const data = reactive<PageData<ProjectForm, ProjectQuery>>({
lat: undefined,
plan: undefined,
onStreamTime: undefined,
playCardStart: undefined,
playCardEnd: undefined,
designTotal: undefined,
securityAgreement: undefined,
sort: undefined,
@ -490,8 +527,8 @@ const data = reactive<PageData<ProjectForm, ProjectQuery>>({
params: {}
},
rules: {
playCardStart: [{ required: true, message: '打卡开始时间不能为空', trigger: 'blur' }],
playCardEnd: [{ required: true, message: '打卡结束时间不能为空', trigger: 'blur' }],
clockInTime: [{ required: true, message: '打卡开始时间不能为空', trigger: 'blur' }],
clockOutTime: [{ required: true, message: '打卡结束时间不能为空', trigger: 'blur' }],
projectName: [{ required: true, message: '项目名称不能为空', trigger: 'blur' }],
shortName: [{ 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 = () => {
proxy?.download(
@ -703,7 +819,36 @@ const handleExport = () => {
`project_${new Date().getTime()}.xlsx`
);
};
// 打开绘制范围
const addPunchRange = (item) => {
earthData.punchName = item.punchName
earthData.punchColor = item.punchColor
earthData.projectId = projectId.value
earthData.id = ''
earthData.punchRange = ''
if (earthData.punchName=='') {
proxy.$modal.msgError('请先填写打卡范围名称');
return
}
earthDialog.value.show()
}
// 接受绘制范围
const handleEarthData = (data) => {
ScopeFlag.value = false;
}
// 关闭绘制范围
const handleEarthClose = () => {
earthDialog.value.show = false
}
// 预览范围
const previewPunchRange = (item)=>{
earthData.id = item.id
earthData.projectId = item.projectId
earthData.punchName = item.punchName
earthData.punchColor = item.punchColor
earthData.punchRange = item.punchRange
earthDialog.value.show()
}
onMounted(() => {
getList();
});

View File

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

View File

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

View File

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