zhangwencui
9 小时以前 eea8cb3bbe6379755410dffb774bd14e389612c2
应急预案查阅模块开发
已添加4个文件
已修改2个文件
1537 ■■■■■ 文件已修改
src/api/safeProduction/emergencyPlanReview.js 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/safeProduction/emergencyPlanReview/detail.vue 724 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/safeProduction/emergencyPlanReview/index.vue 373 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/safeProduction/emergencyPlanReview/view.vue 372 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/safeProduction/emergencyPlanReview.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
// åº”急预案审核页面接口
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢
export function safeContingencyPlanListPage(query) {
  return request({
    url: "/safeContingencyPlan/page",
    method: "get",
    params: query,
  });
}
// æ–°å¢žåº”急预案
export function safeContingencyPlanAdd(query) {
    return request({
        url: '/safeContingencyPlan',
        method: 'post',
        data: query
    })
}
// ä¿®æ”¹åº”急预案
export function safeContingencyPlanUpdate(query) {
    return request({
        url: '/safeContingencyPlan',
        method: 'put',
        data: query
    })
}
// åˆ é™¤åº”急预案
export function safeContingencyPlanDel(ids) {
    return request({
        url: '/safeContingencyPlan/' + ids,
        method: 'delete',
        data: ids
    })
}
src/pages.json
@@ -772,6 +772,27 @@
        "navigationBarTitleText": "危险物料详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/safeProduction/emergencyPlanReview/index",
      "style": {
        "navigationBarTitleText": "应急预案审核",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/safeProduction/emergencyPlanReview/detail",
      "style": {
        "navigationBarTitleText": "应急预案详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/safeProduction/emergencyPlanReview/view",
      "style": {
        "navigationBarTitleText": "应急预案详情",
        "navigationStyle": "custom"
      }
    }
  ],
  "subPackages": [
src/pages/index.vue
@@ -323,6 +323,10 @@
      icon: "/static/images/icon/guzhangfenxi@2x.png",
      label: "危险物料",
    },
    {
      icon: "/static/images/icon/guzhangfenxi@2x.png",
      label: "应急预案",
    },
  ]);
  // ååŒåŠžå…¬åŠŸèƒ½æ•°æ®
  const collaborationItems = reactive([
@@ -706,6 +710,11 @@
          url: "/pages/safeProduction/hazardousMaterialsControl/index",
        });
        break;
      case "应急预案":
        uni.navigateTo({
          url: "/pages/safeProduction/emergencyPlanReview/index",
        });
        break;
      default:
        uni.showToast({
          title: `点击了${item.label}`,
src/pages/safeProduction/emergencyPlanReview/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,724 @@
<template>
  <view class="emergency-plan-detail">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader :title="isEdit ? '编辑应急预案' : '新增应急预案'"
                @back="goBack" />
    <!-- è¡¨å•区域 -->
    <u-form :model="form"
            label-width="110"
            :rules="rules"
            ref="formRef">
      <!-- åº”急预案编码 -->
      <u-form-item label="应急预案编码"
                   border-bottom
                   required
                   prop="planCode">
        <up-input v-model="form.planCode"
                  placeholder="请输入应急预案编码"
                  clearable />
      </u-form-item>
      <!-- åº”急预案名称 -->
      <u-form-item label="应急预案名称"
                   required
                   border-bottom
                   prop="planName">
        <up-input v-model="form.planName"
                  placeholder="请输入应急预案名称"
                  clearable />
      </u-form-item>
      <!-- å‘布生效时间 -->
      <u-form-item label="发布生效时间"
                   required
                   border-bottom
                   prop="publishTime">
        <up-input v-model="form.publishTime"
                  placeholder="请选择发布生效时间"
                  readonly
                  @click="showTime = true" />
        <template #right>
          <up-icon name="arrow-right"
                   @click="showTime = true"></up-icon>
        </template>
      </u-form-item>
      <!-- é¢„案类型 -->
      <u-form-item label="预案类型"
                   prop="planType"
                   required
                   border-bottom>
        <u-input v-model="emergencyPlanTypeLabel"
                 placeholder="请选择预案类型"
                 @click="showPlanTypeActionSheet = true"
                 readonly />
        <template #right>
          <up-icon name="arrow-right"
                   @click="showPlanTypeActionSheet = true"></up-icon>
        </template>
      </u-form-item>
      <u-form-item label="核心责任人"
                   prop="coreResponsorUserId"
                   required
                   border-bottom>
        <u-input v-model="form.coreResponsorUserName"
                 placeholder="请选择核心责任人"
                 @click="showUserActionSheet = true"
                 readonly />
        <template #right>
          <up-icon name="arrow-right"
                   @click="showUserActionSheet = true"></up-icon>
        </template>
      </u-form-item>
      <!-- å¤‡æ³¨ -->
      <u-form-item label="备注"
                   prop="remark">
        <up-input v-model="form.remark"
                  placeholder="请输入备注"
                  type="textarea"
                  rows="3"
                  clearable />
      </u-form-item>
      <!-- é€‚用范围 -->
      <u-form-item label="适用范围"
                   required
                   prop="applyScope">
        <view class="checkbox-group">
          <u-checkbox-group v-model="form.applyScope"
                            @change="handleApplyScopeChange">
            <u-checkbox shape="circle"
                        size="32rpx"
                        class="checkbox-item"
                        v-for="(item, index) in applyScopeOptions"
                        :key="index"
                        :label="item.label"
                        :name="item.value">
            </u-checkbox>
          </u-checkbox-group>
        </view>
      </u-form-item>
      <!-- åº”急处置步骤 -->
      <view class="exec-steps-container">
        <view class="steps-header">
          <text class="steps-title">处置步骤列表</text>
          <text class="steps-count">共 {{ execStepsList.length }} ä¸ªæ­¥éª¤</text>
        </view>
        <view class="steps-list">
          <view v-for="(step, index) in execStepsList"
                :key="index"
                class="exec-step-item">
            <view class="delete-btn"
                  @click="removeExecStep(index)">
              <u-icon name="close"
                      color="#fff"
                      size="16" />
            </view>
            <view class="step-number">
              {{ index + 1 }}
            </view>
            <view class="step-content">
              <view class="step-row">
                <text class="step-label">步骤名称:</text>
                <u-textarea v-model="step.step"
                            placeholder="请输入步骤名称"
                            clearable
                            border-bottom
                            class="step-input" />
              </view>
              <view class="step-row">
                <text class="step-label">处置措施:</text>
                <u-textarea v-model="step.description"
                            placeholder="请输入具体的应急处置措施"
                            type="textarea"
                            clearable
                            class="step-textarea" />
              </view>
            </view>
          </view>
        </view>
        <u-button type="primary"
                  @click="addExecStep"
                  class="add-step-btn">
          <text>添加步骤</text>
        </u-button>
      </view>
    </u-form>
    <!-- å‘布生效时间选择器 -->
    <up-datetime-picker :show="showTime"
                        v-model="currentTime"
                        @confirm="handleDateConfirm"
                        @cancel="showTime = false"
                        mode="date" />
    <!--预案类型选择器 -->
    <up-action-sheet :show="showPlanTypeActionSheet"
                     :actions="emergencyPlanTypeOptions"
                     @select="handlePlanTypeConfirm"
                     title="选择预案类型" />
    <!--核心责任人选择器 -->
    <up-action-sheet :show="showUserActionSheet"
                     :actions="userList"
                     @select="handleUserConfirm"
                     title="选择核心责任人" />
  </view>
  <!-- åº•部按钮 -->
  <view class="bottom-buttons">
    <u-button type="default"
              size="default"
              @click="goBack"
              class="bottom-btn">
      å–消
    </u-button>
    <u-button type="primary"
              size="default"
              @click="submitForm"
              class="bottom-btn">
      ä¿å­˜
    </u-button>
  </view>
</template>
<script setup>
  import { ref, onMounted, computed } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import {
    safeContingencyPlanAdd,
    safeContingencyPlanUpdate,
  } from "@/api/safeProduction/emergencyPlanReview";
  import { userListNoPage } from "@/api/system/user";
  import { useDict } from "@/utils/dict";
  import dayjs from "dayjs";
  // æ›¿æ¢ toast æ–¹æ³•
  defineOptions({ name: "emergency-plan-detail" });
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  // è¡¨å•引用
  const formRef = ref();
  // è¡¨å•数据
  const form = ref({
    id: "",
    planCode: "",
    planName: "",
    publishTime: "",
    planType: "",
    coreResponsorUserId: "",
    coreResponsorUserName: "",
    remark: "",
    applyScope: [],
    execSteps: "",
  });
  // åº”急处置步骤列表
  const execStepsList = ref([]);
  // æ—¥æœŸèŒƒå›´
  const minDate = new Date("2000-01-01");
  const maxDate = new Date("2030-12-31");
  const currentTime = ref(Date.now());
  // ç”¨æˆ·åˆ—表
  const userList = ref([]);
  // åº”急预案类型选项
  const { emergency_plan_type } = useDict("emergency_plan_type");
  const emergencyPlanTypeOptions = computed(() => {
    return (
      emergency_plan_type?.value.map(item => ({
        value: item.value,
        name: item.label,
      })) || []
    );
  });
  // åº”急预案类型标签
  const emergencyPlanTypeLabel = ref("");
  // é€‚用范围选项
  const applyScopeOptions = [
    { value: "all", label: "全体员工" },
    { value: "manager", label: "管理层" },
    { value: "hr", label: "人事部门" },
    { value: "finance", label: "财务部门" },
    { value: "tech", label: "技术部门" },
  ];
  // æ˜¯å¦ä¸ºç¼–辑模式
  const isEdit = ref(false);
  // ActionSheet æ˜¾ç¤ºçŠ¶æ€
  const showPlanTypeActionSheet = ref(false);
  const showUserActionSheet = ref(false);
  const showTime = ref(false);
  // åˆå§‹åŒ–数据
  const initData = () => {
    const emergencyPlan = uni.getStorageSync("emergencyPlan") || {};
    if (emergencyPlan.id) {
      // ç¼–辑模式
      isEdit.value = true;
      form.value = {
        id: emergencyPlan.id,
        planCode: emergencyPlan.planCode || "",
        planName: emergencyPlan.planName || "",
        publishTime: emergencyPlan.publishTime || "",
        planType: emergencyPlan.planType || "",
        coreResponsorUserId: emergencyPlan.coreResponsorUserId || "",
        coreResponsorUserName: emergencyPlan.coreResponsorUserName || "",
        remark: emergencyPlan.remark || "",
        applyScope: emergencyPlan.applyScope
          ? emergencyPlan.applyScope.split(",")
          : [],
        execSteps: emergencyPlan.execSteps || "",
      };
      currentTime.value = new Date(emergencyPlan.publishTime).getTime();
      // è®¾ç½®é¢„案类型标签
      const planTypeItem = emergencyPlanTypeOptions.value.find(
        item => item.value === emergencyPlan.planType
      );
      emergencyPlanTypeLabel.value = planTypeItem ? planTypeItem.name : "";
      console.log(form.value.applyScope, form.value.applyScope);
      // åˆå§‹åŒ–应急处置步骤
      initExecSteps(emergencyPlan.execSteps);
    } else {
      // æ–°å¢žæ¨¡å¼
      isEdit.value = false;
      form.value = {
        planCode: "",
        planName: "",
        publishTime: new Date().toISOString().split("T")[0],
        planType: "",
        coreResponsorUserId: "",
        coreResponsorUserName: "",
        remark: "",
        applyScope: [],
        execSteps: "",
      };
      emergencyPlanTypeLabel.value = "";
      execStepsList.value = [];
      addExecStep();
    }
  };
  const handleApplyScopeChange = e => {
    // form.value.applyScope = e;
    console.log(e, "e");
    console.log(form.value.applyScope, "form.value.applyScope");
  };
  // åˆå§‹åŒ–应急处置步骤
  const initExecSteps = execSteps => {
    if (execSteps) {
      try {
        execStepsList.value = JSON.parse(execSteps);
      } catch (e) {
        execStepsList.value = [];
      }
    } else {
      execStepsList.value = [];
    }
    if (execStepsList.value.length === 0) {
      addExecStep();
    }
  };
  // æ·»åŠ åº”æ€¥å¤„ç½®æ­¥éª¤
  const addExecStep = () => {
    const stepNumber = execStepsList.value.length + 1;
    execStepsList.value.push({
      step: `步骤${stepNumber}`,
      description: "",
    });
  };
  // åˆ é™¤åº”急处置步骤
  const removeExecStep = index => {
    if (execStepsList.value.length > 1) {
      execStepsList.value.splice(index, 1);
    } else {
      showToast("至少保留一个步骤");
    }
  };
  // èŽ·å–ç”¨æˆ·åˆ—è¡¨
  const getUserList = () => {
    userListNoPage()
      .then(res => {
        userList.value = res.data.map(item => ({
          value: item.userId,
          name: item.nickName,
        }));
      })
      .catch(() => {
        showToast("获取用户列表失败");
      });
  };
  // æ—¥æœŸé€‰æ‹©ç¡®è®¤
  const handleDateConfirm = e => {
    form.value.publishTime = dayjs(e.value).format("YYYY-MM-DD");
    showTime.value = false;
  };
  // é¢„案类型选择确认
  const handlePlanTypeConfirm = e => {
    form.value.planType = e.value;
    const selectedType = emergencyPlanTypeOptions.value.find(
      item => item.value === e.value
    );
    if (selectedType) {
      emergencyPlanTypeLabel.value = selectedType.name;
    }
    showPlanTypeActionSheet.value = false;
  };
  // ç”¨æˆ·é€‰æ‹©ç¡®è®¤
  const handleUserConfirm = e => {
    form.value.coreResponsorUserId = e.value;
    const selectedUser = userList.value.find(user => user.value === e.value);
    if (selectedUser) {
      form.value.coreResponsorUserName = selectedUser.name;
    }
    showUserActionSheet.value = false;
  };
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // è¡¨å•验证规则
  const rules = {
    planCode: [
      {
        required: true,
        message: "请输入应急预案编码",
        trigger: ["submit", "blur"],
      },
    ],
    planName: [
      {
        required: true,
        message: "请输入应急预案名称",
        trigger: ["submit", "blur"],
      },
    ],
    publishTime: [
      {
        required: true,
        message: "请选择发布生效时间",
        trigger: ["submit", "change"],
      },
    ],
    planType: [
      {
        required: true,
        message: "请选择预案类型",
        trigger: ["submit", "change"],
      },
    ],
    coreResponsorUserId: [
      {
        required: true,
        message: "请选择核心责任人",
        trigger: ["submit", "change"],
      },
    ],
    applyScope: [
      {
        required: true,
        message: "请选择适用范围",
        trigger: ["submit", "change"],
      },
    ],
  };
  // æäº¤è¡¨å•
  const submitForm = async () => {
    // éªŒè¯è¡¨å•必填项
    if (!formRef.value) return;
    const valid = await formRef.value.validate();
    if (!valid) {
      return;
    }
    // éªŒè¯åº”急处置步骤
    for (let i = 0; i < execStepsList.value.length; i++) {
      const step = execStepsList.value[i];
      if (!step.step || !step.step.trim()) {
        showToast(`第${i + 1}条步骤的"步骤"不能为空`);
        return;
      }
      if (!step.description || !step.description.trim()) {
        showToast(`第${i + 1}条步骤的"措施"不能为空`);
        return;
      }
    }
    // å°†åº”急处置步骤转换为JSON字符串
    form.value.execSteps = JSON.stringify(execStepsList.value);
    // å¤„理适用范围
    form.value.applyScope = form.value.applyScope.join(",");
    showLoadingToast("保存中...");
    try {
      if (isEdit.value) {
        // ç¼–辑模式
        const res = await safeContingencyPlanUpdate(form.value);
        if (res.code === 200) {
          showToast("更新成功");
          setTimeout(() => {
            uni.navigateBack();
          }, 1000);
        } else {
          showToast(res.msg || "更新失败");
        }
      } else {
        // æ–°å¢žæ¨¡å¼
        const res = await safeContingencyPlanAdd(form.value);
        if (res.code === 200) {
          showToast("添加成功");
          setTimeout(() => {
            uni.navigateBack();
          }, 1000);
        } else {
          showToast(res.msg || "添加失败");
        }
      }
    } catch (error) {
      console.error("提交失败:", error);
      showToast("提交失败,请重试");
    } finally {
      closeToast();
    }
  };
  // æ˜¾ç¤ºåŠ è½½æç¤º
  const showLoadingToast = message => {
    uni.showLoading({
      title: message,
      mask: true,
    });
  };
  // å…³é—­æç¤º
  const closeToast = () => {
    uni.hideLoading();
  };
  onMounted(() => {
    initData();
    getUserList();
  });
  onShow(() => {
    initData();
  });
</script>
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  .emergency-plan-detail {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 100px;
  }
  .form-section {
  }
  .checkbox-group {
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
  }
  .checkbox-item {
    margin-right: 16px;
  }
  .select-container {
    position: relative;
    width: 100%;
  }
  .select-container .up-input {
    width: 100%;
    border: 1px solid #e4e7ed;
    border-radius: 8px;
    padding: 12px 16px;
    background-color: #ffffff;
  }
  .exec-steps-container {
    padding: 20px;
    background-color: #fff;
  }
  .steps-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
    padding-bottom: 12px;
    border-bottom: 1px solid #e4e7ed;
  }
  .steps-title {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
  }
  .steps-count {
    font-size: 14px;
    color: #909399;
  }
  .steps-list {
    margin-bottom: 20px;
  }
  .exec-step-item {
    position: relative;
    display: flex;
    margin-bottom: 16px;
    padding: 16px;
    background-color: #ffffff;
    border: 1px solid #e4e7ed;
    border-radius: 8px;
    transition: all 0.3s ease;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  }
  .exec-step-item:hover {
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
    border-color: #409eff;
    transform: translateY(-1px);
  }
  .delete-btn {
    position: absolute;
    top: -25rpx;
    right: -25rpx;
    width: 50rpx;
    height: 50rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    font-size: 20px;
    border-radius: 50%;
    background-color: red;
    border: none;
    z-index: 10;
  }
  .delete-btn:hover {
    transform: scale(1.1);
    box-shadow: 0 3px 6px rgba(245, 108, 108, 0.4);
  }
  .step-number {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    margin-right: 16px;
    background-color: #ecf5ff;
    color: #409eff;
    font-size: 14px;
    font-weight: 600;
    border-radius: 50%;
    flex-shrink: 0;
  }
  .step-content {
    flex: 1;
    min-width: 0;
  }
  .step-row {
    display: flex;
    align-items: flex-start;
    margin-bottom: 12px;
  }
  .step-row:last-child {
    margin-bottom: 0;
  }
  .step-label {
    display: inline-block;
    width: 80px;
    font-size: 14px;
    color: #606266;
    margin-right: 12px;
    flex-shrink: 0;
    line-height: 36px;
  }
  .step-input {
    flex: 1;
    min-width: 0;
  }
  .step-input input {
    font-size: 14px;
    color: #303133;
  }
  .step-textarea {
    flex: 1;
    min-width: 0;
  }
  .step-textarea textarea {
    font-size: 14px;
    color: #303133;
    min-height: 80px;
    line-height: 1.5;
  }
  .add-step-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 44px;
    line-height: 44px;
    font-size: 14px;
    border-radius: 8px;
    transition: all 0.3s ease;
    gap: 8px;
  }
  .add-step-btn:hover {
    transform: translateY(-1px);
    box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
  }
  .add-step-btn text {
    font-size: 14px;
  }
  .bottom-buttons {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    padding: 16px 20px;
    background: #ffffff;
    border-top: 1px solid #f0f0f0;
    gap: 16px;
  }
  .bottom-btn {
    flex: 1;
  }
