yanghao 1 месяц назад
Родитель
Сommit
71cdb0ef26

+ 91 - 14
api/ruihen.js

@@ -1,25 +1,95 @@
-import { request } from '@/utils/request';
+import { request } from "@/utils/request";
 
 export function getRuiHenReportDetail(params) {
   return request({
-    url: '/pms/iot-rh-daily-report/get',
-    method: 'get',
+    url: "/pms/iot-rh-daily-report/get",
+    method: "get",
     params,
   });
 }
 
 export function createIotRhDailyReport(data) {
-  return request({ url: `/pms/iot-rh-daily-report/create`, data, method: 'post' });
+  return request({
+    url: `/pms/iot-rh-daily-report/create`,
+    data,
+    method: "post",
+  });
 }
 
 export function approvalIotRhDailyReport(data) {
-  return request({ url: `/pms/iot-rh-daily-report/approval`, data, method: 'put' });
+  return request({
+    url: `/pms/iot-rh-daily-report/approval`,
+    data,
+    method: "put",
+  });
 }
 
 export function getRuiHenReportPage(params) {
   return request({
-    url: '/pms/iot-rh-daily-report/page',
-    method: 'get',
+    url: "/pms/iot-rh-daily-report/page",
+    method: "get",
+    params,
+  });
+}
+
+// 瑞恒任务列表
+export function getRuiHenTaskList(params) {
+  return request({
+    url: "/rq/iot-project-task/list",
+    method: "get",
+    params,
+  });
+}
+
+// 瑞恒任务详情
+export function getRuiHenTaskDetail(params) {
+  return request({
+    url: "/rq/iot-project-task/get",
+    method: "get",
+    params,
+  });
+}
+
+export function getRuiHenTaskDetail1(params) {
+  return request({
+    url: "/rq/iot-project-task/page",
+    method: "get",
+    params,
+  });
+}
+
+// 新增瑞恒任务
+export function createRuiHenTask(data) {
+  return request({
+    url: "/rq/iot-project-task/create",
+    method: "post",
+    data,
+  });
+}
+
+// 编辑瑞恒任务
+export function updateRuiHenTask(data) {
+  return request({
+    url: "/rq/iot-project-task/update",
+    method: "put",
+    data,
+  });
+}
+
+// 合同分页列表,当前页面只使用 deptId 过滤
+export function getRuiHenProjectInfoPage(params) {
+  return request({
+    url: "/rq/iot-project-info/page",
+    method: "get",
+    params,
+  });
+}
+
+// 根据施工队伍查询可选施工设备
+export function getDevicesByDepts(params) {
+  return request({
+    url: "/rq/iot-device/getDevicesByDepts",
+    method: "get",
     params,
   });
 }
@@ -28,19 +98,26 @@ export function getRuiHenReportPage(params) {
 export function companyLevelChildrenDepts() {
   // return  request.get({ url: '/system/dept/companyLevelChildrenDepts' })
   return request({
-    url: '/system/dept/companyLevelChildrenDepts',
-    method: 'get'
+    url: "/system/dept/companyLevelChildrenDepts",
+    method: "get",
   });
 }
 
-
 // 获取公司层级的部门包含的子部门下的所有人员
 export const companyDeptsEmployee = (params) => {
   // return request.get({ url: '/system/user/companyDeptsEmployee', params })
   return request({
-    url: '/system/user/companyDeptsEmployee',
-    method: 'get',
-   
+    url: "/system/user/companyDeptsEmployee",
+    method: "get",
+
+    params,
+  });
+};
+
+export const selectedDeptsEmployee = (params) => {
+  return request({
+    url: "/system/user/selectedDeptsEmployee",
+    method: "get",
     params,
   });
-}
+};

+ 9 - 4
locale/zh-Hans.json

@@ -129,6 +129,7 @@
   "home.dailyReportRuiDuTip": "填写日报",
   "home.dailyReportRuiHen": "瑞恒日报",
   "home.dailyReportRuiHenTip": "填写日报",
+  "home.dailyReportRuiHenTaskTip": "分配任务",
   "home.dailyReportRuiHenApproval": "审批日报",
   "home.dailyReportRuiYing": "瑞鹰钻井日报",
   "home.dailyReportRuiYingTip": "填写日报",
@@ -405,10 +406,14 @@
   "inspection.pendingInspectionItems": "待填写巡检项",
   "inspection.normalInspectionItems": "正常巡检项",
   "inspection.abnormalInspectionItems": "异常巡检项",
-  // --------------------------------------- 瑞都日报 ----------------------------------------
-  "ruiDu.indexTitle": "日报",
-  "ruiDu.detailTitle": "日报详情",
-  "ruiDu.editTitle": "日报填报",
+  // --------------------------------------- 瑞都日报 ----------------------------------------
+  "ruiDu.indexTitle": "日报",
+  "ruihen.taskTitle": "分配任务",
+  "ruihen.taskCreateTitle": "新增任务",
+  "ruihen.taskDetailTitle": "任务详情",
+  "ruihen.taskEditTitle": "编辑任务",
+  "ruiDu.detailTitle": "日报详情",
+  "ruiDu.editTitle": "日报填报",
   "ruiDu.approvalTitle": "日报填报",
   "ruiDu.createTitle": "新增日报",
   "ruiDu.shiftLeader": "带班干部",

+ 29 - 5
pages.json

@@ -170,11 +170,35 @@
         "navigationStyle": "custom"
       }
     },
