hejianhao
2026-03-04 5c43040ddf6dbf6a4304284b3e5f573de61c4b1e
feat: 优化项目编辑地图标记、参数配置及界面交互

- 在项目编辑表单中新增项目类型图标标记,支持根据项目类型显示不同图标
- 修复项目编辑表单中地图标记的内存泄漏问题,添加组件卸载时的清理逻辑
- 为账号编辑表单中的关联项目/委托方选择器添加搜索过滤功能
- 修复客户参数配置表单中安全检查与设备检查占比的联动验证逻辑
- 调整资产项目参数配置中A/B/C类项目的显示顺序和配置规则
- 优化大屏首页重大隐患数模块的悬浮框显示效果和交互
- 修复安全检查计划中生成报告时的代码格式和逻辑一致性
- 统一项目列表页面的代码格式和引号使用规范
1个文件已删除
3个文件已添加
6个文件已修改
967 ■■■■■ 已修改文件
estate-frontend-v2024/src/assets/images/FIXED_ASSETS.png 补丁 | 查看 | 原始文档 | blame | 历史
estate-frontend-v2024/src/assets/images/HOUSE_CONSTRUCTION.png 补丁 | 查看 | 原始文档 | blame | 历史
estate-frontend-v2024/src/assets/images/MUNICIPAL_ADMINISTRATION.png 补丁 | 查看 | 原始文档 | blame | 历史
estate-frontend-v2024/src/views/business/forms/__tests__/edit-projectPermission-form.test.jsx 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
estate-frontend-v2024/src/views/business/forms/edit-clientParams-form.jsx 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
estate-frontend-v2024/src/views/business/forms/edit-project-form.jsx 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
estate-frontend-v2024/src/views/screenSetting/forms/edit-account-form.jsx 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/home/index.vue 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/planOfAudit/index.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/projectList/index.vue 548 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
estate-frontend-v2024/src/assets/images/FIXED_ASSETS.png
estate-frontend-v2024/src/assets/images/HOUSE_CONSTRUCTION.png
estate-frontend-v2024/src/assets/images/MUNICIPAL_ADMINISTRATION.png
estate-frontend-v2024/src/views/business/forms/__tests__/edit-projectPermission-form.test.jsx
File was deleted
estate-frontend-v2024/src/views/business/forms/edit-clientParams-form.jsx
@@ -340,7 +340,7 @@
      const item = rules[orIndex][andIndex];
      const currentLevel = Object.keys(item)[0];
      const currentQuantity = item[currentLevel];
      if (key === 'level') {
        // 修改隐患等级 (key)
        // 如果之前有数量,保留数量
@@ -357,12 +357,24 @@
  // 更新在建项目参数
  updateUnderConstruction = (key, value) => {
    this.setState(prev => ({
      underConstruction: {
    const raw = value === "" || value === null || value === undefined ? 0 : Number(value);
    const num = Number.isFinite(raw) ? raw : 0;
    const clamped = Math.max(0, Math.min(100, num));
    this.setState((prev) => {
      const next = {
        ...prev.underConstruction,
        [key]: value
        [key]: clamped
      };
      if (key === "safe_prop") {
        next.device_prop = 100 - clamped;
      } else if (key === "device_prop") {
        next.safe_prop = 100 - clamped;
      }
    }));
      return { underConstruction: next };
    });
  }
  handleOk = () => {
@@ -371,6 +383,17 @@
    form.validateFields((err, values) => {
      if (err) return;
      const safeProp = Number(underConstruction.safe_prop);
      const deviceProp = Number(underConstruction.device_prop);
      if (
        !Number.isFinite(safeProp) ||
        !Number.isFinite(deviceProp) ||
        Math.abs(safeProp + deviceProp - 100) > 0.0001
      ) {
        message.error("安全检查与设备检查占比相加必须为100");
        return;
      }
      // 准备数据
      const permissionClient = dataSource
@@ -682,96 +705,98 @@
                      <Divider />
                      <div style={{ fontWeight: 'bold' }}>资产项目:</div>
                      {["a", "b", "c"].map((type, index) => (
                      {["c", "b", "a"].map((type, index) => (
                        <div
                          className="flex"
                          key={type}
                          style={{ marginTop: 12 }}
                        >
                          <div className="label">{['A', 'B', 'C'][index]}类:</div>
                          <div className="label">{['C', 'B', 'A'][index]}类:</div>
                          {
                            type != 'a' &&
                            <div>
                              <Button
                                type="primary"
                                onClick={() => this.addOrCondition(type)}
                                size="small"
                              >
                                添加OR条件
                              </Button>
                              {assetRules[type].map((orGroup, orIndex) => (
                                <div
                                  key={orIndex}
                                  style={{
                                    display: 'flex',
                                    alignItems: "center",
                                    flexWrap: "wrap",
                                    marginTop: 5
                                  }}
                                  className="flex"
                            type != 'a' ?
                              <div>
                                <Button
                                  type="primary"
                                  onClick={() => this.addOrCondition(type)}
                                  size="small"
                                >
                                  {orIndex > 0 && (
                                    <div style={{ display: 'flex', alignItems: 'center' }}>
                                      <span style={{ color: "#ff4d4f", margin: '0 5px' }}>OR</span>
                                  添加OR条件
                                </Button>
                                {assetRules[type].map((orGroup, orIndex) => (
                                  <div
                                    key={orIndex}
                                    style={{
                                      display: 'flex',
                                      alignItems: "center",
                                      flexWrap: "wrap",
                                      marginTop: 5
                                    }}
                                    className="flex"
                                  >
                                    {orIndex > 0 && (
                                      <div style={{ display: 'flex', alignItems: 'center' }}>
                                        <span style={{ color: "#ff4d4f", margin: '0 5px' }}>OR</span>
                                        <Icon
                                          type="delete"
                                          style={{ fontSize: 18, color: "#FF4D4F", cursor: "pointer", marginRight: 8 }}
                                          onClick={() => this.removeOrCondition(type, orIndex)}
                                        />
                                      </div>
                                    )}
                                    <div style={{ border: '1px dashed #ccc', padding: 5, borderRadius: 4, display: 'flex', flexWrap: 'wrap', alignItems: 'center' }}>
                                      {orGroup.map((andItem, andIndex) => {
                                        const level = Object.keys(andItem)[0];
                                        const quantity = andItem[level];
                                        return (
                                          <div key={andIndex} style={{ display: "flex", alignItems: "center", margin: '2px 0' }}>
                                            {andIndex > 0 && (
                                              <div style={{ display: 'flex', alignItems: 'center' }}>
                                                <span style={{ marginLeft: 8, color: "#999" }}>and</span>
                                                <Icon
                                                  type="minus-circle"
                                                  theme="filled"
                                                  style={{ marginLeft: 4, fontSize: 16, color: "#FF4D4F", cursor: "pointer", marginRight: 4 }}
                                                  onClick={() => this.removeAndCondition(type, orIndex, andIndex)}
                                                />
                                              </div>
                                            )}
                                            <Select
                                              style={{ width: 120 }}
                                              placeholder="隐患级别"
                                              value={level ? Number(level) : undefined}
                                              onChange={(val) => this.updateCondition(type, orIndex, andIndex, "level", val)}
                                            >
                                              <Option value={1}>一级隐患</Option>
                                              <Option value={2}>二级隐患</Option>
                                              <Option value={3}>三级隐患</Option>
                                              <Option value={4}>四级隐患</Option>
                                            </Select>
                                            <Input
                                              style={{ width: 80, marginLeft: 8 }}
                                              type="number"
                                              placeholder="数量"
                                              value={quantity}
                                              disabled={!level}
                                              onChange={(e) => this.updateCondition(type, orIndex, andIndex, "quantity", e.target.value)}
                                            />
                                          </div>
                                        )
                                      })}
                                      <Icon
                                        type="delete"
                                        style={{ fontSize: 18, color: "#FF4D4F", cursor: "pointer", marginRight: 8 }}
                                        onClick={() => this.removeOrCondition(type, orIndex)}
                                        type="plus-circle"
                                        theme="filled"
                                        style={{ marginLeft: 8, fontSize: 18, color: "#1DA57A", cursor: "pointer" }}
                                        onClick={() => this.addAndCondition(type, orIndex)}
                                      />
                                    </div>
                                  )}
                                  <div style={{ border: '1px dashed #ccc', padding: 5, borderRadius: 4, display: 'flex', flexWrap: 'wrap', alignItems: 'center' }}>
                                    {orGroup.map((andItem, andIndex) => {
                                      const level = Object.keys(andItem)[0];
                                      const quantity = andItem[level];
                                      return (
                                        <div key={andIndex} style={{ display: "flex", alignItems: "center", margin: '2px 0' }}>
                                          {andIndex > 0 && (
                                            <div style={{ display: 'flex', alignItems: 'center' }}>
                                              <span style={{ marginLeft: 8, color: "#999" }}>and</span>
                                              <Icon
                                                type="minus-circle"
                                                theme="filled"
                                                style={{ marginLeft: 4, fontSize: 16, color: "#FF4D4F", cursor: "pointer", marginRight: 4 }}
                                                onClick={() => this.removeAndCondition(type, orIndex, andIndex)}
                                              />
                                            </div>
                                          )}
                                          <Select
                                            style={{ width: 120 }}
                                            placeholder="隐患级别"
                                            value={level ? Number(level) : undefined}
                                            onChange={(val) => this.updateCondition(type, orIndex, andIndex, "level", val)}
                                          >
                                            <Option value={1}>一级隐患</Option>
                                            <Option value={2}>二级隐患</Option>
                                            <Option value={3}>三级隐患</Option>
                                            <Option value={4}>四级隐患</Option>
                                          </Select>
                                          <Input
                                            style={{ width: 80, marginLeft: 8 }}
                                            type="number"
                                            placeholder="数量"
                                            value={quantity}
                                            disabled={!level}
                                            onChange={(e) => this.updateCondition(type, orIndex, andIndex, "quantity", e.target.value)}
                                          />
                                        </div>
                                      )
                                    })}
                                    <Icon
                                      type="plus-circle"
                                      theme="filled"
                                      style={{ marginLeft: 8, fontSize: 18, color: "#1DA57A", cursor: "pointer" }}
                                      onClick={() => this.addAndCondition(type, orIndex)}
                                    />
                                  </div>
                                </div>
                              ))}
                            </div>
                                ))}
                              </div>
                              :
                              <div style={{ color: "#ff4d4f" }}>不可配置</div>
                          }
                        </div>
                      ))}
@@ -845,4 +870,4 @@
export default Form.create({ name: "EditClientParamsForm" })(
  EditClientParamsForm,
);
);
estate-frontend-v2024/src/views/business/forms/edit-project-form.jsx
@@ -9,6 +9,9 @@
    projectUsageTypeMapNew,
    provinceArr
} from "../../common/constants";
import MUNICIPAL_ADMINISTRATION from "../../../assets/images/MUNICIPAL_ADMINISTRATION.png"
import HOUSE_CONSTRUCTION from "../../../assets/images/HOUSE_CONSTRUCTION.png"
import FIXED_ASSETS from "../../../assets/images/FIXED_ASSETS.png"
import BaseForm from "../../common/edit-form-base";
import { createProjectNumber, toOptionsData } from "../../common/common_func";
import { getClientOptions } from "../../../api/business";
@@ -58,6 +61,10 @@
    }
    componentWillUnmount() {
        if (this.marker && this.marker.setMap) {
            this.marker.setMap(null);
        }
        this.marker = null;
        if (this.map) {
            this.map.destroy();
            this.map = null;
@@ -151,9 +158,35 @@
    }
    viewWillAppear() {
        this.setState({
            keyword: ''
        })
        const { currentRowData } = this.props;
        // 合并当前行数据到 editableData(兼容 addressInfo 为字符串的情况)
        if (currentRowData) {
            let addrInfo = currentRowData.addressInfo;
            try {
                if (typeof addrInfo === 'string') {
                    addrInfo = JSON.parse(addrInfo);
                }
            } catch (e) {
                // ignore parse error
            }
            const merged = {
                ...this.state.editableData,
                ...currentRowData,
                addressInfo: addrInfo || this.state.editableData.addressInfo
            };
            this.setState(
                { editableData: merged, keyword: '' },
                () => {
                    if (this.map) {
                        this.map.destroy();
                        this.map = null;
                    }
                    this.initMap();
                }
            );
            return;
        }
        this.setState({ keyword: '' });
        if (this.map) {
            this.map.destroy();
            this.map = null;
@@ -172,7 +205,14 @@
            });
            const { addressInfo } = this.state.editableData;
            let center = [104.065617, 30.656952];
            if (addressInfo && addressInfo.longitude && addressInfo.latitude) {
            if (addressInfo && addressInfo.lonLat) {
                const parts = String(addressInfo.lonLat).split(',');
                const lng = Number(parts[0]);
                const lat = Number(parts[1]);
                if (Number.isFinite(lng) && Number.isFinite(lat)) {
                    center = [lng, lat];
                }
            } else if (addressInfo && addressInfo.longitude && addressInfo.latitude) {
                center = [addressInfo.longitude, addressInfo.latitude];
            }
@@ -187,7 +227,7 @@
                this.map.setCenter(center);
            }
            if (addressInfo && addressInfo.longitude && addressInfo.latitude) {
            if (addressInfo && (addressInfo.lonLat || (addressInfo.longitude && addressInfo.latitude))) {
                this.addMarker(center);
            }
        }).catch(e => {
@@ -203,12 +243,31 @@
    }
    addMarker = (position) => {
        const typeLabel = projectTypeMapNew[this.state.editableData.type];
        let imageSrc = HOUSE_CONSTRUCTION;
        if (typeLabel && typeLabel.indexOf('市政') !== -1) {
            imageSrc = MUNICIPAL_ADMINISTRATION;
        } else if (typeLabel && typeLabel.indexOf('固定资产') !== -1) {
            imageSrc = FIXED_ASSETS;
        }
        const icon = new this.AMap.Icon({
            image: imageSrc,
            size: new this.AMap.Size(30, 30),
            imageSize: new this.AMap.Size(30, 30)
        });
        if (this.marker) {
            this.marker.setPosition(position);
            if (this.marker.setIcon) {
                this.marker.setIcon(icon);
            }
            if (this.marker.setMap) {
                this.marker.setMap(this.map);
            }
        } else {
            this.marker = new this.AMap.Marker({
                position: position,
                map: this.map
                map: this.map,
                icon: icon
            });
        }
        this.map.setCenter(position);
estate-frontend-v2024/src/views/screenSetting/forms/edit-account-form.jsx
@@ -128,7 +128,18 @@
              initialValue: selectedClientIds,
              onChange: this.handleFieldChange("clientProjectIds"),
            })(
              <Select style={inputLongStyle} mode="multiple" placeholder={type == 1 ? '请选择关联委托方' : '请选择关联项目'}>
              <Select
                style={inputLongStyle}
                mode="multiple"
                placeholder={type == 1 ? '请选择关联委托方' : '请选择关联项目'}
                showSearch
                optionFilterProp="children"
                filterOption={(input, option) =>
                  String(option.props.children || '')
                    .toLowerCase()
                    .includes(String(input || '').toLowerCase())
                }
              >
                {this.state.relationOptions.map(option => {
                  const value = (type === 1 ? option.code : option.id);
                  const finalValue = value !== undefined && value !== null ? value.toString() : "";
screen/src/view/home/index.vue
@@ -35,33 +35,35 @@
      "
      class="flex a-center absolute left--14 bottom--14"
    >
      <img
        src="../../assets/img/home/Group@2x (4).png"
        class="w--110 h--110 mr--9 shrink0"
      />
      <div @click="toHiddenDanger" class="bgimg4 pt--10 c-fff cursor-pointer">
        <div class="flex a-center ml--14">
          <div class="ripple">
            <div class="ripple-wave wave-1"></div>
            <div class="ripple-wave wave-2"></div>
            <div class="ripple-wave wave-3"></div>
          </div>
          <div class="fs--15 lh--15 ml--25">重大隐患数</div>
          <div class="color5 ml--13 fs--30 lh--30 zh_tr">
            {{ screenData.majorHazardTotal }}
          </div>
        </div>
        <div class="flex a-center j-center gap--52 mt--11">
          <div class="flex a-center">
            <div class="fs--14 lh--14 ml--10">未整改</div>
            <div class="color6 ml--13 fs--20 lh--24 zh_tr">
              {{ screenData.unrectifiedHazardCount }}
      <div class="dangerBox">
        <img
          src="../../assets/img/home/Group@2x (4).png"
          class="w--110 h--110 shrink0"
        />
        <div @click="toHiddenDanger" class="hoverBox bgimg4 pt--10 c-fff cursor-pointer">
          <div class="flex a-center ml--14">
            <div class="ripple">
              <div class="ripple-wave wave-1"></div>
              <div class="ripple-wave wave-2"></div>
              <div class="ripple-wave wave-3"></div>
            </div>
            <div class="fs--15 lh--15 ml--25">重大隐患数</div>
            <div class="color5 ml--13 fs--30 lh--30 zh_tr">
              {{ screenData.majorHazardTotal }}
            </div>
          </div>
          <div class="flex a-center">
            <div class="fs--14 lh--14 ml--10">已整改</div>
            <div class="color7 ml--13 fs--20 lh--24 zh_tr">
              {{ screenData.rectifiedHazardCount }}
          <div class="flex a-center j-center gap--52 mt--11">
            <div class="flex a-center">
              <div class="fs--14 lh--14 ml--10">未整改</div>
              <div class="color6 ml--13 fs--20 lh--24 zh_tr">
                {{ screenData.unrectifiedHazardCount }}
              </div>
            </div>
            <div class="flex a-center">
              <div class="fs--14 lh--14 ml--10">已整改</div>
              <div class="color7 ml--13 fs--20 lh--24 zh_tr">
                {{ screenData.rectifiedHazardCount }}
              </div>
            </div>
          </div>
        </div>
@@ -1563,22 +1565,40 @@
                  style.fillOpacity !== undefined ? style.fillOpacity : 0.2,
                zIndex: style.zIndex,
                bubble: true,
                cursor: 'default',
                cursor:
                  level === 'district' && (isActive || isHoverable)
                    ? 'pointer'
                    : 'default',
                extData: { level, isActive }
              })
              // 添加 hover 事件
              if (isHoverable) {
                polygon.on('mouseover', () => {
                  polygon.setOptions({
              if (level === 'district' && (isActive || isHoverable)) {
                const baseFillColor = style.fillColor || 'transparent'
                const baseFillOpacity =
                  style.fillOpacity !== undefined ? style.fillOpacity : 0.2
                const baseStrokeColor = style.strokeColor
                const hoverStyle = isActive
                  ? {
                    fillColor: '#00b3ff',
                    fillOpacity: 0.35,
                    strokeColor: '#00b3ff',
                    strokeWeight: 3
                  }
                  : {
                    fillColor: '#00ffd2',
                    fillOpacity: 0.2
                  })
                  }
                polygon.on('mouseover', () => {
                  polygon.setOptions(hoverStyle)
                })
                polygon.on('mouseout', () => {
                  polygon.setOptions({
                    fillColor: 'transparent',
                    fillOpacity: 0
                    fillColor: baseFillColor,
                    fillOpacity: baseFillOpacity,
                    strokeColor: baseStrokeColor,
                    strokeWeight: 2
                  })
                })
              }
@@ -2085,6 +2105,29 @@
  }
}
.dangerBox .hoverBox {
  position: absolute;
  left: 100%;
  margin-left: 9px;
  top: 8px;
  display: none;
  z-index: 100;
  &::before {
    content: "";
    position: absolute;
    top: 8px;
    left: 9px;
    width: 40px;
    height: 100%;
    background: transparent;
  }
}
.dangerBox:hover .hoverBox {
  display: block;
}
.thisBox .hoverBox {
  position: absolute;
  left: 100%;
screen/src/view/planOfAudit/index.vue
@@ -737,7 +737,7 @@
              this.reportVisible = false
              return
            }
             const safeCommentType = parseInt(localStorage.getItem('commentType') || '1')
            const safeCommentType = parseInt(localStorage.getItem('commentType') || '1')
            if (safeCommentType === 1) {
              wordBlob = await getFuzzyReportWord({
                checkTaskId: checkTaskId,
@@ -745,11 +745,12 @@
                uuid: deviceTemplateName
              })
            } else {
            wordBlob = await getScoreReportWord({
              checkTaskId: checkTaskId,
              templateId: deviceTemplateName,
              uuid: deviceTemplateName
            })}
              wordBlob = await getScoreReportWord({
                checkTaskId: checkTaskId,
                templateId: deviceTemplateName,
                uuid: deviceTemplateName
              })
            }
          }
        } catch (error) {
          console.error('生成Word文档失败:', error)
@@ -770,7 +771,7 @@
        let pdfUrlResponse
        try {
          if (type === 'safe') {
              pdfUrlResponse = await getFuzzyReportPdfUrl({ uuid: safetyName })
            pdfUrlResponse = await getFuzzyReportPdfUrl({ uuid: safetyName })
          } else {
            pdfUrlResponse = await getFuzzyReportPdfUrl({ uuid: deviceTemplateName })
          }
@@ -783,7 +784,7 @@
        }
        let pdfUrl = ''
        console.log('pdfUrlResponse',pdfUrlResponse)
        console.log('pdfUrlResponse', pdfUrlResponse)
        if (typeof pdfUrlResponse === 'string') {
          pdfUrl = pdfUrlResponse
        } else if (pdfUrlResponse && pdfUrlResponse.data) {
screen/src/view/projectList/index.vue
@@ -469,8 +469,8 @@
</template>
<script>
import HeaderBox from "../header/index.vue";
import moment from "moment";
import HeaderBox from '../header/index.vue'
import moment from 'moment'
import {
  getProjectList,
  projectList,
@@ -478,12 +478,12 @@
  getFuzzyReportWord,
  getScoreReportWord,
  getFuzzyReportPdfUrl,
  getScoreReportPdfUrl,
} from "./service";
  getScoreReportPdfUrl
} from './service'
export default {
  components: { HeaderBox },
  props: {},
  data() {
  data () {
    return {
      // 搜索条件
      searchProjectId: null, // 项目选择
@@ -491,23 +491,23 @@
      riskLevel: null, // 项目风险
      startTime: null, // 开始时间
      endTime: null, // 结束时间
      startTimeTxt: "", // 开始时间显示文本
      endTimeTxt: "", // 结束时间显示文本
      projectName: "", // 项目名称搜索
      startTimeTxt: '', // 开始时间显示文本
      endTimeTxt: '', // 结束时间显示文本
      projectName: '', // 项目名称搜索
      // 下拉选项
      projectOptions: [], // 项目下拉列表
      projectTypeOptions: [
        { label: "房建", value: 0 },
        { label: "市政", value: 1 },
        { label: "固定资产", value: 2 },
        { label: '房建', value: 0 },
        { label: '市政', value: 1 },
        { label: '固定资产', value: 2 }
      ], // 项目类型下拉列表
      riskLevelOptions: [
        { label: "拒绝接受风险", value: 0 },
        { label: "不可接受风险", value: 1 },
        { label: "可接受风险", value: 2 },
        { label: "可容忍风险", value: 3 },
        { label: "可忽略风险", value: 4 },
        { label: '拒绝接受风险', value: 0 },
        { label: '不可接受风险', value: 1 },
        { label: '可接受风险', value: 2 },
        { label: '可容忍风险', value: 3 },
        { label: '可忽略风险', value: 4 }
      ],
      // 表格数据
@@ -532,551 +532,551 @@
      // 用户信息
      userInfo: {},
      clientId: localStorage.getItem("clientId"),
      clientId: localStorage.getItem('clientId'),
      isClientMode: false, // 是否是委托方模式
      // 报告相关
      reportUrl: "", // 已废弃,保留兼容性
      reportBlobUrl: "", // Blob URL用于下载
      pdfUrl: "", // PDF预览URL
      reportUrl: '', // 已废弃,保留兼容性
      reportBlobUrl: '', // Blob URL用于下载
      pdfUrl: '', // PDF预览URL
      reportLoading: false,
      showPreview: false, // 是否显示预览
      currentRow: null, // 当前选中的行数据
      // 评价类型
      commentType: localStorage.getItem("commentType") || null,
      projectCategory: null,
    };
      commentType: localStorage.getItem('commentType') || null,
      projectCategory: null
    }
  },
  computed: {
    // 是否显示风险类型列(commentType 不为 2 时显示)
    shouldShowRiskLevel() {
      return this.commentType != "2";
    },
    shouldShowRiskLevel () {
      return this.commentType != '2'
    }
  },
  watch: {
    startTime: {
      handler(newVal) {
      handler (newVal) {
        if (newVal) {
          this.startTimeTxt = moment(newVal).format("YYYY年MM月DD日 HH:mm:ss");
          this.startTimeTxt = moment(newVal).format('YYYY年MM月DD日 HH:mm:ss')
        } else {
          this.startTimeTxt = "";
          this.startTimeTxt = ''
        }
      },
      immediate: true,
      immediate: true
    },
    endTime: {
      handler(newVal) {
      handler (newVal) {
        if (newVal) {
          this.endTimeTxt = moment(newVal).format("YYYY年MM月DD日 HH:mm:ss");
          this.endTimeTxt = moment(newVal).format('YYYY年MM月DD日 HH:mm:ss')
        } else {
          this.endTimeTxt = "";
          this.endTimeTxt = ''
        }
      },
      immediate: true,
    },
      immediate: true
    }
  },
  created() {
  created () {
    if (this.$route.query.projectCategory) {
      this.projectCategory = this.$route.query.projectCategory;
      this.projectCategory = this.$route.query.projectCategory
    }
    // 接收从 home 页面传递过来的时间参数并回填
    if (this.$route.query.startTime) {
      this.startTime = new Date(this.$route.query.startTime);
      this.startTime = new Date(this.$route.query.startTime)
    }
    if (this.$route.query.endTime) {
      this.endTime = new Date(this.$route.query.endTime);
      this.endTime = new Date(this.$route.query.endTime)
    }
    this.initUserInfo();
    this.initData();
    this.initUserInfo()
    this.initData()
  },
  mounted() {},
  mounted () {},
  methods: {
    // 初始化用户信息
    initUserInfo() {
      const userInfo = JSON.parse(localStorage.getItem("userInfo") || "{}");
      this.userInfo = userInfo;
    initUserInfo () {
      const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
      this.userInfo = userInfo
      // 判断是否是委托方(type为1表示委托方)
      this.isClientMode = userInfo.type === 1;
      this.isClientMode = userInfo.type === 1
    },
    // 初始化数据
    async initData() {
      await this.loadProjectOptions();
      await this.loadTableData();
    async initData () {
      await this.loadProjectOptions()
      await this.loadTableData()
    },
    // 加载项目下拉列表(仅委托方需要)
    async loadProjectOptions() {
      if (!this.isClientMode) return;
    async loadProjectOptions () {
      if (!this.isClientMode) return
      try {
        const res = await getProjectList({
          clientId: this.clientId,
          projectCategory: ["ongoing", "fixed"][
            localStorage.getItem("activeTab") - 1
          ],
        });
          projectCategory: ['ongoing', 'fixed'][
            localStorage.getItem('activeTab') - 1
          ]
        })
        if (res) {
          this.projectOptions = res.map((item) => ({
            label: item.name,
            value: item.id,
          }));
            value: item.id
          }))
        }
      } catch (error) {
        console.error("加载项目列表失败:", error);
        console.error('加载项目列表失败:', error)
      }
    },
    // 加载表格数据
    async loadTableData() {
      this.loading = true;
    async loadTableData () {
      this.loading = true
      try {
        const params = {
          pageNum: this.currentPage,
          pageSize: this.pageSize,
          clientId: this.clientId,
        };
          clientId: this.clientId
        }
        // 添加搜索条件
        if (this.searchProjectId) params.projectId = this.searchProjectId;
        if (this.projectType) params.type = this.projectType;
        if (this.riskLevel !== null && this.riskLevel !== "") {
          params.riskLevel = this.riskLevel;
        if (this.searchProjectId) params.projectId = this.searchProjectId
        if (this.projectType) params.type = this.projectType
        if (this.riskLevel !== null && this.riskLevel !== '') {
          params.riskLevel = this.riskLevel
        }
        if (this.startTime) {
          params.startTime = moment(this.startTime).format(
            "YYYY-MM-DD HH:mm:ss"
          );
            'YYYY-MM-DD HH:mm:ss'
          )
        }
        if (this.endTime) {
          params.endTime = moment(this.endTime).format("YYYY-MM-DD HH:mm:ss");
          params.endTime = moment(this.endTime).format('YYYY-MM-DD HH:mm:ss')
        }
        if (this.projectName) params.name = this.projectName;
        if (this.projectName) params.name = this.projectName
        if (this.projectCategory) {
          params.category = localStorage.getItem("activeTab");
          params.category = localStorage.getItem('activeTab')
        }
        const res = await projectList(params);
        const res = await projectList(params)
        if (res) {
          this.tableData = res.records || [];
          this.total = res.total || 0;
          this.tableData = res.records || []
          this.total = res.total || 0
        }
      } catch (error) {
        console.error("加载项目列表失败:", error);
        this.$message.error("加载项目列表失败");
        console.error('加载项目列表失败:', error)
        this.$message.error('加载项目列表失败')
      } finally {
        this.loading = false;
        this.loading = false
      }
    },
    // 搜索
    handleSearch() {
      this.currentPage = 1;
      this.loadTableData();
    handleSearch () {
      this.currentPage = 1
      this.loadTableData()
    },
    // 清除项目选择
    clearSearchProjectId() {
      this.searchProjectId = null;
      this.handleSearch();
    clearSearchProjectId () {
      this.searchProjectId = null
      this.handleSearch()
    },
    // 清除项目类型
    clearProjectType() {
      this.projectType = null;
      this.handleSearch();
    clearProjectType () {
      this.projectType = null
      this.handleSearch()
    },
    // 清除风险等级
    clearRiskLevel() {
      this.riskLevel = null;
      this.handleSearch();
    clearRiskLevel () {
      this.riskLevel = null
      this.handleSearch()
    },
    // 清除开始时间
    clearStartTime() {
      this.startTime = null;
      this.handleSearch();
    clearStartTime () {
      this.startTime = null
      this.handleSearch()
    },
    // 清除结束时间
    clearEndTime() {
      this.endTime = null;
      this.handleSearch();
    clearEndTime () {
      this.endTime = null
      this.handleSearch()
    },
    // 分页大小改变
    handleSizeChange(val) {
      this.pageSize = val;
      this.currentPage = 1;
      this.loadTableData();
    handleSizeChange (val) {
      this.pageSize = val
      this.currentPage = 1
      this.loadTableData()
    },
    // 当前页改变
    handleCurrentChange(val) {
      this.currentPage = val;
      this.loadTableData();
    handleCurrentChange (val) {
      this.currentPage = val
      this.loadTableData()
    },
    // 点击日期选择器
    handleClick(type) {
      this.$refs[`datetime${type}`].focus();
    handleClick (type) {
      this.$refs[`datetime${type}`].focus()
    },
    // 返回
    backFun() {
      this.$router.go(-1);
    backFun () {
      this.$router.go(-1)
    },
    // 跳转到项目详情
    toProjectDetail(row) {
    toProjectDetail (row) {
      this.$router.push({
        path: "/projectHome",
        path: '/projectHome',
        query: {
          projectId: row.id,
          projectName: row.name,
        },
      });
          projectName: row.name
        }
      })
    },
    // 跳转到隐患页面
    toHiddenDanger(row, type) {
    toHiddenDanger (row, type) {
      this.$router.push({
        path: "/hiddenDanger",
        path: '/hiddenDanger',
        query: {
          projectId: row.id,
          activeTab: type,
          projectCategory: ["ongoing", "fixed"][
            localStorage.getItem("activeTab") - 1
          ],
        },
      });
          projectCategory: ['ongoing', 'fixed'][
            localStorage.getItem('activeTab') - 1
          ]
        }
      })
    },
    // 打开报告弹窗
    async openReportDialog(row) {
      this.currentProjectId = row.id;
      this.projectVisible = true;
      await this.loadCheckList(row.id);
    async openReportDialog (row) {
      this.currentProjectId = row.id
      this.projectVisible = true
      await this.loadCheckList(row.id)
    },
    // 加载检查列表
    async loadCheckList(projectId) {
      this.checkLoading = true;
    async loadCheckList (projectId) {
      this.checkLoading = true
      try {
        const res = await getDeviceDetailCheckList({
          projectId: projectId,
        });
          projectId: projectId
        })
        if (res) {
          this.checkList = res.records || [];
          this.checkList = res.records || []
        }
      } catch (error) {
        console.error("加载检查列表失败:", error);
        this.$message.error("加载检查列表失败");
        console.error('加载检查列表失败:', error)
        this.$message.error('加载检查列表失败')
      } finally {
        this.checkLoading = false;
        this.checkLoading = false
      }
    },
    // 打开单位信息弹窗
    openCompanyDialog(row) {
    openCompanyDialog (row) {
      // 解析单位信息
      this.companyInfo = {
        construction: this.parseCompanyInfo(row.constructionCompanyInfo),
        supervisor: this.parseCompanyInfo(row.supervisorCompanyInfo),
        development: this.parseCompanyInfo(row.developmentCompanyInfo),
      };
      this.companyVisible = true;
        development: this.parseCompanyInfo(row.developmentCompanyInfo)
      }
      this.companyVisible = true
    },
    // 解析单位信息字符串
    parseCompanyInfo(infoStr) {
      if (!infoStr) return {};
    parseCompanyInfo (infoStr) {
      if (!infoStr) return {}
      try {
        return JSON.parse(infoStr);
        return JSON.parse(infoStr)
      } catch (error) {
        return {};
        return {}
      }
    },
    // 检查是否有任何报告
    hasAnyReport(row) {
      return row.containSystemRes === 1 || row.containDeviceRes === 1;
    hasAnyReport (row) {
      return row.containSystemRes === 1 || row.containDeviceRes === 1
    },
    // 检查报告可用性(当按钮被禁用时点击)
    checkReportAvailability(row) {
    checkReportAvailability (row) {
      if (!this.hasAnyReport(row)) {
        this.$message.warning("该计划暂无可查看的报告");
        this.$message.warning('该计划暂无可查看的报告')
      }
    },
    // 打开报告
    async openReport(type, row) {
    async openReport (type, row) {
      // 根据类型检查是否有对应报告
      const hasReport =
        type === "safe"
        type === 'safe'
          ? row.containSystemRes === 1
          : row.containDeviceRes === 1;
          : row.containDeviceRes === 1
      if (!hasReport) {
        this.$message.warning(`暂无${type === "safe" ? "安全" : "设备"}报告`);
        return;
        this.$message.warning(`暂无${type === 'safe' ? '安全' : '设备'}报告`)
        return
      }
      this.currentRow = row;
      this.reportVisible = true;
      this.showPreview = false;
      this.reportLoading = true;
      this.currentRow = row
      this.reportVisible = true
      this.showPreview = false
      this.reportLoading = true
      try {
        // 从 localStorage 获取模板 ID
        const deviceTemplateId = localStorage.getItem("deviceTemplateId");
        const safetyId = localStorage.getItem("safetyId");
        const safetyName = localStorage.getItem("safetyName");
        const deviceTemplateName = localStorage.getItem("deviceTemplateName");
        const deviceTemplateId = localStorage.getItem('deviceTemplateId')
        const safetyId = localStorage.getItem('safetyId')
        const safetyName = localStorage.getItem('safetyName')
        const deviceTemplateName = localStorage.getItem('deviceTemplateName')
        // 检查任务 ID
        const checkTaskId = row.id;
        const checkTaskId = row.id
        if (!checkTaskId) {
          this.$message.error("缺少检查任务ID");
          this.reportLoading = false;
          this.reportVisible = false;
          return;
          this.$message.error('缺少检查任务ID')
          this.reportLoading = false
          this.reportVisible = false
          return
        }
        // 使用当前时间戳作为 uuid
        const uuid = Date.now().toString();
        const uuid = Date.now().toString()
        // 第一步:调用接口生成Word文档(返回Word文档流)
        let wordBlob;
        let wordBlob
        try {
          if (type === "safe") {
          if (type === 'safe') {
            // 安全报告(模糊评价)
            if (!safetyId) {
              this.$message.error("缺少安全报告模板ID");
              this.reportLoading = false;
              this.reportVisible = false;
              return;
              this.$message.error('缺少安全报告模板ID')
              this.reportLoading = false
              this.reportVisible = false
              return
            }
            const safeCommentType = parseInt(
              localStorage.getItem("commentType") || "1"
            );
              localStorage.getItem('commentType') || '1'
            )
            if (safeCommentType === 1) {
              wordBlob = await getFuzzyReportWord({
                checkTaskId: checkTaskId,
                templateId: safetyId,
                uuid: safetyName,
              });
                uuid: safetyName
              })
            } else {
              wordBlob = await getScoreReportWord({
                checkTaskId: checkTaskId,
                templateId: safetyId,
                uuid: safetyName,
              });
                uuid: safetyName
              })
            }
          } else {
            // 设备报告(打分评价)
            if (!deviceTemplateId) {
              this.$message.error("缺少设备报告模板ID");
              this.reportLoading = false;
              this.reportVisible = false;
              return;
              this.$message.error('缺少设备报告模板ID')
              this.reportLoading = false
              this.reportVisible = false
              return
            }
            const safeCommentType = parseInt(
              localStorage.getItem("commentType") || "1"
            );
              localStorage.getItem('commentType') || '1'
            )
            if (safeCommentType === 1) {
              wordBlob = await getFuzzyReportWord({
                checkTaskId: checkTaskId,
                templateId: deviceTemplateId,
                uuid: deviceTemplateName,
              });
                uuid: deviceTemplateName
              })
            } else {
              wordBlob = await getScoreReportWord({
                checkTaskId: checkTaskId,
                templateId: deviceTemplateId,
                uuid: deviceTemplateName,
              });
                uuid: deviceTemplateName
              })
            }
          }
        } catch (error) {
          // 捕获生成Word文档时的错误
          console.error("生成Word文档失败:", error);
          this.$message.error(error.message || "生成报告失败");
          this.reportLoading = false;
          this.reportVisible = false;
          return;
          console.error('生成Word文档失败:', error)
          this.$message.error(error.message || '生成报告失败')
          this.reportLoading = false
          this.reportVisible = false
          return
        }
        // 检查 Word 文档是否生成成功
        if (!wordBlob || wordBlob.size === 0) {
          this.$message.error("报告生成失败");
          this.reportLoading = false;
          this.reportVisible = false;
          return;
          this.$message.error('报告生成失败')
          this.reportLoading = false
          this.reportVisible = false
          return
        }
        // 第二步:使用uuid获取PDF链接
        let pdfUrlResponse;
        let pdfUrlResponse
        try {
          if (type === "safe") {
            pdfUrlResponse = await getFuzzyReportPdfUrl({ uuid: safetyName });
          if (type === 'safe') {
            pdfUrlResponse = await getFuzzyReportPdfUrl({ uuid: safetyName })
          } else {
            pdfUrlResponse = await getFuzzyReportPdfUrl({
              uuid: deviceTemplateName,
            });
              uuid: deviceTemplateName
            })
          }
        } catch (error) {
          // 捕获获取PDF链接时的错误
          console.error("获取PDF链接失败:", error);
          this.$message.error(error.message || "获取PDF链接失败");
          this.reportLoading = false;
          this.reportVisible = false;
          return;
          console.error('获取PDF链接失败:', error)
          this.$message.error(error.message || '获取PDF链接失败')
          this.reportLoading = false
          this.reportVisible = false
          return
        }
        // 根据后端返回的数据结构获取PDF链接
        // 假设后端返回格式为 { data: "http://xxx.pdf" } 或直接返回字符串
        const pdfUrl = pdfUrlResponse.data || pdfUrlResponse;
        const pdfUrl = pdfUrlResponse.data || pdfUrlResponse
        if (!pdfUrl) {
          this.$message.error("获取PDF链接失败");
          this.reportLoading = false;
          this.reportVisible = false;
          return;
          this.$message.error('获取PDF链接失败')
          this.reportLoading = false
          this.reportVisible = false
          return
        }
        // 保存PDF链接用于预览和下载
        this.pdfUrl = pdfUrl;
        this.reportBlobUrl = pdfUrl; // 用于下载
        this.showPreview = true;
        this.pdfUrl = pdfUrl
        this.reportBlobUrl = pdfUrl // 用于下载
        this.showPreview = true
        this.$message.success("报告加载成功");
        this.$message.success('报告加载成功')
      } catch (error) {
        console.error("获取报告失败:", error);
        console.error('获取报告失败:', error)
        // 优先显示错误对象中的 message
        if (error.message) {
          this.$message.error(error.message);
          this.$message.error(error.message)
        } else if (error.response) {
          const status = error.response.status;
          const status = error.response.status
          if (status === 404) {
            this.$message.error("报告不存在");
            this.$message.error('报告不存在')
          } else if (status === 500) {
            this.$message.error("服务器错误,请稍后重试");
            this.$message.error('服务器错误,请稍后重试')
          } else {
            this.$message.error(
              `获取报告失败: ${error.response.statusText || "未知错误"}`
            );
              `获取报告失败: ${error.response.statusText || '未知错误'}`
            )
          }
        } else {
          this.$message.error("获取报告失败,请检查网络连接");
          this.$message.error('获取报告失败,请检查网络连接')
        }
        this.showPreview = false;
        this.reportVisible = false;
        this.showPreview = false
        this.reportVisible = false
      } finally {
        this.reportLoading = false;
        this.reportLoading = false
      }
    },
    // 下载报告
    downloadReport() {
    downloadReport () {
      if (!this.reportBlobUrl) {
        this.$message.warning("报告未加载");
        return;
        this.$message.warning('报告未加载')
        return
      }
      try {
        // 直接打开PDF链接进行下载
        window.open(this.reportBlobUrl, "_blank");
        this.$message.success("报告下载成功");
        window.open(this.reportBlobUrl, '_blank')
        this.$message.success('报告下载成功')
      } catch (error) {
        console.error("下载报告失败:", error);
        this.$message.error("下载报告失败");
        console.error('下载报告失败:', error)
        this.$message.error('下载报告失败')
      }
    },
    // 关闭报告弹窗
    closeReportDialog() {
      this.reportVisible = false;
      this.showPreview = false;
    closeReportDialog () {
      this.reportVisible = false
      this.showPreview = false
      // 清空PDF链接(不需要释放URL,因为是服务器链接)
      this.reportBlobUrl = "";
      this.pdfUrl = "";
      this.reportUrl = ""; // 保留兼容性
      this.reportBlobUrl = ''
      this.pdfUrl = ''
      this.reportUrl = '' // 保留兼容性
    },
    // 格式化项目类型
    formatProjectType(type) {
    formatProjectType (type) {
      const typeMap = {
        0: "房建",
        1: "市政",
        2: "建筑工程",
      };
      return typeMap[type] || "-";
        0: '房建',
        1: '市政',
        2: '建筑工程'
      }
      return typeMap[type] || '-'
    },
    // 格式化风险等级
    formatRiskLevel(level) {
    formatRiskLevel (level) {
      const levelMap = {
        0: "拒绝接受风险",
        1: "不可接受风险",
        2: "可接受风险",
        3: "可容忍风险",
        4: "可忽略风险",
      };
      return levelMap[level] || "-";
        0: '拒绝接受风险',
        1: '不可接受风险',
        2: '可接受风险',
        3: '可容忍风险',
        4: '可忽略风险'
      }
      return levelMap[level] || '-'
    },
    // 格式化时间
    formatTime(time) {
      return time ? moment(time).format("YYYY-MM-DD HH:mm:ss") : "-";
    formatTime (time) {
      return time ? moment(time).format('YYYY-MM-DD HH:mm:ss') : '-'
    },
    // 格式化计划类型
    formatPlanType(type) {
    formatPlanType (type) {
      const typeMap = {
        1: "日常检查",
        2: "专项检查",
        3: "综合检查",
      };
      return typeMap[type] || "-";
        1: '日常检查',
        2: '专项检查',
        3: '综合检查'
      }
      return typeMap[type] || '-'
    },
    // 判断检查情况是否良好(用于控制图标颜色)
    isCheckResultGood(checkResult) {
    isCheckResultGood (checkResult) {
      // 检查情况只有"已检查"和"未检查"两种状态
      // 已检查显示绿色,未检查显示红色
      if (!checkResult) return false;
      if (!checkResult) return false
      return checkResult.includes("已检查");
      return checkResult.includes('已检查')
    },
    // 格式化地址信息
    formatAddress(addressInfo) {
      if (!addressInfo) return "-";
    formatAddress (addressInfo) {
      if (!addressInfo) return '-'
      try {
        const addressObj = JSON.parse(addressInfo);
        const addressObj = JSON.parse(addressInfo)
        // 拼接完整地址:省 + 市 + 区 + 详细地址
        const { province, city, district, detail } = addressObj;
        const parts = [];
        if (province) parts.push(province);
        if (city) parts.push(city);
        if (district && district !== "其它区") parts.push(district);
        if (detail) parts.push(detail);
        return parts.join("") || "-";
        const { province, city, district, detail } = addressObj
        const parts = []
        if (province) parts.push(province)
        if (city) parts.push(city)
        if (district && district !== '其它区') parts.push(district)
        if (detail) parts.push(detail)
        return parts.join('') || '-'
      } catch (error) {
        // 如果解析失败,直接返回原始字符串
        return addressInfo || "-";
        return addressInfo || '-'
      }
    },
  },
};
    }
  }
}
</script>
<style lang="less">
.custom-popper {