</style>
src/pages/safeProduction/emergencyPlanReview/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,373 @@
<template>
  <view class="emergency-plan-review">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="应急预案审核"
                @back="goBack" />
    <!-- æœç´¢å’Œç­›é€‰åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    placeholder="请输入应急预案名称"
                    v-model="searchForm.planName"
                    @change="searchChange"
                    clearable />
        </view>
        <view class="filter-button"
              @click="getList">
          <u-icon name="search"
                  size="24"
                  color="#999"></u-icon>
        </view>
      </view>
    </view>
    <!-- åº”急预案列表 -->
    <view class="ledger-list"
          v-if="planList.length > 0">
      <view v-for="(item, index) in planList"
            :key="index">
        <view class="ledger-item">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon">
                <up-icon name="file-text"
                         size="16"
                         color="#ffffff"></up-icon>
              </view>
              <text class="item-title">{{ item.planName }}</text>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details"
                @click="viewDetail(item)">
            <view class="detail-row">
              <text class="detail-label">预案编码</text>
              <text class="detail-value">{{ item.planCode || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">预案类型</text>
              <u-tag :type="getPlanTypeTagType(item.planType)">
                {{ emergencyPlanTypeLabel(item.planType) }}
              </u-tag>
            </view>
            <view class="detail-row">
              <text class="detail-label">发布生效时间</text>
              <text class="detail-value">{{ item.publishTime || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">核心责任人</text>
              <text class="detail-value">{{ item.coreResponsorUserName || '-' }}</text>
            </view>
            <view class="detail-row"
                  v-if="item.remark">
              <text class="detail-label">备注</text>
              <text class="detail-value">{{ item.remark }}</text>
            </view>
          </view>
          <!-- æŒ‰é’®åŒºåŸŸ -->
          <view class="action-buttons">
            <u-button type="primary"
                      size="small"
                      class="action-btn"
                      @click="editPlan(item)">
              ç¼–辑
            </u-button>
            <u-button type="info"
                      size="small"
                      class="action-btn"
                      @click="viewDetail(item)">
              æŸ¥çœ‹è¯¦æƒ…
            </u-button>
            <u-button type="error"
                      size="small"
                      class="action-btn"
                      @click="deletePlan(item)">
              åˆ é™¤
            </u-button>
          </view>
        </view>
      </view>
    </view>
    <view v-else
          class="no-data">
      <text>暂无应急预案</text>
    </view>
    <!-- æµ®åŠ¨æ–°å¢žæŒ‰é’® -->
    <view class="fab-button"
          @click="addPlan">
      <up-icon name="plus"
               size="24"
               color="#ffffff"></up-icon>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted, computed } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import {
    safeContingencyPlanListPage,
    safeContingencyPlanDel,
  } from "@/api/safeProduction/emergencyPlanReview";
  import { useDict } from "@/utils/dict";
  import useUserStore from "@/store/modules/user";
  // æ›¿æ¢ toast æ–¹æ³•
  defineOptions({ name: "emergency-plan-review-index" });
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  const userStore = useUserStore();
  // æœç´¢è¡¨å•
  const searchForm = ref({
    planName: "",
  });
  // åº”急预案数据
  const planList = ref([]);
  // åº”急预案类型选项
  const { emergency_plan_type } = useDict("emergency_plan_type");
  const emergencyPlanTypeOptions = computed(
    () => emergency_plan_type?.value || []
  );
  // èŽ·å–é¢„æ¡ˆç±»åž‹æ ‡ç­¾ç±»åž‹
  const getPlanTypeTagType = planType => {
    const typeMap = {
      emergency: "warning",
      fire: "danger",
      natural: "info",
      accident: "error",
    };
    return typeMap[planType] || "info";
  };
  // èŽ·å–é¢„æ¡ˆç±»åž‹æ ‡ç­¾æ–‡æœ¬
  const emergencyPlanTypeLabel = val => {
    const item = emergencyPlanTypeOptions.value.find(
      i => String(i.value) === String(val)
    );
    return item ? item.label : val;
  };
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  const searchChange = val => {
    searchForm.value.planName = val;
    getList();
  };
  // æŸ¥è¯¢åˆ—表
  const getList = () => {
    showLoadingToast("加载中...");
    const params = {
      current: -1,
      size: -1,
      ...searchForm.value,
    };
    safeContingencyPlanListPage(params)
      .then(res => {
        planList.value = res.records || res.data?.records || [];
        closeToast();
      })
      .catch(() => {
        closeToast();
        showToast("获取数据失败");
      });
  };
  // æ˜¾ç¤ºåŠ è½½æç¤º
  const showLoadingToast = message => {
    uni.showLoading({
      title: message,
      mask: true,
    });
  };
  // å…³é—­æç¤º
  const closeToast = () => {
    uni.hideLoading();
  };
  // æ–°å¢žåº”急预案
  const addPlan = () => {
    uni.setStorageSync("emergencyPlan", {});
    uni.navigateTo({
      url: "/pages/safeProduction/emergencyPlanReview/detail",
    });
  };
  // ç¼–辑应急预案
  const editPlan = item => {
    uni.setStorageSync("emergencyPlan", item);
    uni.navigateTo({
      url: "/pages/safeProduction/emergencyPlanReview/detail",
    });
  };
  // åˆ é™¤åº”急预案
  const deletePlan = item => {
    uni.showModal({
      title: "删除确认",
      content: `确定要删除该应急预案吗?`,
      success: res => {
        if (res.confirm) {
          deleteEmergencyPlan(item.id);
        }
      },
    });
  };
  // åˆ é™¤åº”急预案记录
  const deleteEmergencyPlan = id => {
    showLoadingToast("删除中...");
    safeContingencyPlanDel([id])
      .then(() => {
        closeToast();
        showToast("删除成功");
        getList();
      })
      .catch(() => {
        closeToast();
        showToast("删除失败");
      });
  };
  // æŸ¥çœ‹è¯¦æƒ…
  const viewDetail = item => {
    uni.setStorageSync("emergencyPlan", item);
    uni.navigateTo({
      url: "/pages/safeProduction/emergencyPlanReview/view",
    });
  };
  onMounted(() => {
    getList();
  });
  onShow(() => {
    getList();
  });
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  // é¡µé¢ç‰¹å®šçš„æ ·å¼è¦†ç›–
  .emergency-plan-review {
    min-height: 100vh;
    background: #f8f9fa;
    position: relative;
  }
  // ç‰¹å®šçš„图标样式
  .document-icon {
    background: #2979ff; // ä¸Žé”€å”®æ¨¡å—保持一致的背景色
  }
  // ç‰¹æœ‰æ ·å¼
  .detail-value {
    word-break: break-all; // ä¿ç•™é¡µé¢ç‰¹æœ‰çš„æ–‡æœ¬æ¢è¡Œæ ·å¼
  }
  // ç‰¹å®šçš„æµ®åŠ¨æŒ‰é’®æ ·å¼
  .fab-button {
    background: #2979ff; // ä¸Žé”€å”®æ¨¡å—保持一致的背景色
    box-shadow: 0 4px 16px rgba(41, 121, 255, 0.3); // ä¸Žé”€å”®æ¨¡å—保持一致的阴影效果
  }
  // æ“ä½œæŒ‰é’®æ ·å¼
  .action-buttons {
    display: flex;
    gap: 12px;
    padding: 0 0 16px 0;
    justify-content: space-between;
  }
  .action-btn {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
  }
  // åˆ—表容器样式
  .plan-list {
    padding: 20px;
  }
  // åˆ—表项样式
  .plan-item {
    background: #ffffff;
    border-radius: 12px;
    margin-bottom: 16px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
    padding: 0 16px;
    &:active {
      transform: scale(0.98);
      box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
    }
  }
  // é¡¹ç›®å¤´éƒ¨æ ·å¼
  .item-header {
    padding: 16px 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .item-left {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .item-title {
    font-size: 14px;
    color: #333;
    font-weight: 500;
  }
  // è¯¦æƒ…区域样式
  .item-details {
    padding: 16px 0;
  }
  .detail-row {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    margin-bottom: 8px;
    &:last-child {
      margin-bottom: 0;
    }
  }
  .detail-label {
    font-size: 12px;
    color: #777777;
    min-width: 60px;
  }
  .detail-value {
    font-size: 12px;
    color: #000000;
    text-align: right;
    flex: 1;
    margin-left: 16px;
  }
</style>
src/pages/safeProduction/emergencyPlanReview/view.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,372 @@
<template>
  <view class="emergency-plan-view">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="应急预案详情"
                @back="goBack" />
    <!-- è¯¦æƒ…内容 -->
    <view class="detail-content"
          v-if="currentPlan">
      <!-- åŸºæœ¬ä¿¡æ¯ -->
      <view class="info-section">
        <!-- <view class="section-title">
          <text class="title-text">{{ currentPlan.planName }}</text>
        </view> -->
        <view class="info-grid">
          <view class="info-item">
            <text class="info-label">应急预案编码</text>
            <text class="info-value">{{ currentPlan.planCode || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">应急预案名称</text>
            <text class="info-value">{{ currentPlan.planName || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">预案类型</text>
            <u-tag :type="getPlanTypeTagType(currentPlan.planType)">
              {{ emergencyPlanTypeLabel(currentPlan.planType) }}
            </u-tag>
          </view>
          <view class="info-item">
            <text class="info-label">发布生效时间</text>
            <text class="info-value">{{ currentPlan.publishTime || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">核心责任人</text>
            <text class="info-value">{{ currentPlan.coreResponsorUserName || '-' }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">备注</text>
            <text class="info-value">{{ currentPlan.remark || '-' }}</text>
          </view>
        </view>
      </view>
      <!-- é€‚用范围 -->
      <view class="scope-section">
        <view class="section-header">
          <text class="section-heading">适用范围</text>
        </view>
        <view class="scope-tags">
          <u-tag v-for="(scope, index) in applyScopeList"
                 :key="index"
                 type="primary"
                 size="small"
                 class="scope-tag">
            {{ scope }}
          </u-tag>
        </view>
      </view>
      <!-- åº”急处置步骤 -->
      <view class="steps-section">
        <view class="section-header">
          <text class="section-heading">应急处置步骤</text>
        </view>
        <view class="steps-list"
              v-if="execStepsList.length > 0">
          <view v-for="(step, index) in execStepsList"
                :key="index"
                class="step-item">
            <view class="step-number">{{ index + 1 }}</view>
            <view class="step-content">
              <text class="step-title">{{ step.step }}</text>
              <text class="step-description">{{ step.description }}</text>
            </view>
          </view>
        </view>
        <view class="no-steps"
              v-else>
          <text>暂无应急处置步骤</text>
        </view>
      </view>
    </view>
    <!-- ç©ºçŠ¶æ€ -->
    <view class="empty-state"
          v-else>
      <text>暂无应急预案信息</text>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted, computed } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import { useDict } from "@/utils/dict";
  // æ›¿æ¢ toast æ–¹æ³•
  defineOptions({ name: "emergency-plan-view" });
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  // å½“前应急预案
  const currentPlan = ref(null);
  // åº”急处置步骤列表
  const execStepsList = ref([]);
  // é€‚用范围列表
  const applyScopeList = ref([]);
  // åº”急预案类型选项
  const { emergency_plan_type } = useDict("emergency_plan_type");
  const emergencyPlanTypeOptions = computed(
    () => emergency_plan_type?.value || []
  );
  // èŽ·å–é¢„æ¡ˆç±»åž‹æ ‡ç­¾ç±»åž‹
  const getPlanTypeTagType = planType => {
    const typeMap = {
      emergency: "warning",
      fire: "danger",
      natural: "info",
      accident: "error",
    };
    return typeMap[planType] || "info";
  };
  // èŽ·å–é¢„æ¡ˆç±»åž‹æ ‡ç­¾æ–‡æœ¬
  const emergencyPlanTypeLabel = val => {
    const item = emergencyPlanTypeOptions.value.find(
      i => String(i.value) === String(val)
    );
    return item ? item.label : val;
  };
  // åˆå§‹åŒ–数据
  const initData = () => {
    const emergencyPlan = uni.getStorageSync("emergencyPlan") || {};
    if (emergencyPlan.id) {
      currentPlan.value = emergencyPlan;
      // å¤„理适用范围
      if (emergencyPlan.applyScope) {
        const scopes = emergencyPlan.applyScope.split(",");
        applyScopeList.value = scopes.map(scope => {
          const scopeMap = {
            all: "全体员工",
            manager: "管理层",
            hr: "人事部门",
            finance: "财务部门",
            tech: "技术部门",
          };
          return scopeMap[scope] || scope;
        });
      } else {
        applyScopeList.value = [];
      }
      // å¤„理应急处置步骤
      if (emergencyPlan.execSteps) {
        try {
          execStepsList.value = JSON.parse(emergencyPlan.execSteps);
        } catch (e) {
          execStepsList.value = [];
        }
      } else {
        execStepsList.value = [];
      }
    } else {
      currentPlan.value = null;
      applyScopeList.value = [];
      execStepsList.value = [];
      showToast("未找到应急预案信息");
    }
  };
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  onMounted(() => {
    initData();
  });
  onShow(() => {
    initData();
  });
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .emergency-plan-view {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 32px;
  }
  .detail-content {
    padding: 20px;
  }
  // ä¿¡æ¯ section
  .info-section {
    background: #ffffff;
    border-radius: 12px;
    padding: 24px;
    margin-bottom: 24px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  }
  .section-title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 24px;
  }
  .title-text {
    font-size: 18px;
    font-weight: 600;
    color: #303133;
    flex: 1;
  }
  .type-tag {
    margin-left: 16px;
  }
  .info-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 20px;
  }
  .info-item {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  .info-label {
    font-size: 14px;
    color: #909399;
  }
  .info-value {
    font-size: 14px;
    color: #303133;
    word-break: break-all;
  }
  // é€‚用范围 section
  .scope-section {
    background: #ffffff;
    border-radius: 12px;
    padding: 24px;
    margin-bottom: 24px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  }
  .section-header {
    margin-bottom: 16px;
  }
  .section-heading {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
    border-left: 4px solid #2979ff;
    padding-left: 16px;
  }
  .scope-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
  }
  .scope-tag {
    margin-bottom: 8px;
  }
  // åº”急处置步骤 section
  .steps-section {
    background: #ffffff;
    border-radius: 12px;
    padding: 24px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  }
  .steps-list {
    margin-top: 16px;
  }
  .step-item {
    display: flex;
    margin-bottom: 24px;
    position: relative;
  }
  .step-item::before {
    content: "";
    position: absolute;
    left: 15px;
    top: 40px;
    bottom: -24px;
    width: 2px;
    background-color: #eaeaea;
  }
  .step-item:last-child::before {
    display: none;
  }
  .step-number {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background-color: #2979ff;
    color: #ffffff;
    font-size: 14px;
    font-weight: 600;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 16px;
    flex-shrink: 0;
    margin-top: 4px;
  }
  .step-content {
    flex: 1;
  }
  .step-title {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
    display: block;
    margin-bottom: 8px;
  }
  .step-description {
    font-size: 14px;
    color: #606266;
    line-height: 1.5;
    word-break: break-all;
  }
  .no-steps {
    padding: 40px;
    text-align: center;
    color: #909399;
    font-size: 14px;
    background-color: #f8f9fa;
    border-radius: 8px;
  }
  // ç©ºçŠ¶æ€
  .empty-state {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 60vh;
    font-size: 16px;
    color: #909399;
  }
</style>