-    {
-      "path": "pages/ruihen/index",
-      "style": {
-        "navigationBarTitleText": "%ruiDu.indexTitle%"
-      }
+    {
+      "path": "pages/ruihen-task/index",
+      "style": {
+        "navigationBarTitleText": "%ruihen.taskTitle%"
+      }
+    },
+    {
+      "path": "pages/ruihen-task/create",
+      "style": {
+        "navigationBarTitleText": "%ruihen.taskCreateTitle%"
+      }
+    },
+    {
+      "path": "pages/ruihen-task/detail",
+      "style": {
+        "navigationBarTitleText": "%ruihen.taskDetailTitle%"
+      }
+    },
+    {
+      "path": "pages/ruihen-task/edit",
+      "style": {
+        "navigationBarTitleText": "%ruihen.taskEditTitle%"
+      }
+    },
+    {
+      "path": "pages/ruihen/index",
+      "style": {
+        "navigationBarTitleText": "%ruiDu.indexTitle%"
+      }
     },
     {
       "path": "pages/ruihen/detail",

+ 20 - 0
pages/home/index.vue

@@ -166,6 +166,23 @@
             <uni-icons type="right" :color="'#CACCCF'" size="15" />
           </view>
         </view>
+        <view
+          class="card-cell flex-row align-center justify-between"
+          @click="navigatorTo('/pages/ruihen-task/index')"
+          v-if="rhTaskFlag">
+          <image src="/static/home/ribao.svg" mode="aspectFill"></image>
+          <view class="cell-con flex-row align-center justify-between">
+            <view class="cell-text flex-row align-center justify-start">
+              <view class="title">
+                {{ $t("home.dailyReportRuiHen") }}
+              </view>
+              <view class="subtitle">
+                {{ $t("home.dailyReportRuiHenTaskTip") }}
+              </view>
+            </view>
+            <uni-icons type="right" :color="'#CACCCF'" size="15" />
+          </view>
+        </view>
         <view
           class="card-cell flex-row align-center justify-between"
           @click="navigatorTo('/pages/ruihen/index?type=edit')"
@@ -409,6 +426,7 @@ onMounted(async () => {
 // 是否展示瑞都日报入口
 const isShowRuiduDaily = ref(false);
 const rhReportFlag = ref(false);
+const rhTaskFlag = ref(false);
 const rhReportApprovalFlag = ref(false);
 const ryReportFlag = ref(false);
 const ryReportApprovalFlag = ref(false);
@@ -422,6 +440,8 @@ const getLoginUser = async () => {
     isShowRuiduDaily.value = response.data.rdReportFlag;
     // isShowRuiduDaily.value = true;
     rhReportFlag.value = response.data.rhReportFlag;
+    rhTaskFlag.value = response.data.rhReportFlag;
+    // rhTaskFlag.value = true;
     rhReportApprovalFlag.value = response.data.rhReportApprovalFlag;
     ryReportFlag.value = response.data.ryReportFlag;
     // ryReportFlag.value = true;

+ 1 - 1
pages/ruiDu/create.vue

@@ -39,7 +39,7 @@ const defaultProps = {
 };
 
 const original = {
-  deptId: getDeptId(),
+  // deptId: getDeptId(),
   startTime: "08:00",
   endTime: "08:00",
   ...NON_PROD_FIELDS.reduce((acc, field) => ({ ...acc, [field.key]: 0 }), {}),

+ 943 - 0
pages/ruihen-task/components/form.vue

@@ -0,0 +1,943 @@
+<script setup>
+import {
+  computed,
+  nextTick,
+  onMounted,
+  ref,
+  reactive,
+  defineExpose,
+  watch,
+} from "vue";
+import { useDataDictStore } from "@/store/modules/dataDict";
+import {
+  selectedDeptsEmployee,
+  getRuiHenProjectInfoPage,
+  getDevicesByDepts,
+  getRuiHenTaskDetail1,
+} from "@/api/ruihen";
+import { specifiedSimpleDepts } from "@/api";
+import { getDeptId } from "@/utils/auth";
+import DaTree from "@/components/da-tree/index.vue";
+
+const props = defineProps({
+  type: {
+    type: String,
+    default: "create",
+  },
+  id: {
+    type: Number,
+    default: null,
+  },
+});
+
+const dictStore = useDataDictStore();
+
+const isReadonly = computed(() => props.type === "detail");
+
+const createInitialForm = () => ({
+  id: "",
+  projectId: undefined,
+  wellName: "",
+  location: "",
+  technique: undefined,
+  workloadDesign: undefined,
+  workloadUnit: undefined,
+  deptIds: [],
+  deviceIds: [],
+  responsiblePerson: undefined,
+  remark: "",
+});
+
+const formRef = ref(null);
+const form = ref(createInitialForm());
+
+const rules = reactive({
+  projectId: {
+    rules: [{ required: true, errorMessage: "请选择合同" }],
+  },
+  wellName: {
+    rules: [{ required: true, errorMessage: "请输入井号" }],
+  },
+  location: {
+    rules: [{ required: true, errorMessage: "请输入施工地点" }],
+  },
+  technique: {
+    rules: [{ required: true, errorMessage: "请选择施工工艺" }],
+  },
+  workloadDesign: {
+    rules: [{ required: true, errorMessage: "请输入设计工作量" }],
+  },
+  workloadUnit: {
+    rules: [{ required: true, errorMessage: "请选择工作量单位" }],
+  },
+  deptIds: {
+    rules: [{ required: true, errorMessage: "请选择施工队伍" }],
+  },
+  deviceIds: {
+    rules: [{ required: true, errorMessage: "请选择施工设备" }],
+  },
+  responsiblePerson: {
+    rules: [{ required: true, errorMessage: "请选择责任人" }],
+  },
+});
+
+const validate = async () => {
+  return await formRef.value?.validate();
+};
+
+const buildSubmitData = () => {
+  return {
+    ...(form.value.id ? { id: form.value.id } : {}),
+    projectId: form.value.projectId,
+    wellName: form.value.wellName,
+    location: form.value.location,
+    technique: form.value.technique,
+    workloadDesign: form.value.workloadDesign,
+    workloadUnit: form.value.workloadUnit,
+    deptIds: form.value.deptIds,
+    deviceIds: form.value.deviceIds,
+    responsiblePerson: [form.value.responsiblePerson],
+    remark: form.value.remark,
+    platformWell: "0",
+  };
+};
+
+const defaultProps = computed(() => (label) => ({
+  inputBorder: false,
+  clearable: false,
+  placeholder: isReadonly.value ? " " : `请输入${label}`,
+  style: {
+    "text-align": "right",
+  },
+  styles: {
+    disableColor: "#fff",
+  },
+}));
+
+const contractPopup = ref(null);
+const contractPaging = ref(null);
+const contractList = ref([]);
+const contractSearchValue = ref("");
+const contractTempProjectId = ref("");
+const selectedContractLabel = ref("");
+
+const contractSearchInputStyles = reactive({
+  backgroundColor: "#f5f5f5",
+  color: "#333",
+});
+
+const contractDisplayText = computed(() => {
+  if (!form.value.projectId) return "";
+
+  const currentContract = contractList.value.find(
+    (item) => String(item.id) === String(form.value.projectId)
+  );
+
+  return selectedContractLabel.value || currentContract?.contractName || "";
+});
+
+const syncSelectedContractLabel = (list = []) => {
+  if (!form.value.projectId) return;
+
+  const currentContract = list.find(
+    (item) => String(item.id) === String(form.value.projectId)
+  );
+
+  if (currentContract?.contractName) {
+    selectedContractLabel.value = currentContract.contractName;
+  }
+};
+
+const queryContractList = async (pageNo, pageSize) => {
+  try {
+    const res = await getRuiHenProjectInfoPage({
+      deptId: getDeptId(),
+      pageNo,
+      pageSize,
+      ...(contractSearchValue.value
+        ? { contractName: contractSearchValue.value }
+        : {}),
+    });
+
+    const list = res?.data?.list || [];
+    const total = res?.data?.total || 0;
+
+    syncSelectedContractLabel(list);
+    contractPaging.value?.completeByTotal(list, total);
+  } catch (error) {
+    contractPaging.value?.complete(false);
+  }
+};
+
+const searchContractList = () => {
+  contractPaging.value?.reload();
+};
+
+const openContractPopup = () => {
+  contractSearchValue.value = "";
+  contractTempProjectId.value = form.value.projectId
+    ? String(form.value.projectId)
+    : "";
+  contractPopup.value?.open();
+
+  nextTick(() => {
+    contractPaging.value?.reload();
+  });
+};
+
+const closeContractPopup = () => {
+  contractPopup.value?.close();
+};
+
+const handleContractRadioChange = (event) => {
+  contractTempProjectId.value = event.detail.value;
+};
+
+const handleContractRowClick = (item) => {
+  contractTempProjectId.value = String(item.id);
+};
+
+const handleContractConfirm = () => {
+  if (!contractTempProjectId.value) {
+    uni.showToast({
+      title: "请选择合同",
+      icon: "none",
+    });
+    return;
+  }
+
+  const currentContract = contractList.value.find(
+    (item) => String(item.id) === contractTempProjectId.value
+  );
+  const fallbackProjectId = Number(contractTempProjectId.value);
+
+  form.value.projectId =
+    currentContract?.id ??
+    (Number.isNaN(fallbackProjectId)
+      ? contractTempProjectId.value
+      : fallbackProjectId);
+  selectedContractLabel.value =
+    currentContract?.contractName || selectedContractLabel.value;
+
+  closeContractPopup();
+};
+
+const techniqueOptions = ref([]);
+const workloadUnitOptions = ref([]);
+
+const loadDictOptions = async () => {
+  if (dictStore.dataDict.length <= 0) {
+    await dictStore.loadDataDictList();
+  }
+
+  techniqueOptions.value = dictStore
+    .getIntDictOptions("rq_iot_project_technology_rh")
+    .map((item) => ({
+      text: item.label,
+      value: item.value,
+    }));
+
+  workloadUnitOptions.value = dictStore
+    .getIntDictOptions("rq_iot_project_measure_unit")
+    .map((item) => ({
+      text: item.label,
+      value: item.value,
+    }));
+};
+
+const deptOptions = ref([]);
+const treeData = ref([]);
+const deptLoading = ref(false);
+
+const handleTree = (data, id, parentId, children) => {
+  if (!Array.isArray(data)) {
+    console.warn("data must be an array");
+    return [];
+  }
+  const config = {
+    id: id || "id",
+    parentId: parentId || "parentId",
+    childrenList: children || "children",
+  };
+
+  const childrenListMap = {};
+  const nodeIds = {};
+  const tree = [];
+
+  for (const d of data) {
+    const parentId = d[config.parentId];
+    if (childrenListMap[parentId] == null) {
+      childrenListMap[parentId] = [];
+    }
+    nodeIds[d[config.id]] = d;
+    childrenListMap[parentId].push(d);
+  }
+
+  for (const d of data) {
+    const parentId = d[config.parentId];
+    if (nodeIds[parentId] == null) {
+      tree.push(d);
+    }
+  }
+
+  for (const t of tree) {
+    adaptToChildrenList(t);
+  }
+
+  function adaptToChildrenList(o) {
+    if (childrenListMap[o[config.id]] !== null) {
+      o[config.childrenList] = childrenListMap[o[config.id]];
+    }
+    if (o[config.childrenList]) {
+      for (const c of o[config.childrenList]) {
+        adaptToChildrenList(c);
+      }
+    }
+  }
+
+  return tree;
+};
+
+async function loadDeptOptions() {
+  deptLoading.value = true;
+  try {
+    function sortTreeBySort(treeNodes) {
+      if (!treeNodes || !Array.isArray(treeNodes)) return treeNodes;
+      const sortedNodes = [...treeNodes].sort((a, b) => {
+        const sortA = a.sort != null ? a.sort : 999999;
+        const sortB = b.sort != null ? b.sort : 999999;
+        return sortA - sortB;
+      });
+
+      sortedNodes.forEach((node) => {
+        node.disabled = node.type !== "3";
+        if (node.children && Array.isArray(node.children)) {
+          node.children = sortTreeBySort(node.children);
+        }
+      });
+      return sortedNodes;
+    }
+
+    const depts = await specifiedSimpleDepts(getDeptId());
+
+    deptOptions.value = depts.data.map((item) => ({
+      text: item.name,
+      value: item.id,
+      raw: item,
+    }));
+    treeData.value = sortTreeBySort(handleTree(depts.data));
+  } catch (error) {
+  } finally {
+    deptLoading.value = false;
+  }
+}
+
+const popup = ref();
+
+const openPopup = () => {
+  popup.value.open();
+};
+
+const selectDeptId = ref("");
+
+function handleTreeChange(values) {
+  selectDeptId.value = values;
+}
+
+function handleConfirm() {
+  form.value.deptIds = [selectDeptId.value];
+  popup.value.close();
+  loadDeviceOptions();
+  loadUserOptions();
+}
+
+const deviceOptions = ref([]);
+const deviceLoading = ref(false);
+
+const loadDeviceOptions = async (init = false) => {
+  deviceOptions.value = [];
+  if (!init) form.value.deviceIds = [];
+
+  if (!form.value.deptIds || form.value.deptIds.length === 0) {
+    return;
+  }
+
+  deviceLoading.value = true;
+  try {
+    const res = await getDevicesByDepts({ deptIds: form.value.deptIds });
+    deviceOptions.value = res.data.map((item) => ({
+      text: `${item.deviceCode} ${item.deviceName}`,
+      value: item.id,
+      raw: item,
+    }));
+
+    form.value.deviceIds = deviceOptions.value.map((item) => item.value);
+  } finally {
+    deviceLoading.value = false;
+  }
+};
+
+const userOptions = ref([]);
+const userLoading = ref(false);
+
+const loadUserOptions = async (init = false) => {
+  userOptions.value = [];
+  if (!init) form.value.responsiblePerson = undefined;
+
+  if (!form.value.deptIds || form.value.deptIds.length === 0) {
+    return;
+  }
+
+  userLoading.value = true;
+  try {
+    const res = await selectedDeptsEmployee({
+      deptIds: form.value.deptIds,
+    });
+    userOptions.value = res.data.map((item) => ({
+      text: item.nickname,
+      value: item.id,
+      raw: item,
+    }));
+  } finally {
+    userLoading.value = false;
+  }
+};
+
+const loadDetail = async (id) => {
+  if (!id) return;
+
+  const res = await getRuiHenTaskDetail1({ id });
+
+  const data = res.data.list[0] || createInitialForm();
+
+  Object.assign(form.value, {
+    id: data.id,
+    projectId: data.projectId,
+    wellName: data.wellName,
+    location: data.location,
+    technique: Number(data.technique),
+    workloadDesign: data.workloadDesign,
+    workloadUnit: Number(data.workloadUnit),
+    deptIds: data.deptIds || [],
+    deviceIds: data.deviceIds || [],
+    responsiblePerson: data.responsiblePerson
+      ? data.responsiblePerson[0]
+      : undefined,
+    remark: data.remark,
+  });
+
+  selectedContractLabel.value = data.contractName || "";
+
+  loadDeviceOptions(true);
+  loadUserOptions(true);
+};
+
+watch(
+  () => props.id,
+  async (newId) => {
+    if (props.type !== "create" && newId) {
+      await loadDetail(newId);
+    }
+  },
+  { immediate: true }
+);
+
+watch(
+  () => form.value.projectId,
+  (value) => {
+    if (!value) {
+      selectedContractLabel.value = "";
+      contractTempProjectId.value = "";
+    }
+  }
+);
+
+onMounted(async () => {
+  await Promise.all([loadDictOptions(), loadDeptOptions()]);
+});
+
+defineExpose({
+  form,
+  formRef,
+  validate,
+  buildSubmitData,
+});
+</script>
+
+<template>
+  <view class="content">
+    <uni-forms
+      ref="formRef"
+      labelWidth="auto"
+      :model="form"
+      :rules="rules"
+      validateTrigger="submit"
+      err-show-type="toast">
+      <uni-forms-item label="合同" name="projectId" required>
+        <view class="select-with-button">
+          <view
+            class="popup-select-value"
+            :class="{ 'popup-select-placeholder': !contractDisplayText }"
+            @click="!isReadonly && openContractPopup()">
+            {{ contractDisplayText || "请选择合同" }}
+          </view>
+          <button
+            v-if="!isReadonly"
+            class="popup-button"
+            type="primary"
+            size="mini"
+            @click="openContractPopup">
+            选择
+          </button>
+        </view>
+      </uni-forms-item>
+      <uni-forms-item label="井号" name="wellName" required>
+        <uni-easyinput
+          v-bind="defaultProps('井号')"
+          v-model="form.wellName"
+          :disabled="isReadonly" />
+      </uni-forms-item>
+      <uni-forms-item label="施工地点" name="location" required>
+        <uni-easyinput
+          v-bind="defaultProps('施工地点')"
+          v-model="form.location"
+          :disabled="isReadonly" />
+      </uni-forms-item>
+      <uni-forms-item label="施工工艺" name="technique" required>
+        <uni-data-select
+          :clear="true"
+          align="right"
+          placeholder="请选择施工工艺"
+          :localdata="techniqueOptions"
+          placement="bottom"
+          hideRight
+          :disabled="isReadonly"
+          v-model="form.technique" />
+      </uni-forms-item>
+      <uni-forms-item label="设计工作量" name="workloadDesign" required>
+        <uni-easyinput
+          type="number"
+          v-bind="defaultProps('设计工作量')"
+          v-model.number="form.workloadDesign"
+          :disabled="isReadonly" />
+      </uni-forms-item>
+      <uni-forms-item label="工作量单位" name="workloadUnit" required>
+        <uni-data-select
+          :clear="true"
+          align="right"
+          placeholder="请选择工作量单位"
+          :localdata="workloadUnitOptions"
+          placement="bottom"
+          hideRight
+          :disabled="isReadonly"
+          v-model="form.workloadUnit" />
+      </uni-forms-item>
+      <uni-forms-item label="施工队伍" name="deptIds" required>
+        <view class="select-with-button">
+          <uni-data-select
+            :clear="true"
+            align="right"
+            placeholder="请选择施工队伍"
+            :localdata="deptOptions"
+            placement="bottom"
+            hideRight
+            :disabled="true"
+            multiple
+            v-model="form.deptIds" />
+          <button
+            v-if="!isReadonly"
+            class="popup-button"
+            type="primary"
+            size="mini"
+            :disabled="deptLoading"
+            @click="openPopup">
+            选择
+          </button>
+        </view>
+      </uni-forms-item>
+      <uni-forms-item label="施工设备" name="deviceIds" required>
+        <uni-data-select
+          :clear="true"
+          align="right"
+          placeholder="请选择施工设备"
+          :localdata="deviceOptions"
+          placement="bottom"
+          hideRight
+          multiple
+          :disabled="isReadonly || deviceLoading"
+          v-model="form.deviceIds">
+          <template #selected="{ selectedItems }">
+            <view class="device-selected-box">
+              <view v-if="selectedItems.length" class="device-selected-list">
+                <view
+                  v-for="item in selectedItems"
+                  :key="item.value"
+                  class="device-selected-item">
+                  {{ item.text }}
+                </view>
+              </view>
+              <view v-else class="device-selected-placeholder">
+                请选择施工设备
+              </view>
+            </view>
+          </template>
+        </uni-data-select>
+      </uni-forms-item>
+      <uni-forms-item label="责任人" name="responsiblePerson" required>
+        <uni-data-select
+          :clear="true"
+          align="right"
+          placeholder="请选择责任人"
+          :localdata="userOptions"
+          placement="bottom"
+          hideRight
+          :disabled="isReadonly || userLoading"
+          v-model="form.responsiblePerson" />
+      </uni-forms-item>
+      <uni-forms-item label="备注" name="remark">
+        <uni-easyinput
+          type="textarea"
+          autoHeight
+          v-bind="defaultProps('备注')"
+          v-model="form.remark"
+          :disabled="isReadonly"
+          :maxlength="1000" />
+      </uni-forms-item>
+    </uni-forms>
+  </view>
+
+  <uni-popup
+    ref="contractPopup"
+    type="bottom"
+    :is-mask-click="false"
+    border-radius="10px 10px 0 0">
+    <z-paging
+      ref="contractPaging"
+      v-model="contractList"
+      class="contract-popup-paging"
+      style="top: 140px"
+      :default-page-size="20"
+      @query="queryContractList">
+      <template #top>
+        <view class="contract-popup-top">
+          <view class="contract-popup-header">
+            <text class="contract-popup-action" @click="closeContractPopup">
+              取消
+            </text>
+            <text class="contract-popup-title">选择合同</text>
+            <text
+              class="contract-popup-action primary"
+              @click="handleContractConfirm">
+              确定
+            </text>
+          </view>
+          <view class="contract-search-row">
+            <uni-easyinput
+              v-model="contractSearchValue"
+              :inputBorder="false"
+              :styles="contractSearchInputStyles"
+              placeholder="请输入合同名称"
+              @confirm="searchContractList" />
+            <button
+              class="mini-btn contract-search-button"
+              type="primary"
+              size="mini"
+              @click="searchContractList">
+              搜索
+            </button>
+          </view>
+        </view>
+      </template>
+
+      <radio-group @change="handleContractRadioChange">
+        <view class="contract-popup-list">
+          <view
+            v-for="item in contractList"
+            :key="item.id"
+            class="contract-popup-item"
+            :class="{ active: String(item.id) === contractTempProjectId }"
+            @click="handleContractRowClick(item)">
+            <view class="contract-popup-item-main">
+              <view class="contract-popup-item-name">
+                {{ item.contractName || "--" }}
+              </view>
+              <!-- <view
+                v-if="item.projectNo || item.contractCode"
+                class="contract-popup-item-desc">
+                {{ item.projectNo || item.contractCode }}
+              </view> -->
+            </view>
+            <radio
+              :value="String(item.id)"
+              :checked="String(item.id) === contractTempProjectId"
+              color="#2979ff" />
+          </view>
+        </view>
+      </radio-group>
+    </z-paging>
+  </uni-popup>
+
+  <uni-popup ref="popup" type="bottom">
+    <view class="popup-content">
+      <view class="tree">
+        <DaTree
+          :data="treeData"
+          labelField="name"
+          valueField="id"
+          disabledField="disabled"
+          defaultExpandAll
+          checkedDisabled
+          :defaultCheckedKeys="form.deptIds[0]"
+          @change="handleTreeChange"></DaTree>
+      </view>
+      <button class="mini-btn" type="primary" @click="handleConfirm">
+        确定
+      </button>
+    </view>
+  </uni-popup>
+</template>
+
+<style lang="scss" scoped>
+.content {
+  background-color: #fff;
+  padding: 16px;
+  border-radius: 8px;
+  box-sizing: border-box;
+}
+
+.uni-forms {
+  margin-top: 10px;
+}
+
+:deep(.uni-forms-item) {
+  display: flex;
+  align-items: center;
+  margin-bottom: 6px;
+  border-bottom: 1px dashed #cacccf;
+}
+
+:deep(.uni-forms-item__content) {
+  text-align: right;
+}
+
+:deep(.uni-forms-item__label) {
+  height: 44px;
+  font-weight: 500;
+  font-size: 14px;
+  color: #333 !important;
+  width: max-content !important;
+}
+
+:deep(.uni-select) {
+  border: none;
+  text-align: right;
+  padding-right: 10px;
+}
+
+:deep(.uniui-bottom:before) {
+  content: "\e6b5" !important;
+  font-size: 16px !important;
+}
+
+:deep(.uni-easyinput__content-textarea) {
+  min-height: inherit;
+  margin: 10px;
+}
+
+:deep(.uni-textarea-textarea:disabled),
+:deep(.uni-input-input:disabled),
+:deep(.is-disabled) {
+  color: #333 !important;
+}
+
+:deep(.uni-select--disabled) {
+  background-color: #fff;
+}
+
+.select-with-button {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  gap: 10px;
+  width: 100%;
+}
+
+.readonly-input {
+  flex: 1;
+}
+
+.popup-select-value {
+  flex: 1;
+  min-height: 32px;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  color: #333;
+  font-size: 14px;
+  line-height: 1.4;
+  text-align: right;
+  word-break: break-all;
+}
+
+.popup-select-placeholder {
+  color: #999;
+}
+
+.contract-popup-paging {
+  background-color: #fff;
+  border-radius: 10px 10px 0 0;
+}
+
+.contract-popup-top {
+  padding: 16px;
+  padding-bottom: 10px;
+  background-color: #fff;
+}
+
+.contract-popup-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 16px;
+  font-size: 15px;
+}
+
+.contract-popup-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #333;
+}
+
+.contract-popup-action {
+  color: #666;
+}
+
+.contract-popup-action.primary {
+  color: #2979ff;
+}
+
+.contract-search-row {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.contract-search-button {
+  width: 72px;
+  margin: 0;
+  flex-shrink: 0;
+}
+
+.contract-popup-list {
+  padding: 0 16px 16px;
+  background-color: #fff;
+}
+
+.contract-popup-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  padding: 14px 0;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.contract-popup-item.active {
+  color: #2979ff;
+}
+
+.contract-popup-item-main {
+  flex: 1;
+  min-width: 0;
+}
+
+.contract-popup-item-name {
+  font-size: 14px;
+  line-height: 1.5;
+  color: inherit;
+  word-break: break-all;
+}
+
+.contract-popup-item-desc {
+  margin-top: 4px;
+  font-size: 12px;
+  color: #999;
+}
+
+.device-selected-box {
+  width: 100%;
+  min-height: 24px;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+}
+
+.device-selected-list {
+  width: 100%;
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: flex-end;
+  gap: 6px;
+}
+
+.device-selected-item {
+  max-width: 100%;
+  padding: 4px 8px;
+  border-radius: 4px;
+  background: #f3f5f9;
+  color: #333;
+  font-size: 12px;
+  line-height: 1.4;
+  word-break: break-all;
+}
+
+.device-selected-placeholder {
+  color: #999;
+  font-size: 14px;
+}
+
+:deep(.popup-button) {
+  width: 62px;
+  margin: 0;
+  flex-shrink: 0;
+}
+
+.popup-content {
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  padding: 15px;
+  height: 500px;
+  background-color: #fff;
+}
+
+.tree {
+  flex: 1;
+  max-height: 420px;
+}
+
+.popup-content button {
+  width: 100%;
+  height: 44px;
+}
+
+:deep(.uni-select__selector-item) {
+  padding: 0;
+  width: 100%;
+  overflow-x: auto;
+  overflow-y: hidden;
+  white-space: nowrap;
+  -webkit-overflow-scrolling: touch;
+
+  uni-text {
+    padding: 0 10px;
+  }
+}
+
+:deep(.uni-select__selector-item uni-text),
+:deep(.uni-select__selector-item uni-text span) {
+  display: inline-block;
+  white-space: nowrap;
+  min-width: max-content;
+}
+</style>

+ 75 - 0
pages/ruihen-task/create.vue

@@ -0,0 +1,75 @@
+<script setup>
+import { ref } from "vue";
+import { createRuiHenTask } from "@/api/ruihen";
+import Form from "./components/form.vue";
+
+const formRef = ref(null);
+const formLoading = ref(false);
+
+const submitForm = async () => {
+  if (!formRef.value) return;
+
+  try {
+    await formRef.value.validate();
+    formLoading.value = true;
+
+    await createRuiHenTask({ taskList: [formRef.value.buildSubmitData()] });
+
+    uni.showToast({
+      title: "新增成功",
+      icon: "success",
+    });
+
+    setTimeout(() => {
+      uni.navigateBack();
+    }, 500);
+  } catch (error) {
+    console.log("submit create task error", error);
+  } finally {
+    formLoading.value = false;
+  }
+};
+</script>
+
+<template>
+  <view class="page">
+    <scroll-view scroll-y="true" class="segmented-content">
+      <!-- 复用任务表单组件 -->
+      <Form ref="formRef" type="create"></Form>
+    </scroll-view>
+    <view class="segmented-footer">
+      <view class="footer-btn">
+        <!-- 新增页底部确认按钮 -->
+        <button
+          :loading="formLoading"
+          class="mini-btn"
+          type="primary"
+          @click="submitForm">
+          确定
+        </button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+@import "@/style/work-order-segmented.scss";
+
+.page {
+  padding-bottom: 0;
+}
+
+.footer-btn {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  padding: 0 16px;
+  height: 100%;
+}
+
+:deep(.mini-btn) {
+  height: 38px !important;
+  font-size: 16px !important;
+  margin: 0;
+}
+</style>

+ 30 - 0
pages/ruihen-task/detail.vue

@@ -0,0 +1,30 @@
+<script setup>
+import { ref } from "vue";
+import { onLoad } from "@dcloudio/uni-app";
+import Form from "./components/form.vue";
+
+// 当前查看的任务 id
+const id = ref(null);
+
+// 从路由参数读取详情 id
+onLoad((options) => {
+  id.value = Number(options.id) || null;
+});
+</script>
+
+<template>
+  <view class="page">
+    <scroll-view scroll-y="true" style="height: 100%">
+      <!-- 详情页复用同一表单组件,只读展示 -->
+      <Form type="detail" :id="id"></Form>
+    </scroll-view>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+@import "@/style/work-order-segmented.scss";
+
+.page {
+  padding-bottom: 0;
+}
+</style>

+ 81 - 0
pages/ruihen-task/edit.vue

@@ -0,0 +1,81 @@
+<script setup>
+import { ref } from "vue";
+import { onLoad } from "@dcloudio/uni-app";
+import { updateRuiHenTask } from "@/api/ruihen";
+import Form from "./components/form.vue";
+
+const formRef = ref(null);
+const formLoading = ref(false);
+const id = ref(null);
+
+onLoad((options) => {
+  id.value = Number(options.id) || null;
+});
+
+const submitForm = async () => {
+  if (!formRef.value) return;
+
+  try {
+    await formRef.value.validate();
+    formLoading.value = true;
+
+    await updateRuiHenTask({ taskList: [formRef.value.buildSubmitData()] });
+
+    uni.showToast({
+      title: "修改成功",
+      icon: "success",
+    });
+
+    setTimeout(() => {
+      uni.navigateBack();
+    }, 500);
+  } catch (error) {
+    console.log("submit edit task error", error);
+  } finally {
+    formLoading.value = false;
+  }
+};
+</script>
+
+<template>
+  <view class="page">
+    <scroll-view scroll-y="true" class="segmented-content">
+      <!-- 复用任务表单组件,传入编辑模式和任务 id -->
+      <Form ref="formRef" type="edit" :id="id"></Form>
+    </scroll-view>
+    <view class="segmented-footer">
+      <view class="footer-btn">
+        <!-- 编辑页底部确认按钮 -->
+        <button
+          :loading="formLoading"
+          class="mini-btn"
+          type="primary"
+          @click="submitForm">
+          确定
+        </button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+@import "@/style/work-order-segmented.scss";
+
+.page {
+  padding-bottom: 0;
+}
+
+.footer-btn {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  padding: 0 16px;
+  height: 100%;
+}
+
+:deep(.mini-btn) {
+  height: 38px !important;
+  font-size: 16px !important;
+  margin: 0;
+}
+</style>

+ 284 - 0
pages/ruihen-task/index.vue

@@ -0,0 +1,284 @@
+<script setup>
+import { onShow } from "@dcloudio/uni-app";
+import { nextTick, reactive, ref } from "vue";
+import dayjs from "dayjs";
+import { getRuiHenTaskList } from "@/api/ruihen";
+
+// 搜索关键词与分页实例
+const searchKey = ref("");
+const paging = ref(null);
+const dataList = ref([]);
+
+// 搜索框样式
+const placeholderStyle = ref("color:#797979;font-weight:500;font-size:16px");
+const inputStyles = reactive({
+  backgroundColor: "#FFFFFF",
+  color: "#797979",
+});
+
+// 列表查询:搜索条件统一走 searchKey
+const queryList = (pageNo, pageSize) => {
+  getRuiHenTaskList({
+    pageNo,
+    pageSize,
+    searchKey: searchKey.value,
+  })
+    .then((res) => {
+      paging.value.complete(res.data?.list || []);
+    })
+    .catch(() => {
+      paging.value.complete(false);
+    });
+};
+
+// 触发 z-paging 重新加载
+const searchList = () => {
+  paging.value?.reload();
+};
+
+// 跳转新增页
+const handleCreateTask = () => {
+  uni.navigateTo({
+    url: "/pages/ruihen-task/create",
+  });
+};
+
+// 跳转详情页
+const handleView = (item) => {
+  uni.navigateTo({
+    url: "/pages/ruihen-task/detail?id=" + item.id,
+  });
+};
+
+// 跳转编辑页
+const handleEdit = (item) => {
+  uni.navigateTo({
+    url: "/pages/ruihen-task/edit?id=" + item.id,
+  });
+};
+
+// 同时兼容秒级和毫秒级时间戳
+const normalizeTimestamp = (time) => {
+  if (!time) return null;
+  const value = Number(time);
+  if (Number.isNaN(value)) return time;
+  return value < 1000000000000 ? value * 1000 : value;
+};
+
+// 创建时间格式化
+const formatTime = (time) => {
+  const value = normalizeTimestamp(time);
+  return value ? dayjs(value).format("YYYY-MM-DD HH:mm:ss") : "——";
+};
+
+// 施工队伍兼容数组和字符串两种返回结构
+const formatDeptNames = (deptNames) => {
+  if (Array.isArray(deptNames)) {
+    return deptNames.filter(Boolean).join("、") || "——";
+  }
+  return deptNames || "——";
+};
+
+onShow(() => {
+  nextTick(() => {
+    searchList();
+  });
+});
+</script>
+
+<template>
+  <z-paging class="page" ref="paging" v-model="dataList" @query="queryList">
+    <template #top>
+      <view class="top">
+        <!-- 搜索区 -->
+        <view class="search-row">
+          <uni-easyinput
+            v-model="searchKey"
+            :styles="inputStyles"
+            :placeholderStyle="placeholderStyle"
+            :placeholder="$t('operation.searchText')"
+            @confirm="searchList" />
+          <button
+            class="mini-btn"
+            type="primary"
+            size="mini"
+            @click="searchList">
+            {{ $t("operation.search") }}
+          </button>
+        </view>
+        <!-- 查询区下方新增按钮 -->
+        <button
+          class="create-btn"
+          style="width: 100%"
+          type="primary"
+          size="mini"
+          @click="handleCreateTask">
+          新增任务
+        </button>
+      </view>
+    </template>
+
+    <view class="list">
+      <!-- 任务卡片列表 -->
+      <view
+        class="item"
+        v-for="(item, index) in dataList"
+        :key="item.id || index">
+        <view class="header">
+          <!-- 右上角状态固定统一蓝色,仅展示 statusLabel -->
+          <span class="contract-name"
+            >合同名称:{{ item.contractName || "——" }}</span
+          >
+          <span class="status-tag">{{ item.statusLabel || "——" }}</span>
+        </view>
+
+        <view class="content">
+          <view class="content-item">
+            <span class="label">井号:</span>
+            <span>{{ item.wellName || "——" }}</span>
+          </view>
+          <view class="content-item">
+            <span class="label">施工地点:</span>
+            <span>{{ item.location || "——" }}</span>
+          </view>
+          <view class="content-item">
+            <span class="label">施工队伍:</span>
+            <span>{{ formatDeptNames(item.deptNames) }}</span>
+          </view>
+          <view class="content-item">
+            <span class="label">创建时间:</span>
+            <span>{{ formatTime(item.createTime) }}</span>
+          </view>
+        </view>
+
+        <view class="footer">
+          <!-- 列表操作按钮 -->
+          <button
+            class="button"
+            size="mini"
+            type="primary"
+            plain="true"
+            @click="handleView(item)">
+            {{ $t("operation.view") }}
+          </button>
+          <button
+            class="button"
+            size="mini"
+            type="primary"
+            @click="handleEdit(item)">
+            {{ $t("operation.edit") }}
+          </button>
+        </view>
+      </view>
+    </view>
+  </z-paging>
+</template>
+
+<style scoped>
+.page {
+  padding: 10px;
+}
+
+.top {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  background: #f3f5f9;
+  padding: 10px 0;
+}
+
+.search-row {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.search-row :deep(.uni-easyinput) {
+  flex: 1;
+}
+
+:deep(.mini-btn) {
+  height: 38px !important;
+  font-size: 16px !important;
+  margin: 0;
+}
+
+.create-btn {
+  margin: 0;
+  align-self: flex-start;
+}
+
+.list {
+  margin-top: 16px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.item {
+  background-color: #fff;
+  padding: 12px;
+  border-radius: 8px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.header {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 12px;
+}
+
+.contract-name {
+  flex: 1;
+  font-size: 16px;
+  font-weight: 600;
+  color: #1f2329;
+  word-break: break-all;
+}
+
+.status-tag {
+  flex-shrink: 0;
+  display: inline-flex;
+  align-items: center;
+  padding: 5px 10px;
+  border-radius: 4px;
+  font-size: 14px;
+  font-weight: 500;
+  color: #1677ff;
+  background: rgba(22, 119, 255, 0.12);
+}
+
+.content {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.content-item {
+  font-size: 14px;
+  font-weight: 400;
+  color: #4e5969;
+  word-break: break-all;
+}
+
+.label {
+  display: inline-block;
+  width: 76px;
+  font-weight: 500;
+  color: #1f2329;
+}
+
+.footer {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  gap: 12px;
+}
+
+.button {
+  margin: 0;
+}
+</style>