pyt
2 天以前 20e32587ecbab27e4436f2f64a15faa3c89e4f41
Merge branch 'master' of http://120.76.84.145:10101/gitblit/r/H5/threeSide
25个文件已修改
5个文件已添加
2576 ■■■■■ 已修改文件
H5/components/voiceInputPopup.vue 368 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/manifest.json 171 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/package.json 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/pages/Appeal/Appeal.vue 72 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/pages/add-progress/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/pages/supervision/edit-supervision-progress.vue 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/pages/supervision/service.js 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/pages/supervision/supervision-progress.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/pages/supervision/supervision.vue 66 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/pages/work-detail/work-detail.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/uni_modules/mumu-recorder/changelog.md 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/uni_modules/mumu-recorder/components/mumu-recorder/mumu-recorder.vue 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/uni_modules/mumu-recorder/package.json 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/uni_modules/mumu-recorder/readme.md 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/utils/request.js 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/config/routes.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/src/pages/appeal-management/detail.jsx 235 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/src/pages/appeal-management/service.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/src/pages/appeal-management/statistics/index.jsx 42 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/src/pages/appeal-management/statistics/service.js 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/src/pages/logManagement/index.jsx 66 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/src/pages/logManagement/service.js 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/src/pages/party/manage/index.jsx 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/src/pages/setting/role/components/addAndEdit.jsx 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/src/pages/setting/role/index.jsx 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/src/pages/setting/user/components/addAndEdit.jsx 699 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/src/pages/setting/user/index.jsx 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/src/pages/setting/user/index.less 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/src/pages/setting/user/service.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/src/requestErrorConfig.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/components/voiceInputPopup.vue
@@ -1,25 +1,32 @@
<template>
  <u-popup :show="show" mode="bottom" :closeOnClickOverlay="false" @close="closePopup" zIndex="10071">
  <u-popup :show="show" mode="bottom" :closeOnClickOverlay="false" @close="closePopup" :zIndex="9999" :overlay="true"
    :safeAreaInsetBottom="true">
    <view class="voice-popup">
      <view class="header">
        <text class="pl-30">语音输入</text>
        <image src="/static/Appeal/close.png" class="w-34 h-34 mr-30" mode="" @click="closePopup"></image>
      </view>
      <view class="record-anim">
        <image src="/static/Appeal/step.png" class="w-153 h-119" mode=""  @click="onPlay"></image>
        <image src="/static/Appeal/step.png" class="w-153 h-119" mode=""></image>
      </view>
      <view class="timer">{{ time }}</view>
      <view class="btns">
        <image src="/static/Appeal/start.png" class="w-153 h-153 mr-14" mode=""  @click="onPlay"></image>
        <image src="/static/Appeal/stop.png" class="w-153 h-153 mr-14" mode="" @click="onPause"></image>
        <image :src="getRecordButtonSrc" class="w-153 h-153 mr-14" mode="" @click="handlerOnCahnger"></image>
        <image src="/static/Appeal/cancel.png" class="w-153 h-153 mr-14" mode="" @click="onStop"></image>
      </view>
    </view>
    <mumu-recorder ref="mumuRecorder" @success="handlerSuccess" @error="handleError"></mumu-recorder>
  </u-popup>
</template>
<script>
import { getSignature } from '../pages/index/service'
import MumuRecorder from '@/uni_modules/mumu-recorder/components/mumu-recorder/mumu-recorder.vue'
export default {
  name: 'VoiceInputPopup',
  components: {
    MumuRecorder
  },
  props: {
    show: {
      type: Boolean,
@@ -29,16 +36,317 @@
  data() {
    return {
      time: '00:00:00',
      barHeights: [20, 30, 40, 30, 20], // 可做动画
      isRecording: false,
      isPaused: false,
      tempFilePath: '',
      timer: null,
      seconds: 0,
      minutes: 0,
      hours: 0,
      recordSegments: [], // 存储录音片段
      status: false,
      recorder: null,
      currentSegment: null // 当前录音片段
    }
  },
  computed: {
    getRecordButtonSrc() {
      if (this.isPaused) {
        return '/static/Appeal/start.png'
      }
      return this.isRecording ? '/static/Appeal/stop.png' : '/static/Appeal/start.png'
    }
  },
  mounted() {
  },
  methods: {
    closePopup() {
      this.$emit('update:show', false)
    handlerSave() {
      let tag = document.createElement('a')
      tag.href = this.recorder.localUrl
      tag.download = '录音'
      tag.click()
    },
    onPlay() {},
    onPause() {},
    onStop() {},
    handlerOnCahnger() {
      if (!this.isRecording && !this.isPaused) {
        // 开始新的录音
        this.startNewRecording()
      } else if (this.isRecording) {
        // 暂停当前录音
        this.pauseRecording()
      } else if (this.isPaused) {
        // 继续录音
        this.resumeRecording()
      }
    },
    startNewRecording() {
      console.log('开始新的录音')
      this.isRecording = true
      this.isPaused = false
      this.startTimer()
      this.$refs.mumuRecorder.start()
    },
    pauseRecording() {
      console.log('暂停录音')
      this.isRecording = false
      this.isPaused = true
      this.stopTimer()
      this.$refs.mumuRecorder.stop()
    },
    resumeRecording() {
      console.log('继续录音')
      this.isRecording = true
      this.isPaused = false
      this.startTimer()
      this.$refs.mumuRecorder.start()
    },
    handlerSuccess(res) {
      console.log('录音成功回调:', res)
      this.recorder = res
      // 保存当前录音片段
      if (res.localUrl) {
        console.log('当前录音片段URL:', res.localUrl)
        console.log('当前已存储的片段数:', this.recordSegments.length)
        console.log('当前片段:', this.currentSegment)
        // 只有当当前片段与上一个不同时才添加
        if (!this.currentSegment || this.currentSegment.url !== res.localUrl) {
          console.log('添加新的录音片段')
          this.currentSegment = {
            url: res.localUrl,
            data: res.data,
            duration: this.seconds + this.minutes * 60 + this.hours * 3600
          }
          this.recordSegments.push(this.currentSegment)
          console.log('更新后的片段列表:', this.recordSegments)
        } else {
          console.log('跳过重复的录音片段')
        }
      }
    },
    handlerError(code) {
      switch (code) {
        case '101':
          uni.showModal({
            content: '当前浏览器版本较低,请更换浏览器使用,推荐在微信中打开。'
          })
          break;
        case '201':
          uni.showModal({
            content: '麦克风权限被拒绝,请刷新页面后授权麦克风权限。'
          })
          break
        default:
          uni.showModal({
            content: '未知错误,请刷新页面重试'
          })
          break
      }
    },
    closePopup() {
      // 清空录音片段
      this.recordSegments = []
      this.currentSegment = null
      // 重置计时器
      this.seconds = 0
      this.minutes = 0
      this.hours = 0
      this.updateTimeDisplay()
      // 关闭弹窗
      this.$emit('close')
    },
    startTimer() {
      this.stopTimer();
      this.timer = setInterval(() => {
        this.seconds++;
        if (this.seconds >= 60) {
          this.seconds = 0;
          this.minutes++;
          if (this.minutes >= 60) {
            this.minutes = 0;
            this.hours++;
          }
        }
        this.updateTimeDisplay();
      }, 1000);
    },
    stopTimer() {
      if (this.timer) {
        clearInterval(this.timer);
        this.timer = null;
      }
    },
    updateTimeDisplay() {
      this.time = `${String(this.hours).padStart(2, '0')}:${String(this.minutes).padStart(2, '0')}:${String(this.seconds).padStart(2, '0')}`;
    },
    // 合并音频文件
    async mergeAudioFiles(audioUrls) {
      try {
        const audioContext = new (window.AudioContext || window.webkitAudioContext)();
        const audioBuffers = [];
        // 加载所有音频文件
        for (const url of audioUrls) {
          const response = await fetch(url);
          const arrayBuffer = await response.arrayBuffer();
          const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
          audioBuffers.push(audioBuffer);
        }
        // 计算总时长
        const totalLength = audioBuffers.reduce((acc, buffer) => acc + buffer.length, 0);
        // 创建新的音频缓冲区
        const mergedBuffer = audioContext.createBuffer(
          audioBuffers[0].numberOfChannels,
          totalLength,
          audioBuffers[0].sampleRate
        );
        // 合并音频数据
        let offset = 0;
        for (const buffer of audioBuffers) {
          for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
            const channelData = buffer.getChannelData(channel);
            mergedBuffer.copyToChannel(channelData, channel, offset);
          }
          offset += buffer.length;
        }
        // 将合并后的音频转换为 Blob
        const wavBlob = await this.audioBufferToWav(mergedBuffer);
        const mergedUrl = URL.createObjectURL(wavBlob);
        return mergedUrl;
      } catch (error) {
        console.error('合并音频失败:', error);
        throw error;
      }
    },
    // 将 AudioBuffer 转换为 WAV 格式
    audioBufferToWav(buffer) {
      const numOfChan = buffer.numberOfChannels;
      const length = buffer.length * numOfChan * 2;
      const buffer2 = new ArrayBuffer(44 + length);
      const view = new DataView(buffer2);
      const channels = [];
      let sample;
      let offset = 0;
      let pos = 0;
      // 写入 WAV 文件头
      setUint32(0x46464952);                         // "RIFF"
      setUint32(36 + length);                        // 文件长度
      setUint32(0x45564157);                         // "WAVE"
      setUint32(0x20746d66);                         // "fmt " chunk
      setUint32(16);                                 // 长度 = 16
      setUint16(1);                                  // PCM (uncompressed)
      setUint16(numOfChan);
      setUint32(buffer.sampleRate);
      setUint32(buffer.sampleRate * 2 * numOfChan);  // avg. bytes/sec
      setUint16(numOfChan * 2);                      // block-align
      setUint16(16);                                 // 16-bit
      setUint32(0x61746164);                         // "data" - chunk
      setUint32(length);                             // chunk length
      // 写入音频数据
      for (let i = 0; i < buffer.numberOfChannels; i++) {
        channels.push(buffer.getChannelData(i));
      }
      while (pos < buffer.length) {
        for (let i = 0; i < numOfChan; i++) {
          sample = Math.max(-1, Math.min(1, channels[i][pos]));
          sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0;
          view.setInt16(44 + offset, sample, true);
          offset += 2;
        }
        pos++;
      }
      return new Blob([buffer2], { type: 'audio/wav' });
      function setUint16(data) {
        view.setUint16(pos, data, true);
        pos += 2;
      }
      function setUint32(data) {
        view.setUint32(pos, data, true);
        pos += 4;
      }
    },
    // 修改 onStop 方法
    async onStop() {
      console.log('停止录音')
      // 如果正在录音,先停止当前录音
      if (this.isRecording) {
        this.$refs.mumuRecorder.stop()
      }
      // 停止计时
      this.stopTimer()
      // 重置状态
      this.isRecording = false
      this.isPaused = false
      this.currentSegment = null
      // 处理录音片段
      if (this.recordSegments.length > 0) {
        try {
          uni.showLoading({
            title: '正在处理音频...'
          })
          // 获取所有录音片段的URL
          const audioUrls = this.recordSegments.map(segment => segment.url)
          console.log('准备合并的音频片段列表:', audioUrls)
          console.log('音频片段数量:', audioUrls.length)
          // 合并音频文件
          const mergedUrl = await this.mergeAudioFiles(audioUrls)
          // 创建合并后的音频数据对象
          const mergedAudioData = {
            url: this.recordSegments[this.recordSegments.length - 1].url,
            data: this.recordSegments[this.recordSegments.length - 1].data,
            duration: this.seconds + this.minutes * 60 + this.hours * 3600
          }
          uni.showToast({
            title: '录制成功',
            icon: 'success',
            duration: 2000
          })
          setTimeout(() => {
            this.$emit('submit', mergedAudioData)
          }, 2000)
          uni.hideLoading()
          this.closePopup()
        } catch (error) {
          console.error('音频合并失败:', error)
          uni.hideLoading()
          uni.showToast({
            title: '音频合并失败,请重试',
            icon: 'none',
            duration: 2000
          })
        }
      } else {
        uni.showToast({
          title: '未检测到录音文件',
          icon: 'none',
          duration: 2000
        })
      }
    }
  },
  beforeDestroy() {
    this.stopTimer();
  }
}
</script>
@@ -48,6 +356,9 @@
  background: #fff;
  border-radius: 24rpx 24rpx 0 0;
  padding: 40rpx 0 30rpx 0;
  position: relative;
  z-index: 9999;
  .header {
    display: flex;
    justify-content: center;
@@ -56,25 +367,30 @@
    font-size: 32rpx;
    font-weight: bold;
    margin-bottom: 80rpx;
    text {
      flex: 1;
      text-align: center;
    }
    .u-icon {
      position: absolute;
      right: 30rpx;
      top: 0;
    }
  }
  .record-anim {
    display: flex;
    justify-content: center;
    align-items: center;
    margin-bottom: 57rpx;
    .bars {
      display: flex;
      align-items: flex-end;
      height: 60rpx;
      .bar {
        width: 10rpx;
        margin: 0 4rpx;
@@ -84,26 +400,34 @@
      }
    }
  }
  .timer {
    text-align: center;
    font-size: 28rpx;
    color: #888;
    // margin-bottom: 30rpx;
  }
  .btns {
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 40rpx 67rpx 76rpx 67rpx;
    // .u-button {
    //   width: 100rpx;
    //   height: 100rpx;
    //   border-radius: 50%;
    //   display: flex;
    //   justify-content: center;
    //   align-items: center;
    //   margin: 0 10rpx;
    // }
    padding: 40rpx 67rpx 6rpx 67rpx;
  }
}
</style>
::v-deep .uni-toast {
  z-index: 10099 !important;
}
.uni-sample-toast {
  z-index: 10099 !important;
}
::v-deep .uni-toast .uni-sample-toast {
  z-index: 10099 !important;
}
/deep/ .u-transition.u-fade-enter-to.u-fade-enter-active {
  z-index: 997 !important;
}
</style>
H5/manifest.json
@@ -1,85 +1,88 @@
{
    "name" : "“三个身边”群众工作机制",
    "appid" : "__UNI__DB035F5",
    "description" : "",
    "versionName" : "1.0.0",
    "versionCode" : "100",
    "transformPx" : false,
    /* 5+App特有相关 */
    "app-plus" : {
        "usingComponents" : true,
        "nvueStyleCompiler" : "uni-app",
        "compilerVersion" : 3,
        "splashscreen" : {
            "alwaysShowBeforeRender" : true,
            "waiting" : true,
            "autoclose" : true,
            "delay" : 0
        },
        /* 模块配置 */
        "modules" : {},
        /* 应用发布信息 */
        "distribute" : {
            /* android打包配置 */
            "android" : {
                "permissions" : [
                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
                    "<uses-feature android:name=\"android.hardware.camera\"/>",
                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
                ]
            },
            /* ios打包配置 */
            "ios" : {},
            /* SDK配置 */
            "sdkConfigs" : {}
        }
    },
    "sassImplementationName" : "node-sass",
    /* 快应用特有相关 */
    "quickapp" : {},
    /* 小程序特有相关 */
    "mp-weixin" : {
        "appid" : "",
        "setting" : {
            "urlCheck" : false
        },
        "usingComponents" : true
    },
    "mp-alipay" : {
        "usingComponents" : true
    },
    "mp-baidu" : {
        "usingComponents" : true
    },
    "mp-toutiao" : {
        "usingComponents" : true
    },
    "uniStatistics" : {
        "enable" : false
    },
    "vueVersion" : "2",
    "h5" : {
        "template" : "index.html",
        "title" : "“三个身边”群众工作机制",
        "optimization" : {
            "treeShaking" : {
                "enable" : false
            }
        },
        "router" : {
            "base" : "./"
        }
    }
}
    "name": "“三个身边”群众工作机制",
    "appid": "__UNI__DB035F5",
    "description": "",
    "versionName": "1.0.0",
    "versionCode": "100",
    "transformPx": false,
    /* 5+App特有相关 */
    "app-plus": {
        "usingComponents": true,
        "nvueStyleCompiler": "uni-app",
        "compilerVersion": 3,
        "splashscreen": {
            "alwaysShowBeforeRender": true,
            "waiting": true,
            "autoclose": true,
            "delay": 0
        },
        /* 模块配置 */
        "modules": {},
        /* 应用发布信息 */
        "distribute": {
            /* android打包配置 */
            "android": {
                "permissions": [
                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
                    "<uses-feature android:name=\"android.hardware.camera\"/>",
                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
                ]
            },
            /* ios打包配置 */
            "ios": {},
            /* SDK配置 */
            "sdkConfigs": {}
        }
    },
    "sassImplementationName": "node-sass",
    /* 快应用特有相关 */
    "quickapp": {},
    /* 小程序特有相关 */
    "mp-weixin": {
        "appid": "",
        "setting": {
            "urlCheck": false
        },
        "usingComponents": true
    },
    "mp-alipay": {
        "usingComponents": true
    },
    "mp-baidu": {
        "usingComponents": true
    },
    "mp-toutiao": {
        "usingComponents": true
    },
    "uniStatistics": {
        "enable": false
    },
    "vueVersion": "2",
    "h5": {
        "template": "index.html",
        "title": "“三个身边”群众工作机制",
        "optimization": {
            "treeShaking": {
                "enable": false
            }
        },
        "devServer": {
            "https": true
        },
        "router": {
            "base": "./"
        }
    }
}
H5/package.json
@@ -1,5 +1,10 @@
{
  "dependencies": {
    "echarts": "^5.6.0"
  },
  "permission": {
    "scope.record": {
      "desc": "录音功能需要使用您的录音权限"
    }
  }
}
H5/pages/Appeal/Appeal.vue
@@ -62,7 +62,7 @@
                <view class="flex a-center j-between">
                    <text class="w-108 fs-27 font-bold mr-85">问题描述</text>
                    <view class="flex a-center" @click="voiceInput">
                        <image src="/static/Appeal/yuyin.png" class="w-30 h-30 mr-11" mode=""></image>
                        <text class="fs-23 red">语音输入</text>
                    </view>
                </view>
@@ -154,9 +154,9 @@
import config from '@/config/index.js'
import voiceInputPopup from '@/components/voiceInputPopup.vue'
import {
        mapActions,
        mapState
    } from "vuex";
    mapActions,
    mapState
} from "vuex";
export default {
    components: {
@@ -178,14 +178,12 @@
            detailedAddress: '',
            descriptionTitle: '',
            descriptionContent: '',
            videoContent: [
                { url: 'xxx1', playing: false },
                { url: 'xxx2', playing: false }
            ],
            videoContent: [],
            latitude: '',
            longitude: '',
            images: [],
            videos: [],
            voiceFile: '',//语音文件多个逗号拼接
            userInfo: uni.getStorageSync('userInfo'), //个人信息
            voiceInputShow: false,
        };
@@ -202,7 +200,7 @@
        this.getproblem()
        this.time = dayjs().format('YYYY-MM-DD')
    },
    methods: {
        ...mapActions(["playRecording", "pausePlaying"]),
        onPlayRecording(index) {
@@ -224,7 +222,8 @@
            this.voiceInputShow = false;
        },
        submitVoiceInput(e) {
            this.videoContent.push(e);
            console.log('eeeeeeeeeeeeeeeeeee', e)
            this.videoContent.push({ url: e.url, data: e.data, playing: false });
            this.voiceInputShow = false;
        },
        previewImage(index) {
@@ -262,7 +261,55 @@
                })]
            }))
        },
        submit(type) {
        async submit(type) {
            if (this.videoContent.length > 0) {
                uni.showLoading({
                    title: '正在上传语音文件...'
                });
                const uploadPromises = this.videoContent.map(item => {
                    return new Promise((resolve, reject) => {
                        console.log('item.data', item.data)
                        uni.uploadFile({
                            url: config.imageUrl,
                            file: item.data,
                            name: 'file',
                            // fileType: 'audio/mpeg',
                            // filePath: item.url,
                            // name: 'file',
                            header: {
                                'Content-Type': 'multipart/form-data',
                                'Authorization': uni.getStorageSync('token')
                            },
                            success: (uploadFileRes) => {
                                const response = JSON.parse(uploadFileRes.data);
                                if (response.code === 200) {
                                    resolve(response.data);
                                } else {
                                    reject(new Error('上传失败'));
                                }
                            },
                            fail: (error) => {
                                reject(error);
                            }
                        });
                    });
                });
                try {
                    const uploadedUrls = await Promise.all(uploadPromises);
                    this.voiceFile = uploadedUrls.join(',');
                    console.log('this.voiceFile', this.voiceFile)
                    uni.hideLoading();
                } catch (error) {
                    uni.hideLoading();
                    uni.showToast({
                        title: '语音文件上传失败',
                        icon: 'error'
                    });
                    return;
                }
            }
            const preciseRegex = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/;
            if (!this.time) {
                uni.showToast({
@@ -308,6 +355,7 @@
                })
                return
            }
            const data = {
                time: this.time,
                problemType: this.problemType,
@@ -320,6 +368,7 @@
                descriptionContent: this.descriptionContent,
                images: this.images.join(','),
                videos: this.videos.join(','),
                voiceFile: this.voiceFile,
            }
            // 问题上报
            if (type == 1) {
@@ -382,6 +431,7 @@
                success: (res) => {
                    uni.showLoading()
                    console.log('res.tempFilePaths[0]', res.tempFilePaths[0])
                    uni.uploadFile({
                        url: config.imageUrl,
                        filePath: res.tempFilePaths[0],
H5/pages/add-progress/index.vue
@@ -236,7 +236,7 @@
                                timeout: 1000 * 45,
                                name: 'file',
                                header: {
                                    Authorization: cuni.getStorageSync('token')
                                    Authorization: uni.getStorageSync('token')
                                },
                                success: (res) => {
                                    if (JSON.parse(res.data).code == 200) {
H5/pages/supervision/edit-supervision-progress.vue
@@ -74,7 +74,7 @@
            </view>
        </view>
        <view @click.stop="submit" class="mt-38 fs-35 lh-96 br-48 txt-center font-bold bgcolor6 color4 fixed"
            style="width: calc(100% - 62rpx);bottom: calc(env(safe-area-inset-bottom) + 10rpx);">确认添加</view>
            style="width: calc(100% - 62rpx);bottom: calc(env(safe-area-inset-bottom) + 10rpx);">确认</view>
        <view class="btn-box"></view>
        <view class="safe-box"></view>
    </view>
@@ -82,7 +82,7 @@
<script>
    import {
        saveProcess,
        editProgress,
        getComplaintDetail
    } from './service'
    import config from '@/config/index.js'
@@ -97,10 +97,16 @@
                video: [],
                againCklicFlag: true,
                rulsFlag: false,
                progressId: ''
            }
        },
        onLoad(params) {
            this.complaintId = params.id
            this.describe = JSON.parse(params.data).describe
            this.progressId = JSON.parse(params.data).id
            this.video = JSON.parse(params.data).video.split(',')
            this.imgUrls = JSON.parse(params.data).imgUrl.split(',')
            this.localImageUrls = JSON.parse(params.data).imgUrl.split(',')
            getComplaintDetail({
                id: params.id
            }).then(res => {
@@ -122,12 +128,13 @@
                    })
                }
                let obj = {
                    id: this.progressId,
                    complaintId: this.complaintId,
                    describe: this.describe,
                    imgUrl: this.imgUrls.join(','),
                    video: this.video.join(',')
                }
                saveProcess(obj).then(res => {
                editProgress(obj).then(res => {
                    if (res.code == 200) {
                        uni.showToast({
                            title: '提交成功',
H5/pages/supervision/service.js
@@ -1,11 +1,25 @@
import request from '@/utils/request.js'
// 工单列表
export const getList = (params) => {
    return request.post(`/api/huacheng-sangeshenbian/applet/complaint/list`, params)
// 处理记录
export const getProcessingList = (data) => {
    return request.post(`/api/huacheng-sangeshenbian/applet/supervise/processing-record-page`, data)
}
// 问题驳回池
export const getRejectList = (data) => {
    return request.post(`/api/huacheng-sangeshenbian/applet/supervise/reject-record-page`, data)
}
// 工单详情
// 详情
export const getComplaintDetail = (params) => {
    return request.get('/api/huacheng-sangeshenbian/applet/complaint/detail', params)
}
// 编辑办理进度
export const editProgress = (params) => {
    return request.put('/api/huacheng-sangeshenbian/applet/complaint-progress/edit', params)
}
// 删除办理进度
export const delProgress = (id) => {
    return request.delete(`/api/huacheng-sangeshenbian/applet/complaint-progress/${id}`)
}
H5/pages/supervision/supervision-progress.vue
@@ -99,7 +99,8 @@
<script>
    import dayjs from '../../uni_modules/uview-ui/libs/util/dayjs'
    import {
        getComplaintDetail
        getComplaintDetail,
        delProgress
    } from './service'
    export default {
        data() {
@@ -129,7 +130,6 @@
            getComplaintDetail({
                id: this.id
            }).then(res => {
                this.info = {
                    ...res.data
                }
@@ -137,7 +137,23 @@
        },
        methods: {
            deleteProgress() {
                delProgress(this.row.id).then(res => {
                    uni.showToast({
                        icon: 'none',
                        mask: true,
                        title: '删除成功'
                    })
                    this.$refs.customPopup.show = false
                    setTimeout(() => {
                        getComplaintDetail({
                            id: this.id
                        }).then(res => {
                            this.info = {
                                ...res.data
                            }
                        })
                    }, 1500)
                })
            },
            toDelete(item) {
                this.row = item
@@ -145,7 +161,7 @@
            },
            toEdit(item) {
                uni.navigateTo({
                    url: `/pages/supervision/edit-supervision-progress?id=${this.id}`
                    url: `/pages/supervision/edit-supervision-progress?id=${this.id}&data=${JSON.stringify(item)}`
                })
            },
            callPhone(phoneNumber) {
H5/pages/supervision/supervision.vue
@@ -2,25 +2,23 @@
    <view class="content">
        <view class="bgColor2 shadow1 pb-19" style="position: sticky;top: 0;z-index: 999;">
            <view class="h-96 flex a-center fs-27  j-between txt-center  font-w400 color1">
                <view @click="changeType('1')" class="flex1 bgColor2 relative"
                    :class="searchParams.type == '1' && 'color2 font-bold'">
                <view @click="changeType(1)" class="flex1 bgColor2 relative" :class="type == 1 && 'color2 font-bold'">
                    <view class="relative zIndex1000">
                        处理记录
                    </view>
                    <view v-if="searchParams.type == '1'" class="bgColor1"></view>
                    <view v-if="type == 1" class="bgColor1"></view>
                </view>
                <view @click="changeType('2')" class="flex1 bgColor2 relative"
                    :class="searchParams.type == '2' && 'color2 font-bold'">
                <view @click="changeType(2)" class="flex1 bgColor2 relative" :class="type == 2 && 'color2 font-bold'">
                    <view class="relative zIndex1000">
                        问题驳回池
                    </view>
                    <view v-if="searchParams.type == '2'" class="bgColor1"></view>
                    <view v-if="type == 2" class="bgColor1"></view>
                </view>
            </view>
            <view class="flex a-center j-between mx-27 pl-38 border2">
                <view class="flex a-center flex1">
                    <image src="../../static/search.png" mode="widthFix" class="w-31 h-31 shrink0 mr-13" />
                    <input v-model="searchParams.searchStr" class="fs-27 flex1" placeholder="输入姓名、联系电话关键字搜索" />
                    <input v-model="searchParams.keyword" class="fs-27 flex1" placeholder="输入姓名、联系电话关键字搜索" />
                </view>
                <view @click="searchList" class="fs-23 lh-69 txt-center px-29 br-48 my-4 mx-4 bgcolor88">搜索</view>
            </view>
@@ -101,18 +99,18 @@
                <view class="mt-31 border1">
                    <view class="left"></view>
                    <view class="right"></view>
                    <view v-if="searchParams.type == '1'">
                    <view v-if="type == 1">
                        <view class="flex a-center j-between lh-38 mt-29 ml-38 mr-23 fs-27">
                            <view>添加办理进度数</view>
                            <view class="font-bold">10</view>
                            <view class="font-bold">{{item.progressCount}}</view>
                        </view>
                        <view class="flex a-center j-between lh-38 mt-29 ml-38 mr-23 fs-27">
                            <view>已下派次数</view>
                            <view class="font-bold">10</view>
                            <view class="font-bold">{{item.assignmentCount}}</view>
                        </view>
                        <view class="flex a-center j-between lh-38 mt-29 ml-38 mr-23 fs-27">
                            <view>已上报次数</view>
                            <view class="font-bold">10</view>
                            <view class="font-bold">{{item.reportCount}}</view>
                        </view>
                        <view class="flex a-center j-between lh-38 mt-29 ml-38 mr-23 fs-27">
                            <view>创建时间</view>
@@ -122,31 +120,32 @@
                    <view v-else>
                        <view class="flex a-center j-between lh-38 mt-29 ml-38 mr-23 fs-27">
                            <view>申请时间</view>
                            <view class="font-bold">2025-09-09 11:09:09</view>
                            <view class="font-bold">{{ item.applyTime | formatTime }}</view>
                        </view>
                        <view class="flex a-center j-between lh-38 mt-29 ml-38 mr-23 fs-27">
                            <view>申请人</view>
                            <view class="flex a-center">
                                <view class="font-bold">李雷 13987654321</view>
                                <image @click.stop="callPhone(item.contactNumber)" src="../../static/tell.png"
                                <view class="font-bold">{{item.reporter}} {{item.reporterPhone}}</view>
                                <image @click.stop="callPhone(item.reporterPhone)" src="../../static/tell.png"
                                    class="w-58 h-58 shrink0 ml-19" />
                            </view>
                        </view>
                        <view class="flex a-center j-between lh-38 mt-29 ml-38 mr-23 fs-27">
                            <view>审批时间</view>
                            <view class="font-bold">2025-09-09 11:09:09</view>
                            <view class="font-bold">{{ item.auditTime | formatTime }}</view>
                        </view>
                        <view class="flex a-center j-between lh-38 mt-29 ml-38 mr-23 fs-27">
                            <view>审批人</view>
                            <view class="flex a-center">
                                <view class="font-bold">李雷 13987654321</view>
                                <image @click.stop="callPhone(item.contactNumber)" src="../../static/tell.png"
                                <view class="font-bold">{{item.auditorName}} {{item.auditorPhone}}</view>
                                <image @click.stop="callPhone(item.auditorPhone)" src="../../static/tell.png"
                                    class="w-58 h-58 shrink0 ml-19" />
                            </view>
                        </view>
                        <view class="flex a-center j-between lh-38 mt-29 ml-38 mr-23 fs-27">
                        <view class="flex j-between lh-38 mt-29 ml-38 mr-23 fs-27">
                            <view class="shrink0">驳回理由</view>
                            <view style="max-width: 458rpx; text-align: right;font-weight: bold;">不同意当前申请不同意当前申请不同意当前申请
                            <view style="max-width: 458rpx; text-align: right;font-weight: bold;">
                                {{item.rejectReason}}
                            </view>
                        </view>
                    </view>
@@ -160,17 +159,17 @@
<script>
    import dayjs from '@/uni_modules/uview-ui/libs/util/dayjs.js'
    import {
        getList
        getProcessingList,
        getRejectList
    } from './service.js'
    export default {
        data() {
            return {
                active: 1,
                type: 1,
                searchParams: {
                    pageNum: 1,
                    pageSize: 10,
                    type: '1',
                    searchStr: ''
                    keyword: ''
                },
                list: [],
                status: 'loadMore',
@@ -185,7 +184,7 @@
                    '6': '上级驳回',
                    '7': '延期待审核',
                    '8': '已办结'
                }
                },
            }
        },
        onReachBottom() {
@@ -220,6 +219,14 @@
        },
        methods: {
            searchList() {
                if (this.searchParams.keyword == '') {
                    uni.showToast({
                        title: '请输入关键字搜索',
                        icon: 'none',
                        mask: true
                    })
                    return
                }
                this.searchParams.pageNum = 1
                this.fetchList(this.searchParams, (e) => {
                    this.list = e.records || []
@@ -242,24 +249,19 @@
            },
            fetchList(params, callback) {
                if (this.status == 'loading') return
                this.status = 'loading'
                getList(params).then(res => {
                this.status = 'loading';
                [getProcessingList, getRejectList][this.type - 1](params).then(res => {
                    if (res.code == 200) {
                        res.data.records.map(item => {
                            if (item.images) {
                                item.images = item.images.split(',')
                            }
                        })
                        callback(res.data)
                    }
                })
            },
            // 切换状态筛选
            changeType(type) {
                this.type = type
                this.searchParams = {
                    pageNum: 1,
                    pageSize: 10,
                    type, //全部:不传,上报待审核:0,正在办理:1, 办结:2
                }
                this.fetchList(this.searchParams, (e) => {
                    this.list = e.records || []
H5/pages/work-detail/work-detail.vue
@@ -165,9 +165,9 @@
            <!-- 问题描述 -->
            <view class="problem">
                <view class="title">问题描述</view>
                <view v-for="(item,index) in 2" :key="index"
                <view v-for="(item,index) in getVoiceFile(orderInfo.voiceFile)" :key="index"
                    class="flex a-center j-between py-17 px-19 br-8 bgcolor1 mb-19">
                    <view class="fs-27 lh-38">语音名字</view>
                    <view class="fs-27 lh-38">语音名字{{(index + 1) | numToWords}}</view>
                    <image v-if="!playFlag" @click.stop="playRecording(item)" src="../../static/24gf-playCircle@2x.png"
                        class="w-27 h-27 shrink0" />
                    <image v-else @click.stop="pausePlaying" src="../../static/pausePlaying.png"
@@ -432,6 +432,18 @@
            formatTime(val) {
                if (!val) return ''
                return dayjs(val).format('YYYY-MM-DD HH:mm:ss')
            },
            numToWords(val) {
                const words = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
                // 处理 0-10
                if (val >= 0 && val <= 10) return words[val];
                // 处理 11-99
                if (val > 10 && val < 100) {
                    const ten = Math.floor(val / 10);
                    const unit = val % 10;
                    return `${ten > 1 ? words[ten] : ''}十${unit > 0 ? words[unit] : ''}`;
                }
                return val; // 超过99返回原数字
            }
        },
        onShow() {
@@ -454,6 +466,10 @@
        },
        methods: {
            ...mapActions(["playRecording", "pausePlaying"]),
            getVoiceFile(voiceFile) {
                if (!voiceFile || voiceFile.length == 0) return []
                return voiceFile.split(',')
            },
            callPhone(phoneNumber) {
                uni.makePhoneCall({
                    phoneNumber
H5/uni_modules/mumu-recorder/changelog.md
New file
@@ -0,0 +1,4 @@
## 1.0.1(2022-06-11)
修复苹果手机在微信中无法获取音频长度问题
## 1.0.0(2022-06-10)
版本上线
H5/uni_modules/mumu-recorder/components/mumu-recorder/mumu-recorder.vue
New file
@@ -0,0 +1,113 @@
<template>
    <view class="recorder">
    </view>
</template>
<script>
    export default {
        data() {
            return {
                isUserMedia: false,
                stream: null,
                audio: null,
                recorder: null,
                chunks: [],
                startTime: 0
            }
        },
        mounted() {
            /**
             *     error 事件的返回状态
             *     100: 请在HTTPS环境中使用
             *     101: 浏览器不支持
             *  201: 用户拒绝授权
             *  500: 未知错误
             * */
            if (origin.indexOf('https') === -1) {
                this.$emit('error', '100')
                throw '请在 https 环境中使用本插件。'
            }
            if (!navigator.mediaDevices || !window.MediaRecorder) {
                this.$emit('error', '101')
                throw '当前浏览器不支持'
            }
            this.getRecorderManager()
        },
        methods: {
            getRecorderManager() {
                this.audio = document.createElement('audio')
                navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
                    this.isUserMedia = true
                    stream.getTracks().forEach((track) => {
                        track.stop()
                    })
                }).catch(err => {
                    this.onErrorHandler(err)
                })
            },
            start() {
                if (!this.isUserMedia) return console.log('设备不支持')
                navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
                    this.startTime = new Date().getTime()
                    this.stream = stream
                    this.recorder = new MediaRecorder(stream)
                    this.recorder.ondataavailable = this.getRecordingData
                    this.recorder.onstop = this.saveRecordingData
                    this.recorder.start()
                }).catch(err => {
                    this.onErrorHandler(err)
                })
            },
            stop() {
                this.recorder.stop()
                this.stream.getTracks().forEach((track) => {
                    track.stop()
                })
            },
            getRecordingData(e) {
                this.chunks.push(e.data)
            },
            saveRecordingData() {
                const blob = new Blob(this.chunks, { 'type': 'audio/mpeg' }),
                    localUrl = URL.createObjectURL(blob)
                const endTime = new Date().getTime()
                let duration = (endTime - this.startTime).toString().split('')
                duration.splice(duration.length - 2)
                duration.splice(duration.length - 1, 0, '.')
                duration = parseFloat(duration.join(''))
                const recorder = {
                    data: blob,
                    duration: duration,
                    localUrl: localUrl
                }
                this.$emit('success', recorder)
            },
            onErrorHandler(err) {
                console.log(err)
                if (err.name === 'NotAllowedError') {
                    this.$emit('error', '201')
                    throw '用户拒绝了当前的浏览器实例的访问请求'
                }
                if (err.name === 'NotReadableError') {
                    this.$emit('error', '101')
                    throw '当前浏览器不支持'
                }
                this.$emit('error', '500')
                throw '调用失败,原因不详'
            }
        },
        destroyed() {
            this.stop()
        }
    }
</script>
<style>
</style>
H5/uni_modules/mumu-recorder/package.json
New file
@@ -0,0 +1,87 @@
{
  "id": "mumu-recorder",
  "displayName": "h5录音组件,调用H5原生功能使用麦克风进行录音",
  "version": "1.0.1",
  "description": "演示案例中模仿了微信的长按发送语音,与普通录音demo。",
  "keywords": [
    "录音",
    "麦克风",
    "模仿微信"
],
  "repository": "",
  "engines": {
    "HBuilderX": "^3.1.0"
  },
  "dcloudext": {
    "category": [
        "前端组件",
        "通用组件"
    ],
    "sale": {
      "regular": {
        "price": "0.00"
      },
      "sourcecode": {
        "price": "0.00"
      }
    },
    "contact": {
      "qq": ""
    },
    "declaration": {
      "ads": "无",
      "data": "无",
      "permissions": "麦克风"
    },
    "npmurl": ""
  },
  "uni_modules": {
    "dependencies": [],
    "encrypt": [],
    "platforms": {
      "cloud": {
        "tcb": "y",
        "aliyun": "y"
      },
      "client": {
        "Vue": {
          "vue2": "y",
          "vue3": "y"
        },
        "App": {
          "app-vue": "n",
          "app-nvue": "n"
        },
        "H5-mobile": {
          "Safari": "y",
          "Android Browser": "y",
          "微信浏览器(Android)": "y",
          "QQ浏览器(Android)": "y"
        },
        "H5-pc": {
          "Chrome": "y",
          "IE": "n",
          "Edge": "y",
          "Firefox": "y",
          "Safari": "y"
        },
        "小程序": {
          "微信": "u",
          "阿里": "u",
          "百度": "u",
          "字节跳动": "u",
          "QQ": "u",
          "钉钉": "u",
          "快手": "u",
          "飞书": "u",
          "京东": "u",
          "小红书": "u"
        },
        "快应用": {
          "华为": "u",
          "联盟": "u"
        }
      }
    }
  }
}
H5/uni_modules/mumu-recorder/readme.md
New file
@@ -0,0 +1,117 @@
## 插件简绍
### 实现原理
> 通过 navigator.mediaDevices.getUserMedia(需要https环境) 这个api调用麦克风,获取到到音频流数据。
>
> 通过 MediaRecorder 这个构造函数对音频流进行接收,完成录制后会返回一个存储`Blob`内容的录制数据。
### 使用环境
需要https环境才能使用,本地测试可以在 manifest.json  中点击源码展示,找到h5 ,添加:"devServer" : { "https" : true}
**请勿使用 UC浏览器 与 夸克等阿里旗下的浏览器,发现他们使用的内核都较低,无法正常获取音频流,并且都有对接音频流截取的插件,导致无法正常获取音频流的数据。在微信中可以正常使用,推荐在微信内打开演示案例 **
需要https环境才能使用!!!
需要https环境才能使用!!!
需要https环境才能使用!!!
### 插件使用
**插件已支持 uni_modules 支持组件easycom,以下代码演示的是普通使用**
``` html
<!-- HTML -->
    <view>
    <audio :src='recorder.localUrl' v-if='recorder' name='本地录音' controls="true"></audio>
    <view @click='handlerOnCahnger'>
            {{!status?'开始录音':'结束录音'}}
        </view>
        <mumu-recorder ref='recorder' @success='handlerSuccess' @error='handlerError'></mumu-recorder>
    </view>
```
``` javascript
// js
    import MumuRecorder from '@/uni_modules/mumu-recorder/components/mumu-recorder/mumu-recorder.vue'
    export default {
        components: { MumuRecorder },
        data() {
            return {
                status: false,
                recorder: null
            }
        },
        onLoad() {
        },
        methods: {
            handlerSave() {
                let tag = document.createElement('a')
                tag.href = this.recorder.localUrl
                tag.download = '录音'
                tag.click()
            },
            handlerOnCahnger() {
                if (this.status) {
                    this.$refs.recorder.stop()
                } else {
                    this.$refs.recorder.start()
                }
                this.status = !this.status
            },
            handlerSuccess(res) {
                console.log(res)
                this.recorder = res
            },
            handlerError(code) {
                switch (code) {
                    case '101':
                        uni.showModal({
                            content: '当前浏览器版本较低,请更换浏览器使用,推荐在微信中打开。'
                        })
                        break;
                    case '201':
                        uni.showModal({
                            content: '麦克风权限被拒绝,请刷新页面后授权麦克风权限。'
                        })
                        break
                    default:
                        uni.showModal({
                            content: '未知错误,请刷新页面重试'
                        })
                        break
                }
            }
        }
    }
```
### 相关API
##### 组件内部方法($refs 调用)
| 方法名 | 说明     | 参数 |
| ------ | -------- | ---- |
| start  | 开始录音 | 无   |
| stop   | 结束录音 | 无   |
##### 事件(Events)
| 事件名  | 说明                 | 回调参数                                                     |
| ------- | -------------------- | ------------------------------------------------------------ |
| success | 停止录音后调用此事件 | 返回录音数据,是一个对象<br />{ data: 音频的 blob 数据,上传请使用这个            <br />duration: 当前音频长度<br/>localUrl: 当前音频的本地链接,可直接通过 audio 标签进行播放 } |
| error   | 组件内部发生错误     | 错误码:<100 当前不是https环境> <101 浏览器不支持> <201 麦克风权限被拒绝> <500  未知错误> |
### 案例演示
![enter description here](https://h5plugin.mumudev.top/public/recorder/qrcode.png)
## 支持作者
![支持作者](https://student.mumudev.top/wxMP.jpg)
H5/utils/request.js
@@ -19,9 +19,9 @@
    return new Promise(function(resolve, reject) {
        let token = uni.getStorageSync('token')
        // token =
        //     'Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOjE2MjA0LCJ0eXBlIjoxLCJleHAiOjE3NDM1NTY3NzYsImNyZWF0ZWQiOjE3NDIyNjA3NzY2NTR9.SAmdlprtz_Z1cNSNB4ANYsKJFC7E7Jfxo6XbT9vpbO6zHBpTRfq_oyp9UlDQtHNiYgv-MuyqCBPw1-x88C_-8A'
        // uni.setStorageSync('token', token)
        token =
            'Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOjE4OTU1MjI1Nzk1MDcwODEyMTgsInR5cGUiOjEsImV4cCI6MTc0Nzk4MzIyMiwiY3JlYXRlZCI6MTc0NjY4NzIyMjAxMX0.pmUfTkxkbBirDMbnMR1IaLsbSiiwHc366_yyAetCzTOWYxTNgmQlmvw26_W62NHLOebB_ZAEgZsPvkJcaLOoPg'
        uni.setStorageSync('token', token)
        let header = {
            'content-type': type ? 'application/x-www-form-urlencoded;charset=UTF-8' : 'application/json',
            'Authorization': token,
management/config/routes.ts
@@ -52,7 +52,7 @@
        access: '/system_setting/position_management',
      },
      {
        name: '角色管理',
        name: '权限管理',
        path: '/setting/role',
        component: './setting/role',
        access: '/system_setting/role_management',
management/src/pages/appeal-management/detail.jsx
@@ -1,13 +1,14 @@
import { buildProTableDataSource, sendRequest, showDelConfirm } from '@/utils/antdUtils';
import { PageContainer, ProTable } from '@ant-design/pro-components';
import { Button, Card, Select, Space, Descriptions, Divider, Steps, message } from 'antd';
import { Button, Card, Select, Space, Descriptions, Divider, Steps, message, Modal, Form, Input, Upload, Popconfirm } from 'antd';
import { useRef, useState, useEffect } from 'react';
import { Access, useAccess } from 'umi'
import { history, useLocation } from "@umijs/max"
import { getDetail } from './service';
import { getDetail, editProgress, deleteProgress } from './service';
import moment from 'moment';
import './index.less';
import {downLoad } from '@/utils/utils';
import {downLoad, customRequest } from '@/utils/utils';
import { PlusOutlined } from '@ant-design/icons';
const Account = () => {
    const actionRef = useRef();
@@ -16,6 +17,22 @@
    const [detail, setDetail] = useState({});
    const searchParams = new URLSearchParams(useLocation().search);
    const id = searchParams.get('id');
    const [editModalVisible, setEditModalVisible] = useState(false);
    const [editProgressData, setEditProgressData] = useState(null);
    const [editForm] = Form.useForm();
    const [loading, setLoading] = useState(false);
    const [imgFileList, setImgFileList] = useState([]);
    const [videoFileList, setVideoFileList] = useState([]);
    const [previewVisible, setPreviewVisible] = useState(false);
    const [previewImage, setPreviewImage] = useState('');
    const [previewType, setPreviewType] = useState('image'); // 'image' or 'video'
    const uploadConfig = {
        name: 'file',
        action: 'https://huacheng.psciio.com/api/huacheng-communitybackstage/communitypartybuilding/uploadimage',
        headers: {
            Authorization: 'Bearer ' + localStorage.getItem('token'),
        },
    };
    useEffect(() => {
        getDetail({ id: id }).then((res) => {
@@ -111,6 +128,117 @@
            span: 4,
        },
    ];
    // 上传前校验
    const beforeUpload = (file) => {
        return new Promise((resolve, reject) => {
            if (file.name.includes(',')) {
                message.warning('上传文件不能包含英文逗号(,)');
                return Upload.LIST_IGNORE;
            }
            setLoading(true);
            resolve(file);
        });
    };
    // 图片上传change
    const handleImgChange = ({ file, fileList: newFileList }) => {
        if (file.status === 'error' || (file.status === 'done' && file.response && file.response.code !== 200)) {
            setLoading(false);
            setImgFileList([]);
            message.error('上传失败');
            editForm.setFieldValue('imgUrl', []);
            return;
        }
        if (file.status === 'done' && file.response && file.response.code === 200) {
            setLoading(false);
            message.success('上传成功');
        }
        let urls = newFileList.map(item => {
            if (item.status === 'done' && item.response && item.response.data) {
                item.url = item.response.data;
            }
            return item.url || item.url;
        }).filter(Boolean);
        setImgFileList(newFileList);
        editForm.setFieldValue('imgUrl', urls);
    };
    // 视频上传change
    const handleVideoChange = ({ file, fileList: newFileList }) => {
        if (file.status === 'error' || (file.status === 'done' && file.response && file.response.code !== 200)) {
            setLoading(false);
            setVideoFileList([]);
            message.error('上传失败');
            editForm.setFieldValue('videoUrl', []);
            return;
        }
        if (file.status === 'done' && file.response && file.response.code === 200) {
            setLoading(false);
            message.success('上传成功');
        }
        let urls = newFileList.map(item => {
            if (item.status === 'done' && item.response && item.response.data) {
                item.url = item.response.data;
            }
            return item.url || item.url;
        }).filter(Boolean);
        setVideoFileList(newFileList);
        editForm.setFieldValue('videoUrl', urls);
    };
    // 办理进度编辑提交
    const handleEditProgress = async () => {
        try {
            setLoading(true);
            console.log('editProgressData', editProgressData);
            const values = await editForm.validateFields();
            await editProgress({
                complaintId: id,
                describe: values.describe,
                id: editProgressData.id,
                imgUrl: (values.imgUrl || []).join(','),
                video: (values.videoUrl || []).join(',')
            });
            message.success('编辑成功');
            setEditModalVisible(false);
            getDetail({ id }).then((res) => setDetail(res.data));
        } catch (e) {
            // 校验或请求失败
        } finally {
            setLoading(false);
        }
    };
    // 办理进度删除
    const handleDeleteProgress = async (progressId) => {
        try {
            await deleteProgress({ id: progressId });
            message.success('删除成功');
            getDetail({ id }).then((res) => setDetail(res.data));
        } catch (e) {
            message.error('删除失败');
        }
    };
    // 在弹窗打开时同步 fileList
    useEffect(() => {
        if (editModalVisible && editProgressData) {
            setImgFileList((editProgressData.imgUrl ? editProgressData.imgUrl.split(',') : []).map((url, idx) => ({ uid: idx, url, status: 'done' })));
            setVideoFileList((editProgressData.video ? editProgressData.video.split(',') : []).map((url, idx) => ({ uid: idx, url, status: 'done' })));
            editForm.setFieldsValue({
                describe: editProgressData.describe,
                imgUrl: editProgressData.imgUrl ? editProgressData.imgUrl.split(',') : [],
                videoUrl: editProgressData.video ? editProgressData.video.split(',') : [],
            });
        }
        if (!editModalVisible) {
            setImgFileList([]);
            setVideoFileList([]);
        }
    }, [editModalVisible, editProgressData]);
    return (
        <div>
            <PageContainer className={'appeal-management-detail'} header={{ breadcrumb: {} }} title="述求详情" >
@@ -121,22 +249,34 @@
                        <Descriptions title="办理进度" column={1}>
                            {detail.complaintProgresses?.length > 0 ? detail.complaintProgresses.map((item, index) => (
                                <Descriptions.Item key={index}>
                                    <Card style={{ width: '800px' }} title={item.createByName} extra={moment(item.createTime).format('YYYY-MM-DD HH:mm:ss')}>
                                    <Card style={{ width: '800px', position: 'relative' }} title={item.createByName} extra={moment(item.createTime).format('YYYY-MM-DD HH:mm:ss')}>
                                        <Descriptions column={1} >
                                            <Descriptions.Item>
                                                {item.describe}
                                            </Descriptions.Item>
                                            <Descriptions.Item>{item.describe}</Descriptions.Item>
                                            <Descriptions.Item label={'上传图片'}>
                                                {item.imgUrl && (item.imgUrl || '').split(',').map((item, index) => (
                                                    <img width={80} style={{ marginRight: '10px' }} height={80} src={item} key={index} alt="example" />
                                                {item.imgUrl && (item.imgUrl || '').split(',').map((img, idx) => (
                                                    <img width={80} style={{ marginRight: '10px' }} height={80} src={img} key={idx} alt="example" />
                                                ))}
                                            </Descriptions.Item>
                                            <Descriptions.Item label={'上传视频'}>
                                                {item.videoUrl && (item.videoUrl || '').split(',').map((item, index) => (
                                                    <video width={280} style={{ marginRight: '10px' }} src={item} key={index} controls></video>
                                                {item.video && item.video.split(',').map((video, idx) => (
                                                    <video width={280} style={{ marginRight: '10px' }} src={video} key={idx} controls></video>
                                                ))}
                                            </Descriptions.Item>
                                        </Descriptions>
                                        <div style={{ position: 'absolute', right: 24, bottom: 16 }}>
                                            <Button size="small" type="primary" style={{ marginRight: 8 }} onClick={() => {
                                                setEditProgressData(item);
                                                setEditModalVisible(true);
                                                editForm.setFieldsValue({
                                                    describe: item.describe,
                                                    imgUrl: item.imgUrl ? item.imgUrl.split(',') : [],
                                                    videoUrl: item.video ? item.video.split(',') : [],
                                                });
                                            }}>编辑</Button>
                                            <Popconfirm title="确定要删除该办理进度吗?" onConfirm={() => handleDeleteProgress(item.id)} okText="确定" cancelText="取消">
                                                <Button size="small" danger>删除</Button>
                                            </Popconfirm>
                                        </div>
                                    </Card>
                                </Descriptions.Item>
                            )) : <Descriptions.Item span={4} >暂无办理进度</Descriptions.Item>}
@@ -252,7 +392,80 @@
                    </div>
                </Card>
                <Modal
                    title="编辑办理进度"
                    open={editModalVisible}
                    onCancel={() => setEditModalVisible(false)}
                    onOk={handleEditProgress}
                    confirmLoading={loading}
                    destroyOnClose
                    width={600}
                >
                    <Form form={editForm} layout="vertical" initialValues={{ describe: '', imgUrl: [], videoUrl: [] }}>
                        <Form.Item name="describe" label="办理进度描述" rules={[{ required: true, message: '请输入办理进度描述' }]}>
                            <Input.TextArea rows={4} maxLength={500} showCount />
                        </Form.Item>
                        <Form.Item name="imgUrl" label="图片:">
                            <Upload
                                {...uploadConfig}
                                listType="picture-card"
                                maxCount={9}
                                beforeUpload={beforeUpload}
                                onChange={handleImgChange}
                                onRemove={() => {
                                    setImgFileList([]);
                                    editForm.setFieldValue('imgUrl', []);
                                }}
                                showUploadList={{ showPreviewIcon: true }}
                                accept="image/*"
                                fileList={imgFileList}
                                onPreview={file => {
                                    setPreviewType('image');
                                    setPreviewImage(file.url || file.thumbUrl);
                                    setPreviewVisible(true);
                                }}
                            >
                                {imgFileList.length < 9 && <PlusOutlined />}
                            </Upload>
                        </Form.Item>
                        <Form.Item name="videoUrl" label="视频:">
                            <Upload
                                {...uploadConfig}
                                listType="picture-card"
                                maxCount={9}
                                beforeUpload={beforeUpload}
                                onChange={handleVideoChange}
                                onRemove={() => {
                                    setVideoFileList([]);
                                    editForm.setFieldValue('videoUrl', []);
                                }}
                                showUploadList={{ showPreviewIcon: true }}
                                accept="video/*"
                                fileList={videoFileList}
                                onPreview={file => {
                                    setPreviewType('video');
                                    setPreviewImage(file.url || file.thumbUrl);
                                    setPreviewVisible(true);
                                }}
                            >
                                {videoFileList.length < 9 && <PlusOutlined />}
                            </Upload>
                        </Form.Item>
                    </Form>
                </Modal>
                <Modal
                    open={previewVisible}
                    footer={null}
                    onCancel={() => setPreviewVisible(false)}
                    width={previewType === 'video' ? 800 : 600}
                >
                    {previewType === 'image' ? (
                        <img alt="预览" style={{ width: '100%' }} src={previewImage} />
                    ) : (
                        <video style={{ width: '100%' }} src={previewImage} controls autoPlay />
                    )}
                </Modal>
            </PageContainer>
        </div>
    );
management/src/pages/appeal-management/service.js
@@ -15,3 +15,19 @@
        data
    });
}
//编辑办理进度
export const editProgress = async (data) => {
    return request(`/api/huacheng-sangeshenbian/complaint/update-progress`, {
        method: 'POST',
        data
    });
}
//删除办理进度
export const deleteProgress = async (data) => {
    return request(`/api/huacheng-sangeshenbian/complaint/del-progress/${data.id}`, {
        method: 'DELETE',
        data
    });
}
management/src/pages/appeal-management/statistics/index.jsx
@@ -7,14 +7,12 @@
import { getList } from './service';
const Account = () => {
    const actionRef = useRef();
    const access = useAccess();
    const formRef = useRef();
    const columns = [
        {
            title: '述求号',
            dataIndex: 'reportUserName',
            dataIndex: 'serialNumber',
            order: 8,
        },
        {
@@ -29,30 +27,30 @@
        },
        {
            title: '申请人',
            dataIndex: 'name',
            dataIndex: 'applyUserName',
            order: 5,
        },
        {
            title: '申请时间',
            dataIndex: 'name',
            dataIndex: 'applyTime',
            hideInTable: true,
            valueType: 'dateRange',
            order: 3,
        },
        {
            title: '审批时间',
            dataIndex: 'name',
            dataIndex: 'examineTime',
            valueType: 'dateRange',
            order: 2,
        },
        {
            title: '审批人',
            dataIndex: 'name',
            dataIndex: 'examineUserName',
            order: 4,
        },
        {
            title: '驳回理由',
            dataIndex: 'contactNumber',
            dataIndex: 'remark',
            hideInSearch: true,
        },
        {
@@ -64,11 +62,13 @@
                1: '延期办理',
                2: '超时办理',
                3: '已办结',
                4: '上报待审核',
                4: '群众撤销',
                5: '上报待审核',
                6: '上级驳回',
                7: '延期待审核',
                8: '已评价',
                9: '延期驳回',
            },
            render: (text, record) => {
                return Number(record.status) == 0 ? '正在办理' : record.status == 1 ? '延期办理' : record.status == 2 ? '超时办理' : record.status == 3 ? '已办结' : record.status == 4 ? '上报待审核' : '已办结';
            }
        },
        {
            title: '操作',
@@ -102,20 +102,16 @@
            >
                <ProTable
                    rowKey="id"
                    actionRef={actionRef}
                    columns={columns}
                    formRef={formRef}
                    request={async (params) => {
                        if (params.time && params.time.length > 0) {
                            params.startTime = moment(params.time[0]).format('YYYY-MM-DD HH:mm:ss');
                            params.endTime = moment(params.time[1]).format('YYYY-MM-DD 23:59:59');
                            delete params.time
                        } else {
                            delete params.startTime
                            delete params.endTime
                        if (params.applyTime && params.applyTime.length > 0) {
                            params.applyTime = moment(params.applyTime[0]).format('YYYY-MM-DD') +
                                ' - ' + moment(params.applyTime[1]).format('YYYY-MM-DD')
                        }
                        if (params.examineTime && params.examineTime.length > 0) {
                            params.examineTime = moment(params.examineTime[0]).format('YYYY-MM-DD') +
                                ' - ' + moment(params.examineTime[1]).format('YYYY-MM-DD')
                        }
                        return buildProTableDataSource(getList, params);
                    }}
management/src/pages/appeal-management/statistics/service.js
@@ -1,17 +1,8 @@
import { request } from '@umijs/max';
// 获取诉求列表
export const getList = async (data) => {
    return request(`/api/huacheng-sangeshenbian/complaint/page`, {
        method: 'POST',
        data
    });
}
// 获取述求详情
export const getDetail = async (data) => {
    return request(`/api/huacheng-sangeshenbian/complaint/detail/${data.id}`, {
export const getList = async (params) => {
    return request(`/api/huacheng-sangeshenbian/complaint-reject/list`, {
        method: 'GET',
        data
        params
    });
}
management/src/pages/logManagement/index.jsx
@@ -14,24 +14,24 @@
    const columns = [
        {
            title: '操作时间',
            dataIndex: 'name',
            dataIndex: 'createTime',
            hideInTable: true,
            valueType: 'dateRange',
            order: 1,
        },
        {
            title: '操作用户',
            dataIndex: 'reportUserName',
            dataIndex: 'operatorName',
            order: 5,
        },
        {
            title: '联系电话',
            dataIndex: 'reportUserName',
            dataIndex: 'operatorPhone',
            order: 4,
        },
        {
            title: '操作类型',
            dataIndex: 'reportUserPhone',
            dataIndex: 'operatorCategory',
            order: 3,
            valueEnum: {
                1: '登录',
@@ -48,45 +48,41 @@
        },
        {
            title: '对象名称',
            dataIndex: 'name',
            dataIndex: 'targetName',
            hideInSearch: true,
        },
        {
            title: '所在IP',
            dataIndex: 'name',
            dataIndex: 'ip',
            order: 2,
        },
    ];
    return (
        <div>
            <PageContainer header={{
                breadcrumb: {},
            }}
                title={'日志记录'}
            >
                <ProTable
                    rowKey="id"
                    actionRef={actionRef}
                    columns={columns}
                    formRef={formRef}
                    request={async (params) => {
                        if (params.time && params.time.length > 0) {
                            params.startTime = moment(params.time[0]).format('YYYY-MM-DD HH:mm:ss');
                            params.endTime = moment(params.time[1]).format('YYYY-MM-DD 23:59:59');
                            delete params.time
                        } else {
                            delete params.startTime
                            delete params.endTime
                        }
                        return buildProTableDataSource(getList, params);
                    }}
                    search={{ labelWidth: 'auto', defaultCollapsed: false }}
                />
            </PageContainer>
        </div>
        <PageContainer header={{
            breadcrumb: {},
        }}
            title={'日志记录'}
        >
            <ProTable
                rowKey="id"
                actionRef={actionRef}
                columns={columns}
                formRef={formRef}
                request={async (params) => {
                    if (params.time && params.time.length > 0) {
                        params.startTime = moment(params.time[0]).format('YYYY-MM-DD HH:mm:ss');
                        params.endTime = moment(params.time[1]).format('YYYY-MM-DD 23:59:59');
                        delete params.time
                    } else {
                        delete params.startTime
                        delete params.endTime
                    }
                    return buildProTableDataSource(getList, params);
                }}
                search={{ labelWidth: 'auto', defaultCollapsed: false }}
            />
        </PageContainer>
    );
};
export default Account;
export default Account;
management/src/pages/logManagement/service.js
@@ -1,17 +1,8 @@
import { request } from '@umijs/max';
// 获取诉求列表
export const getList = async (data) => {
    return request(`/api/huacheng-sangeshenbian/complaint/page`, {
        method: 'POST',
        data
    });
}
// 获取述求详情
export const getDetail = async (data) => {
    return request(`/api/huacheng-sangeshenbian/complaint/detail/${data.id}`, {
export const getList = async (params) => {
    return request(`/api/huacheng-sangeshenbian/system-log/list`, {
        method: 'GET',
        data
        params
    });
}
management/src/pages/party/manage/index.jsx
@@ -66,6 +66,18 @@
      }
    },
    {
      title: '创建时间',
      dataIndex: 'createTime',
      sorter: true,
      hideInSearch: true,
    },
    {
      title: '更新时间',
      dataIndex: 'updateTime',
      sorter: true,
      hideInSearch: true,
    },
    {
      title: '状态',
      dataIndex: 'freezeStatus',
      valueEnum: {
@@ -75,80 +87,84 @@
      }
    },
    {
      title: '身份证号',
      dataIndex: 'idNumber',
    },
    {
      title: '操作',
      hideInSearch: true,
      render: (text, record) => {
        return (
          <Space>
            <Access accessible={access['/party_member/edit']}>
            <Button
              type="link"
              onClick={() => {
                history.push(`/party/manage/add?type=edit&id=${record.id}`)
              }}
            >
              编辑
            </Button>
            </Access>
            <Access accessible={access['/party_member/del']}>
            <Button
              type="link"
              onClick={() => {
                showDelConfirm(async () => {
                  let status = await sendRequest(deleteBanner, {id:record.id})
                  if (status) {
                    actionRef.current.reload();
                  }
                }, '确认删除所选信息吗?');
              }}
            >
              删除
            </Button>
            </Access>
            <Access accessible={access['/party_member/detail']}>
            <Button
              type="link"
              onClick={() => {
                history.push(`/party/manage/add?detail=true&id=${record.id}`)
              }}
            >
              查看详情
            </Button>
            </Access>
            <Access accessible={access['/party_member/freeze']}>
            {record.freezeStatus == 0 && (
              <Button
                type="link"
                onClick={() => {
                  showDelConfirm1(async () => {
                    let status = await sendRequest(freeze, { id: record.id })
                  history.push(`/party/manage/add?type=edit&id=${record.id}`)
                }}
              >
                编辑
              </Button>
            </Access>
            <Access accessible={access['/party_member/del']}>
              <Button
                type="link"
                onClick={() => {
                  showDelConfirm(async () => {
                    let status = await sendRequest(deleteBanner, { id: record.id })
                    if (status) {
                      actionRef.current.reload();
                    }
                  }, '确认冻结该党员信息吗?', '冻结', '', '确认冻结该党员信息吗?');
                  }, '确认删除所选信息吗?');
                }}
              >
                冻结
                删除
              </Button>
            )}
            </Access>
            <Access accessible={access['/party_member/detail']}>
              <Button
                type="link"
                onClick={() => {
                  history.push(`/party/manage/add?detail=true&id=${record.id}`)
                }}
              >
                查看详情
              </Button>
            </Access>
            <Access accessible={access['/party_member/freeze']}>
              {record.freezeStatus == 0 && (
                <Button
                  type="link"
                  onClick={() => {
                    showDelConfirm1(async () => {
                      let status = await sendRequest(freeze, { id: record.id })
                      if (status) {
                        actionRef.current.reload();
                      }
                    }, '确认冻结该党员信息吗?', '冻结', '', '确认冻结该党员信息吗?');
                  }}
                >
                  冻结
                </Button>
              )}
            </Access>
            <Access accessible={access['/party_member/freeze']}>
            {record.freezeStatus == 1 && (
              <Button
                type="link"
                onClick={() => {
                  showDelConfirm1(async () => {
                    let status = await sendRequest(freeze, { id: record.id })
                    if (status) {
                      actionRef.current.reload();
                    }
                  }, '确认解冻该党员信息吗?', '解冻', '', '确认解冻该党员信息吗?');
                }}
              >
                解冻
              </Button>
            )}
              {record.freezeStatus == 1 && (
                <Button
                  type="link"
                  onClick={() => {
                    showDelConfirm1(async () => {
                      let status = await sendRequest(freeze, { id: record.id })
                      if (status) {
                        actionRef.current.reload();
                      }
                    }, '确认解冻该党员信息吗?', '解冻', '', '确认解冻该党员信息吗?');
                  }}
                >
                  解冻
                </Button>
              )}
            </Access>
          </Space >
@@ -175,7 +191,15 @@
          showQuickJumper: true,
          defaultPageSize: 10,
        }}
        request={(params) => {
        request={(params, sorter) => {
          console.log('weqweqeqwe', sorter);
          if (sorter.createTime) {
            params.createTimeSort = sorter.createTime === 'ascend' ? 2 : 1;
          }
          if (sorter.updateTime) {
            params.updateTimeSort = sorter.updateTime === 'ascend' ? 2 : 1;
          }
          params.communityId = params.community ? params.community[params.community.length - 1] : ''
          params.auditStatus = 1
          setExcelParams(() => params)
@@ -184,35 +208,35 @@
        toolBarRender={(action, selectRows) => [
          <Space>
            <Access accessible={access['/party_member/add']}>
            <Button
              type="primary"
              onClick={() => {
                history.push('/party/manage/add?type=add')
              }}
            >
              添加
            </Button>
              <Button
                type="primary"
                onClick={() => {
                  history.push('/party/manage/add?type=add')
                }}
              >
                添加
              </Button>
            </Access>
            <Access accessible={access['/party_member/export']}>
            <Button
              type="primary"
              onClick={() => {
                exportExcell('党员列表', excelParams, '/api/huacheng-sangeshenbian/party-member/export')
              }}
            >
              导出
            </Button>
              <Button
                type="primary"
                onClick={() => {
                  exportExcell('党员列表', excelParams, '/api/huacheng-sangeshenbian/party-member/export')
                }}
              >
                导出
              </Button>
            </Access>
            <Access accessible={access['/party_member/import']}>
            <Button
              type="primary"
              onClick={() => {
                handleModalExport(true)
                modalExportRef.current.clean()
              }}
            >
              导入
            </Button>
              <Button
                type="primary"
                onClick={() => {
                  handleModalExport(true)
                  modalExportRef.current.clean()
                }}
              >
                导入
              </Button>
            </Access>
          </Space>
        ]}
management/src/pages/setting/role/components/addAndEdit.jsx
@@ -128,7 +128,7 @@
            getContainer={false}
            width="20%"
            destroyOnClose
            title={detailType ? '角色详情' : data.id ? '编辑角色' : '添加角色'}
            title={detailType ? '权限详情' : data.id ? '编辑权限' : '添加权限'}
            open={visible}
            onCancel={() => onCancel(false)}
            afterClose={() => {
@@ -150,10 +150,10 @@
            <Form layout="horizontal" {...formItemLayout} form={form} scrollToFirstError>
                <Form.Item
                    name="name"
                    label="角色名称"
                    rules={[{ required: true, message: '请输入角色名称' }]}
                    label="权限名称"
                    rules={[{ required: true, message: '请输入权限名称' }]}
                >
                    <Input disabled={detailType} placeholder='请输入角色名称' />
                    <Input disabled={detailType} placeholder='请输入权限名称' />
                </Form.Item>
                <Form.Item
                    name="tree"
management/src/pages/setting/role/index.jsx
@@ -14,7 +14,7 @@
    const columns = [
        {
            title: '角色名称',
            title: '权限名称',
            dataIndex: 'name',
        },
        {
management/src/pages/setting/user/components/addAndEdit.jsx
@@ -1,7 +1,9 @@
import { Form, Input, Modal, Tree, Button, Spin, Row, Col, Select, Radio, Cascader } from 'antd';
import { Form, Input, Modal, Tree, Button, Spin, Row, Col, Select, Radio, Cascader, Card } from 'antd';
import { forwardRef, useImperativeHandle, useState } from 'react';
import { useEffect } from 'react';
import { getDepartmentList, systemPostList, systemRoleList, getSystemUserInfo, getCityList, addSystemUserInfo, editSystemUserInfo } from '../service';
import { DeleteOutlined } from '@ant-design/icons';
import '../index.less'
import { getDepartmentList, systemPostList, systemRoleList, getSystemUserInfo, getCityList, addSystemUserInfo, editSystemUserInfo, getCascaderData } from '../service';
import CryptoJS from 'crypto-js';
const formItemLayout = {
    labelCol: { span: 8 },
@@ -27,7 +29,10 @@
    //所属角色
    const [roleList, setRoleList] = useState([])
    //账号层级
    const [levelList, setLevelList] = useState([{ name: '市级账号', value: 1 }, { name: '区县账号', value: 2 }, { name: '街道账号', value: 3 }, { name: '社区账号', value: 4 }, { name: '党员账号', value: 5 }])
    const [levelList, setLevelList] = useState([])
    //级联数据
    const [cascaderData, setCascaderData] = useState([])
    //选择的层级
    const [activeLevel, setActiveLevel] = useState('')
    //所属区县
@@ -37,11 +42,11 @@
    const [streetList, setStreetList] = useState([])
    const [activeStreet, setActiveStreet] = useState({})
    //所属社区
    const [communityList, setCommunityList] = useState([])
    const [activeCommunity, setActiveCommunity] = useState({})
    const [accountLevels, setAccountLevels] = useState([
        { area: [], isDiscipline: 0, options: [] }
    ]);
    useEffect(() => {
        // 获取单位
@@ -51,63 +56,14 @@
    useImperativeHandle(ref, () => {
        return {
            refreshData: (data, companyList) => {
                getCountyList()
                getCascaderDatas()
                setOneCompanyList(() => companyList)
                console.log('companyList', companyList)
                systemPostList({ pageNum: 1, pageSize: 10000 }).then(res => {
                    setPositionList(() => res.data.records)
                })
                systemRoleList({ pageNum: 1, pageSize: 10000 }).then(res => {
                    setRoleList(() => res.data.records)
                })
                // 权限判断
                let adminInfo = JSON.parse(localStorage.getItem('userInfo'))
                setAdminLevel(() => adminInfo.accountLevel)
                switch (adminInfo.accountLevel) {
                    case 1:
                        setLevelList(() => [{ name: '市级账号', value: 1 }, { name: '区县账号', value: 2 }, { name: '街道账号', value: 3 }, { name: '社区账号', value: 4 }, { name: '党员账号', value: 5 }])
                        break;
                    case 2:
                        getStreetList(adminInfo.districtsCode)
                        setLevelList(() => [{ name: '区县账号', value: 2 }, { name: '街道账号', value: 3 }, { name: '社区账号', value: 4 }, { name: '党员账号', value: 5 }])
                        this.$nextTick(() => {
                            form.setFieldsValue({ districtsCode: adminInfo.districtsCode })
                        })
                        break;
                    case 3:
                        getStreetList(adminInfo.districtsCode)
                        getcommunityList(adminInfo.streetId)
                        setLevelList(() => [{ name: '街道账号', value: 3 }, { name: '社区账号', value: 4 }, { name: '党员账号', value: 5 }])
                        form.setFieldsValue({ districtsCode: adminInfo.districtsCode })
                        form.setFieldsValue({ streetId: adminInfo.streetId })
                        break;
                    case 4:
                        getStreetList(adminInfo.districtsCode)
                        getcommunityList(adminInfo.streetId)
                        setLevelList(() => [{ name: '社区账号', value: 4 }, { name: '党员账号', value: 5 }])
                        // this.$nextTick(() => {
                        form.setFieldsValue({ districtsCode: adminInfo.districtsCode })
                        form.setFieldsValue({ streetId: adminInfo.streetId })
                        form.setFieldsValue({ communityId: adminInfo.communityId * 1 })
                        // })
                        break;
                    case 5:
                        getStreetList(adminInfo.districtsCode)
                        getcommunityList(adminInfo.streetId)
                        setLevelList(() => [{ name: '党员账号', value: 5 }])
                        form.setFieldsValue({ districtsCode: adminInfo.districtsCode })
                        form.setFieldsValue({ streetId: adminInfo.streetId })
                        form.setFieldsValue({ communityId: adminInfo.communityId * 1 })
                        break;
                    default:
                        break;
                }
                if (data.id) {
                    getInfo(data.id)
@@ -128,9 +84,51 @@
    // 保存
    const okHandle = () => {
        form.validateFields().then((values) => {
            if(values.password){
            // 校验accountLevels必填
            if (!accountLevels.length || accountLevels.some(item => !item.area || item.area.length === 0)) {
                if (window?.antd?.message?.error) {
                    window.antd.message.error('请完整选择账号所属层级区域!');
                } else {
                    alert('请完整选择账号所属层级区域!');
                }
                return;
            }
            // 打印提交时的accountLevels数组
            console.log('提交时的accountLevels:', accountLevels);
            // 组装 systemUserLevels,严格按照接口字段,优先用原始字段
            const systemUserLevels = accountLevels.map(item => {
                // Get the first selected option's id as the level
                const firstSelectedOption = item.selectedOptions?.[0];
                const level = firstSelectedOption?.id || item.level || '';
                // 判断不同级别(假设市级别id为1,区县级别id为2,街道级别id为3,社区级别id为4)
                const isCityLevel = level === '1';
                const isDistrictLevel = level === '2';
                const isStreetLevel = level === '3';
                const isCommunityLevel = level === '4';
                return {
                    // 市级别:所有下级字段为空
                    // 区县级别:街道和社区字段为空
                    // 街道级别:社区字段为空
                    // 社区级别:不做处理
                    community: (isCityLevel || isDistrictLevel || isStreetLevel) ? '' : (item.community || item.selectedOptions?.[3]?.name || ''),
                    communityId: (isCityLevel || isDistrictLevel || isStreetLevel) ? '' : (item.communityId || item.selectedOptions?.[3]?.id || ''),
                    districts: isCityLevel ? '' : (item.districts || item.selectedOptions?.[1]?.name || ''),
                    districtsCode: isCityLevel ? '' : (item.districtsCode || item.selectedOptions?.[1]?.id || ''),
                    id: item.id || '', // 编辑时可用
                    level: level, // Use the determined level value
                    status: 1,
                    street: (isCityLevel || isDistrictLevel) ? '' : (item.street || item.selectedOptions?.[2]?.name || ''),
                    streetId: (isCityLevel || isDistrictLevel) ? '' : (item.streetId || item.selectedOptions?.[2]?.id || ''),
                    superviseFlag: typeof item.isDiscipline === 'number' ? item.isDiscipline : (item.isDiscipline ? 1 : 0),
                    // systemUserId: 可选,如有需要补充
                };
            });
            values.systemUserLevels = systemUserLevels;
            if (values.password) {
                values.password = CryptoJS.MD5(values.password).toString();
            }else{
            } else {
                delete values.password
            }
            if (values.DepartmentId) {
@@ -144,54 +142,73 @@
                onUpdate(values)
            } else {
                onSave(values);
                setAccountLevels([{ area: [], isDiscipline: 0, options: [] }]); // 清空
            }
        });
    };
    const getCountyList = (id) => {
        getCityList({ id: '510400', tier: 2 }).then(res => {
            setCountyList(() => res.data)
        })
    }
    const getStreetList = (id) => {
        getCityList({ id: id, tier: 3 }).then(res => {
            setStreetList(() => res.data)
        })
    }
    const getcommunityList = (id) => {
        getCityList({ id: id, tier: 4 }).then(res => {
            setCommunityList(() => res.data)
        })
    }
    const changeCountry = (value, label) => {
        setActiveCounty(label)
        getStreetList(value)
        form.setFieldsValue({ streetId: '', communityId: '' })
        setActiveCommunity({ name: '', id: '' })
        setActiveStreet({ name: '', id: '' })
    // 获取级联数据
    const getCascaderDatas = () => {
        getCascaderData().then(res => {
            // 处理级联数据
            const processedData = res.data.map(item => {
                // 根据第一级类型处理数据
                if (item.children) {
                    // 区县级别
                    if (item.id == 2) { // 假设type=2表示区县
                        // 只保留第一级children
                        item.children = item.children.map(child => {
                            const { children, ...childWithoutChildren } = child;
                            return childWithoutChildren;
                        });
                    }
                    // 街道级别
                    else if (item.id == 3) { // 假设type=3表示街道
                        // 保留区县和街道两级
                        item.children = item.children.map(child => {
                            if (child.children) {
                                child.children = child.children.map(street => {
                                    const { children, ...streetWithoutChildren } = street;
                                    return streetWithoutChildren;
                                });
                            }
                            return child;
                        });
                    }
                    // 社区级别
                    else if (item.id == 4) { // 假设type=4表示社区
                        // 保留所有层级数据
                        return item;
                    }
                }
                return item;
            });
            let arr = JSON.parse(JSON.stringify(processedData))
            let accountLevels = localStorage.getItem('userInfo')
            let accountLevel = JSON.parse(accountLevels).accountLevel
            if (accountLevel == 2) {
                arr = arr.filter(item => item.id != 1)
            } else if (accountLevel == 3) {
                arr = arr.filter(item => item.id != 1 && item.id != 2)
            } else if (accountLevel == 4) {
                arr = arr.filter(item => item.id != 1 && item.id != 2 && item.id != 3)
            }
            setCascaderData(() => arr);
        });
    }
    const changeStreet = (value, label) => {
        setActiveStreet(label)
        getcommunityList(value)
        form.setFieldsValue({ communityId: '' })
        setActiveCommunity({ name: '', id: '' })
    }
    const changeCommunity = (value, label) => {
        setActiveCommunity(label)
    }
    const getInfo = (id) => {
        getSystemUserInfo(id).then(res => {
            if (res.data.districtsCode) {
                setActiveStreet({ name: res.data.districts, id: res.data.districtsCode })
                getStreetList(res.data.districtsCode)
            }
            if (res.data.streetId) {
                setActiveCounty({ name: res.data.street, id: res.data.streetId })
                getcommunityList(res.data.streetId)
            }
            if (res.data.communityId) {
                setActiveCommunity({ name: res.data.community, id: res.data.communityId })
            }
            // delete res.data.password
            let departmentId = []
@@ -208,8 +225,89 @@
                departmentId.push(res.data.fourDepartmentId)
            }
            res.data.DepartmentId = departmentId
            console.log('departmentId', departmentId)
            setActiveLevel(() => res.data.accountLevel)
            // 先获取级联数据再回显
            getCascaderData().then(cascadeRes => {
                // 处理级联数据
                const processedData = cascadeRes.data.map(item => {
                    // 根据第一级类型处理数据
                    if (item.children) {
                        // 区县级别
                        if (item.id == 2) { // 假设type=2表示区县
                            // 只保留第一级children
                            item.children = item.children.map(child => {
                                const { children, ...childWithoutChildren } = child;
                                return childWithoutChildren;
                            });
                        }
                        // 街道级别
                        else if (item.id == 3) { // 假设type=3表示街道
                            // 保留区县和街道两级
                            item.children = item.children.map(child => {
                                if (child.children) {
                                    child.children = child.children.map(street => {
                                        const { children, ...streetWithoutChildren } = street;
                                        return streetWithoutChildren;
                                    });
                                }
                                return child;
                            });
                        }
                        // 社区级别
                        else if (item.id == 4) { // 假设type=4表示社区
                            // 保留所有层级数据
                            return item;
                        }
                    }
                    return item;
                });
                let arr = JSON.parse(JSON.stringify(processedData))
                let accountLevels = localStorage.getItem('userInfo')
                let accountLevel = JSON.parse(accountLevels).accountLevel
                if (accountLevel == 2) {
                    arr = arr.filter(item => item.id != 1)
                } else if (accountLevel == 3) {
                    arr = arr.filter(item => item.id != 1 && item.id != 2)
                } else if (accountLevel == 4) {
                    arr = arr.filter(item => item.id != 1 && item.id != 2 && item.id != 3)
                }
                setCascaderData(() => arr);
                // 回显accountLevels,area始终为4级,id类型统一
                if (res.data.systemUserLevels && Array.isArray(res.data.systemUserLevels)) {
                    const newAccountLevels = res.data.systemUserLevels.map(level => {
                        const toId = v => (v === undefined || v === null) ? undefined : String(v);
                        const area = [
                            toId(level.level),
                            toId(level.twoLevelId || level.districtsCode),
                            toId(level.threeLevelId || level.streetId),
                            toId(level.fourLevelId || level.communityId)
                        ].filter(v => v !== undefined && v !== null && v !== '');
                        return {
                            area,
                            isDiscipline: Number(level.superviseFlag) === 1 ? 1 : 0,
                            options: [],
                            selectedOptions: [
                                level.level ? { id: toId(level.level), name: '' } : undefined,
                                level.twoLevelName ? { id: toId(level.twoLevelId), name: level.twoLevelName } : undefined,
                                level.threeLevelName ? { id: toId(level.threeLevelId), name: level.threeLevelName } : undefined,
                                level.fourLevelName ? { id: toId(level.fourLevelId), name: level.fourLevelName } : undefined,
                            ].filter(Boolean),
                            level: toId(level.level) || '',
                            community: level.community || '',
                            communityId: level.communityId || '',
                            districts: level.districts || '',
                            districtsCode: level.districtsCode || '',
                            id: level.id || '',
                            street: level.street || '',
                            streetId: level.streetId || '',
                        };
                    });
                    setAccountLevels(newAccountLevels.length ? newAccountLevels : [{ area: [], isDiscipline: 0, options: [], level: '', community: '', communityId: '', districts: '', districtsCode: '', id: '', street: '', streetId: '' }]);
                }
            });
            form.setFieldsValue({ ...res.data, password: '' })
        })
@@ -235,13 +333,32 @@
        return false
    }
    // 动态添加账号层级项
    const addAccountLevel = () => {
        setAccountLevels([...accountLevels, { area: [], isDiscipline: 0, options: [] }]);
    };
    // 删除账号层级项
    const removeAccountLevel = (idx) => {
        if (accountLevels.length === 1) return;
        setAccountLevels(accountLevels.filter((_, i) => i !== idx));
    };
    // 纪检委单选变更
    const handleDisciplineChange = (e, idx) => {
        setAccountLevels(levels => {
            const newLevels = [...levels];
            newLevels[idx].isDiscipline = e.target.value;
            return newLevels;
        });
    };
    return (
        <Modal
            getContainer={false}
            width="65%"
            width="75%"
            destroyOnClose
            title={data.type == 'detail' ? '人员详情' : data.type == 'edit' ? '编辑人员' : '添加人员'}
            open={visible}
            className='addAndEditModal'
            onCancel={() => onCancel(false)}
            afterClose={() => {
                form.resetFields()
@@ -258,8 +375,8 @@
                    <Button key="back" onClick={() => onCancel(false)}>关闭</Button>
            }
        >
            <Form layout="horizontal" {...formItemLayout} form={form} scrollToFirstError>
                <Row>
            <Form layout="horizontal" form={form} scrollToFirstError labelCol={{ span: 6 }} wrapperCol={{ span: 20 }}>
                <Row gutter={16}>
                    <Col span={8}>
                        <Form.Item
                            name="name"
@@ -269,90 +386,6 @@
                            <Input disabled={data.type == 'detail'} placeholder='请输入人员姓名' />
                        </Form.Item>
                    </Col>
                </Row>
                <Row>
                    <Col span={16}>
                        <Form.Item
                            name="DepartmentId"
                            label="所属单位"
                            labelCol={{ span: 4 }}
                            rules={[{ required: true, message: '请选择所属单位' }]}
                        >
                            {/* <Select
                                key="searchSelect"
                                allowClear
                                disabled={data.type == 'detail'}
                                placeholder="请选择"
                                options={oneCompanyList}
                                fieldNames={{ label: 'name', value: 'id' }}
                                filterOption={false}
                            >
                            </Select > */}
                            <Cascader
                                changeSelect
                                options={oneCompanyList}
                                fieldNames={{ value: 'key', label: 'name' }}
                                placeholder="请选择"
                                // displayRender={(label) => label[label.length - 1]}
                                changeOnSelect={true}
                            />
                        </Form.Item>
                    </Col>
                    {/* <Col span={8}>
                        <Form.Item
                            name="twoDepartmentId"
                            label="所属二级单位"
                        >
                            <Select
                                key="searchSelect"
                                allowClear
                                disabled={data.type == 'detail'}
                                placeholder="请选择"
                                options={twoCompanyList}
                                fieldNames={{ label: 'name', value: 'id' }}
                                filterOption={false}
                            >
                            </Select >
                        </Form.Item>
                    </Col>
                    <Col span={8}>
                        <Form.Item
                            name="threeDepartmentId"
                            label="所属三级单位"
                        >
                            <Select
                                key="searchSelect"
                                allowClear
                                disabled={data.type == 'detail'}
                                placeholder="请选择"
                                options={threeCompanyList}
                                fieldNames={{ label: 'name', value: 'id' }}
                                filterOption={false}
                            >
                            </Select >
                        </Form.Item>
                    </Col> */}
                </Row>
                {/* <Row>
                    <Col span={8}>
                        <Form.Item
                            name="fourDepartmentId"
                            label="所属四级单位"
                        >
                            <Select
                                key="searchSelect"
                                allowClear
                                disabled={data.type == 'detail'}
                                placeholder="请选择"
                                options={fourCompanyList}
                                fieldNames={{ label: 'name', value: 'id' }}
                                filterOption={false}
                            >
                            </Select >
                        </Form.Item>
                    </Col>
                </Row> */}
                <Row>
                    <Col span={8}>
                        <Form.Item
                            name="systemPostId"
@@ -360,197 +393,120 @@
                            rules={[{ required: true, message: '请选择所属职位' }]}
                        >
                            <Select
                                key="searchSelect"
                                allowClear
                                disabled={data.type == 'detail'}
                                placeholder="请选择"
                                options={positionList}
                                fieldNames={{ label: 'name', value: 'id' }}
                                filterOption={false}
                            >
                            </Select >
                            />
                        </Form.Item>
                    </Col>
                    <Col span={8}>
                        <Form.Item
                            name="systemRoleId"
                            label="所属角色"
                            rules={[{ required: true, message: '请选择所属角色' }]}
                            label="后台权限"
                            rules={[{ required: true, message: '请选择后台权限' }]}
                        >
                            <Select
                                key="searchSelect"
                                allowClear
                                disabled={data.type == 'detail'}
                                placeholder="请选择"
                                options={roleList}
                                fieldNames={{ label: 'name', value: 'id' }}
                                filterOption={false}
                            >
                            </Select >
                            />
                        </Form.Item>
                    </Col>
                </Row>
                <Row>
                <Row gutter={16}>
                    <Col span={24}>
                        <Form.Item
                            label="账号所属层级"
                            colon={true}
                            required
                            labelCol={{ span: 2 }}
                            wrapperCol={{ span: 12 }}
                            style={{ marginBottom: 0 }}
                        >
                            <Button type={'primary'} onClick={addAccountLevel} style={{ marginBottom: 8 }}>添加</Button>
                            {accountLevels.map((item, idx) => (
                                <Card key={idx} style={{ marginBottom: 8 }} size='small' >
                                    <div style={{ display: 'flex', alignItems: 'center' }}>
                                        <Cascader
                                            style={{ width: 320, marginRight: 16 }}
                                            placeholder="请选择区域"
                                            options={cascaderData}
                                            fieldNames={{ label: 'name', value: 'id' }}
                                            value={item.area}
                                            onChange={(value, selectedOptions) => {
                                                setAccountLevels(levels => {
                                                    const newLevels = [...levels];
                                                    newLevels[idx] = {
                                                        ...newLevels[idx],
                                                        area: value,
                                                        selectedOptions: selectedOptions,
                                                        // 清空原有数据,确保新选择的数据生效
                                                        community: '',
                                                        communityId: '',
                                                        districts: '',
                                                        districtsCode: '',
                                                        street: '',
                                                        streetId: '',
                                                        level: selectedOptions?.[0]?.id || ''
                                                    };
                                                    return newLevels;
                                                });
                                            }}
                                        />
                                        <div style={{ marginLeft: 16, marginTop: 22 }}>
                                            <span style={{ marginRight: 8 }}>是否为纪检委账号:</span>
                                            <Radio.Group
                                                style={{ marginRight: 16 }}
                                                value={item.isDiscipline ?? 0}
                                                onChange={e => handleDisciplineChange(e, idx)}
                                                options={[
                                                    { label: '否', value: 0 },
                                                    { label: '是', value: 1 }
                                                ]}
                                            />
                                        </div>
                                        {idx > 0 && <DeleteOutlined style={{ marginRight: 16 }} onClick={() => removeAccountLevel(idx)} disabled={accountLevels.length === 1} />}
                                    </div>
                                </Card>
                            ))}
                        </Form.Item>
                    </Col>
                </Row>
                <Row gutter={16}>
                    <Col span={8}>
                        <Form.Item
                            name="accountLevel"
                            label="账号层级"
                            rules={[{ required: true, message: '请选择账号层级' }]}
                        >
                            <Select
                                key="searchSelect"
                                allowClear
                                onChange={(e) => setActiveLevel(e)}
                                disabled={data.type == 'detail'}
                                placeholder="请选择"
                                options={levelList}
                                fieldNames={{ label: 'name', value: 'value' }}
                            // filterOption={false}
                            >
                            </Select >
                        </Form.Item>
                    </Col>
                    <Col span={16}>
                        <Form.Item
                            label="是否管理员"
                            labelCol={{ span: 4 }}
                            wrapperCol={{ span: 20 }}
                            style={{ marginBottom: 0 }}
                        >
                            <div style={{ display: "flex", flex: 1 }}>
                                <Form.Item
                                    name="isAdmin"
                                    rules={[{ required: true, message: '是否管理员' }]}
                                >
                                    <Radio.Group
                                        disabled={data.type == 'detail'}
                                        // style={style}
                                        // onChange={onChange}
                                        // value={value}
                                        options={[{ value: 0, label: '否', }, { value: 1, label: '是', },
                                        ]}
                                    />
                                </Form.Item>
                                <div style={{ fontSize: '12px', color: "rgba(0,0,0,0.5)", marginLeft: "10px" }}>管理员主要用于接收实现临期提醒,以及上级端登录</div>
                            </div>
                        </Form.Item>
                    </Col>
                </Row>
                {activeLevel != 1 && (
                    <Row>
                        {[2, 3, 4, 5].includes(activeLevel) && (
                            <Col span={8}>
                                <Form.Item
                                    name="districtsCode"
                                    label="所属区县"
                                    rules={[{ required: true, message: '请选择所属区县' }]}
                                >
                                    {/* <Select
                                // onChange={changeCountry}
                                // value={activeCounty}
                                placeholder="请选择"
                                options={levelList}
                                fieldNames={{ label: 'name', value: 'id' }}
                            >
                            </Select > */}
                                    <Select
                                        key="searchSelect"
                                        allowClear
                                        disabled={data.type == 'detail' || [2, 3, 4, 5].includes(adminLevel)}
                                        onChange={changeCountry}
                                        value={activeCounty.id}
                                        placeholder="请选择"
                                        options={countyList}
                                        fieldNames={{ label: 'name', value: 'id' }}
                                    // filterOption={false}
                                    >
                                    </Select >
                                </Form.Item>
                            </Col>
                        )}
                        {[3, 4, 5].includes(activeLevel) && (
                            <Col span={8}>
                                <Form.Item
                                    name="streetId"
                                    label="所属街道"
                                    rules={[{ required: true, message: '请选择所属街道' }]}
                                >
                                    <Select
                                        onChange={changeStreet}
                                        disabled={!activeCounty || data.type == 'detail' || [3, 4, 5].includes(adminLevel)}
                                        key="searchSelect"
                                        allowClear
                                        value={activeStreet.id}
                                        placeholder="请选择"
                                        options={streetList}
                                        fieldNames={{ label: 'name', value: 'id' }}
                                    >
                                    </Select >
                                </Form.Item>
                            </Col>
                        )}
                        {[4, 5].includes(activeLevel) && (
                            <Col span={8}>
                                <Form.Item
                                    name="communityId"
                                    label="所属社区"
                                    rules={[{ required: true, message: '请选择所属社区' }]}
                                >
                                    <Select
                                        onChange={changeCommunity}
                                        disabled={!activeStreet || data.type == 'detail' || [4, 5].includes(adminLevel)}
                                        key="searchSelect"
                                        allowClear
                                        value={activeCommunity.id}
                                        placeholder="请选择"
                                        options={communityList}
                                        fieldNames={{ label: 'name', value: 'id' }}
                                    >
                                    </Select >
                                </Form.Item>
                            </Col>
                        )}
                    </Row>
                )}
                <Row>
                    <Col span={16}>
                        <Form.Item
                            label="联系方式"
                            required
                            labelCol={{ span: 4 }}
                            wrapperCol={{ span: 20 }}
                            style={{ marginBottom: 0 }}
                            name="phone"
                            extra={'联系方式将作为登录账号使用'}
                            rules={[{
                                validator: (rule, value) => {
                                    return new Promise((resolve, reject) => {
                                        if (!value) {
                                            reject('请输入联系方式');
                                        }
                                        const phoneRegex = /^((\+86)?(13|14|15|16|17|18|19)[0-9]{9})|((\+86)?(0[0-9]{2,3})?([2-9][0-9]{6,7}))$/;
                                        if (!phoneRegex.test(value)) {
                                            reject('请输入正确的电话号码');
                                        }
                                        resolve('');
                                    });
                                },
                            }]}
                        >
                            <div style={{ display: "flex", flex: 1 }}>
                                <Form.Item
                                    name="phone"
                                    rules={[{
                                        validator: (rule, value) => {
                                            return new Promise((resolve, reject) => {
                                                if (!value) {
                                                    reject('请输入联系方式');
                                                }
                                                const phoneRegex = /^((\+86)?(13|14|15|16|17|18|19)[0-9]{9})|((\+86)?(0[0-9]{2,3})?([2-9][0-9]{6,7}))$/;
                                                if (!phoneRegex.test(value)) {
                                                    reject('请输入正确的电话号码');
                                                }
                                                resolve('');
                                            });
                                        },
                                    }]}
                                >
                                    <Input disabled={data.type == 'detail'} placeholder='请输入联系方式' />
                                </Form.Item>
                                <div style={{ fontSize: '12px', color: "rgba(0,0,0,0.5)", marginLeft: "10px" }}>联系方式将作为登录账号使用</div>
                            </div>
                            <Input disabled={data.type == 'detail'} placeholder='请输入联系方式' />
                        </Form.Item>
                    </Col>
                </Row>
                <Row>
                <Row gutter={16}>
                    <Col span={8}>
                        <Form.Item
                            required={data.type == 'add'}
@@ -574,39 +530,32 @@
                            <Input.Password disabled={data.type == 'detail'} placeholder='请输入' />
                        </Form.Item>
                    </Col>
                    <Col span={16}>
                </Row>
                <Row gutter={16}>
                    <Col span={8}>
                        <Form.Item
                            label="确认密码"
                            name="surePassword"
                            extra={'密码需要包含大小写字母,数字和特殊符号,且长度为8位以上'}
                            required={data.type == 'add'}
                            labelCol={{ span: 4 }}
                            wrapperCol={{ span: 20 }}
                            style={{ marginBottom: 0 }}
                            rules={[{
                                validator: (rule, value) => {
                                    return new Promise((resolve, reject) => {
                                        if (!value && data.type == 'add') {
                                            reject('请再次输入新密码');
                                        }
                                        if (value != form.getFieldValue('password') && data.type == 'add') {
                                            reject('两次密码请保持一致');
                                        }
                                        resolve('');
                                    });
                                },
                            }]}
                        >
                            <div style={{ display: "flex", flex: 1 }}>
                                <Form.Item
                                    name="surePassword"
                                    rules={[{
                                        validator: (rule, value) => {
                                            return new Promise((resolve, reject) => {
                                                if (!value && data.type == 'add') {
                                                    reject('请再次输入新密码');
                                                }
                                                if (value != form.getFieldValue('password') && data.type == 'add') {
                                                    reject('两次密码请保持一致');
                                                }
                                                resolve('');
                                            });
                                        },
                                    },]}
                                >
                                    <Input.Password disabled={data.type == 'detail'} placeholder='请输入' />
                                </Form.Item>
                                <div style={{ fontSize: '12px', color: "rgba(0,0,0,0.5)", marginLeft: "10px" }}>密码需要包含大小写字母,数字和特殊符号,且长度为8位以上</div>
                            </div>
                            <Input.Password disabled={data.type == 'detail'} placeholder='请输入' />
                        </Form.Item>
                    </Col>
                </Row>
            </Form>
            <Spin spinning={spinning} fullscreen />
management/src/pages/setting/user/index.jsx
@@ -1,7 +1,7 @@
import { buildProTableDataSource, sendRequest, showDelConfirm, showConfirm } from '@/utils/antdUtils';
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
import { PageContainer, ProFormText, ProTable, QueryFilter } from '@ant-design/pro-components';
import { Button, Cascader, Col, Menu, Row, Select, Space } from 'antd';
import { PageContainer, ProFormText, ProTable, QueryFilter, ProFormSelect } from '@ant-design/pro-components';
import { Button, Cascader, Col, Menu, Row, Select, Space, Form } from 'antd';
import { useEffect, useRef, useState } from 'react';
import { Access, useAccess } from 'umi';
import AddAndEdit from './components/addAndEdit';
@@ -21,6 +21,7 @@
} from './service';
const Role = () => {
  const [form] = Form.useForm();
  const actionRef = useRef();
  const addViewRef = useRef();
  const addViewRef1 = useRef();
@@ -48,7 +49,7 @@
        }}
      >
        <span>{item.name}</span>
        <div>
        {/* <div>
          {item.tier < 4 && (
            <Access accessible={access['/system_setting/unit_management/add']}>
              <PlusOutlined
@@ -82,31 +83,40 @@
              }}
            />
          </Access>
        </div>
        </div> */}
      </div >
    );
  };
  const renderMenuItems = (items) => {
  const renderMenuItems = (items, level = 0) => {
    return items.map((item) => {
      if (item.children && item.children.length > 0) {
        return (
          <SubMenu
            key={item.key}
            title={node(item)}
            onTitleClick={(item) => {
              setUnitId(item.key);
              actionRef.current.reload();
            onTitleClick={(e) => {
              if (e && e.domEvent) {
                e.domEvent.stopPropagation();
              }
              setUnitId(item.id);
              const values = form.getFieldsValue();
              actionRef.current.reload(values);
            }}
          >
            {renderMenuItems(item.children)}
            {renderMenuItems(item.children, level + 1)}
          </SubMenu>
        );
      }
      return (
        <Menu.Item
          onClick={(item) => {
            setUnitId(item.key);
            actionRef.current.reload();
          onClick={(e) => {
            if (e && e.domEvent) {
              e.domEvent.stopPropagation();
            }
            const itemId = item.id || item.key;
            setUnitId(itemId);
            const values = form.getFieldsValue();
            actionRef.current.reload(values);
          }}
          key={item.key}
        >
@@ -120,6 +130,8 @@
      if (res.code == 200 && res.data) {
        const traverseItems = (items) => {
          return items.map((item) => {
            if (!item.id) {
            }
            item.key = item.id;
            item.title = '1';
            if (item.child && item.child.length > 0) {
@@ -128,7 +140,8 @@
            return item;
          });
        };
        setItems(traverseItems(res.data));
        const processedItems = traverseItems(res.data);
        setItems(processedItems);
      }
    });
  };
@@ -141,50 +154,50 @@
      title: '联系方式',
      dataIndex: 'phone',
    },
    {
      title: '所在单位',
      dataIndex: 'departmentName',
      hideInSearch: true,
      renderFormItem: () => {
        return (
          <Cascader
            options={items}
            fieldNames={{ value: 'key', label: 'name' }}
            placeholder="请选择"
            displayRender={(label) => label[label.length - 1]}
            changeOnSelect={true}
          />
        );
      },
    },
    {
      hideInTable: true,
      title: '所在单位',
      dataIndex: 'departmentId',
      renderFormItem: () => {
        return (
          <Cascader
            options={items}
            fieldNames={{ value: 'key', label: 'name' }}
            placeholder="请选择"
            displayRender={(label) => label[label.length - 1]}
            changeOnSelect={true}
          />
        );
      },
    },
    // {
    //   title: '所在单位',
    //   dataIndex: 'departmentName',
    //   hideInSearch: true,
    //   renderFormItem: () => {
    //     return (
    //       <Cascader
    //         options={items}
    //         fieldNames={{ value: 'key', label: 'name' }}
    //         placeholder="请选择"
    //         displayRender={(label) => label[label.length - 1]}
    //         changeOnSelect={true}
    //       />
    //     );
    //   },
    // },
    // {
    //   hideInTable: true,
    //   title: '所属权限',
    //   dataIndex: 'departmentId',
    //   renderFormItem: () => {
    //     return (
    //       <Cascader
    //         options={items}
    //         fieldNames={{ value: 'key', label: 'name' }}
    //         placeholder="请选择"
    //         displayRender={(label) => label[label.length - 1]}
    //         changeOnSelect={true}
    //       />
    //     );
    //   },
    // },
    {
      title: '所属职位',
      dataIndex: 'systemPostName',
      hideInSearch: true,
    },
    {
      title: '所属角色',
      title: '后台权限',
      dataIndex: 'systemRoleName',
      hideInSearch: true,
    },
    {
      title: '所属角色',
      title: '所属权限',
      dataIndex: 'systemRoleId',
      hideInTable: true,
      renderFormItem: () => {
@@ -201,31 +214,24 @@
      },
    },
    {
      title: '账号层级',
      title: '账号所属层级',
      dataIndex: 'accountLevel',
      // (1=市级账号,2=区县账号,3=街道账号,4=社区账号)
      render: (text, record) => {
        let role = '';
        switch (record.accountLevel) {
          case 1:
            role = '市';
            break;
          case 2:
            role = '区县';
            break;
          case 3:
            role = '街道';
            break;
          case 4:
            role = '社区';
            break;
          case 5:
            role = '党员';
            break;
          default:
            role = '';
        }
        return role;
        record.list.length>0&&record.list.map(item=>{
          item.str = ''
          if(item.level==1){
           item.str = '市'
          }else if(item.level==2){
           item.str = '区县' + '/' + item.districts
          }else if(item.level==3){
            item.str = '街道' + '/' + item.districts + '/' + item.street
          }else{
            item.str = '社区' + '/' + item.districts + '/' + item.street + '/' + item.community
          }
          return item.str;
        })
        return record.list.map(item=>item.str).join('、');
      },
      valueEnum: {
        1: '市',
@@ -265,7 +271,7 @@
              <Access accessible={access['/system_setting/people_management/edit']}>
                <a
                  onClick={() => {
                    addViewRef.current.refreshData({ ...record, type: 'edit' },items);
                    addViewRef.current.refreshData({ ...record, type: 'edit' }, items);
                    handleModalVisibles(true);
                  }}
                >
@@ -292,7 +298,7 @@
            <Access accessible={access['/system_setting/people_management/detail']}>
              <a
                onClick={() => {
                  addViewRef.current.refreshData({ ...record, type: 'detail' },items);
                  addViewRef.current.refreshData({ ...record, type: 'detail' }, items);
                  handleModalVisibles(true);
                }}
              >
@@ -302,7 +308,7 @@
            <Access accessible={access['/system_setting/people_management/freeze']}>
              <a
                onClick={() => {
                  showConfirm(`确认${record.status === 1 ? '冻结' : '解冻'}该人员吗?`,'', async () => {
                  showConfirm(`确认${record.status === 1 ? '冻结' : '解冻'}该人员吗?`, '', async () => {
                    let status = await sendRequest(
                      record.status === 1 ? freezeApi : unfreezeApi,
                      record.id,
@@ -331,6 +337,8 @@
    >
      <div style={{ background: '#fff' }}>
        <QueryFilter
          form={form}
          labelWidth={100}
          onReset={(values) => {
            fetchUnit(values);
            setUnitId('');
@@ -342,14 +350,21 @@
            actionRef.current.reload();
          }}
        >
          <ProFormText name="name" label="单位名称" />
          <ProFormText name="name" label="组织结构名称" />
          <ProFormSelect name="type" label="筛选维度" options={[{
            label: '当前组织结构',
            value: 1,
          }, {
            label: '当前及下级组织结构',
            value: 2,
          }]} />
        </QueryFilter>
      </div>
      <Row style={{ marginTop: 20, background: '#fff' }}>
        <Col span={4}>
          <Space style={{ margin: '10px 0' }}>
            <span style={{ margin: '0 27px' }}>单位管理</span>
            <Button
            <span style={{ margin: '0 27px' }}>组织结构</span>
            {/* <Button
              type="primary"
              onClick={() => {
                addViewRef1.current.refreshData({});
@@ -357,10 +372,17 @@
              }}
            >
              添加
            </Button>
            </Button> */}
          </Space>
          <Menu mode="inline">{renderMenuItems(items)}</Menu>
          <Menu
            mode="inline"
            onClick={({ key, domEvent }) => {
              domEvent.stopPropagation();
            }}
          >
            {renderMenuItems(items)}
          </Menu>
        </Col>
        <Col span={20} style={{ minHeight: 650 }}>
          <ProTable
@@ -385,6 +407,9 @@
              if (params.departmentId) {
                obj.departmentId = params.departmentId[params.departmentId.length - 1];
              }
              if (form.getFieldValue('type')) {
                obj.type = form.getFieldValue('type');
              }
              return buildProTableDataSource(getList, obj);
            }}
            toolBarRender={(action, selectRows) => [
@@ -393,7 +418,7 @@
                  <Button
                    type="primary"
                    onClick={() => {
                      addViewRef.current.refreshData({ type: 'add', unitId : unitId  },items);
                      addViewRef.current.refreshData({ type: 'add', unitId: unitId }, items);
                      handleModalVisibles(true);
                    }}
                  >
management/src/pages/setting/user/index.less
New file
@@ -0,0 +1,5 @@
.addAndEditModal{
    .ant-modal-body{
        padding: 0 !important;
    }
}
management/src/pages/setting/user/service.js
@@ -134,4 +134,12 @@
    return request(`/api/huacheng-sangeshenbian/systemUser/unfreeze/${id}`, {
        method: 'PUT',
    });
}
}
//获取级联数据
export const getCascaderData = async (data) => {
    return request(`/api/huacheng-sangeshenbian/systemUser/getAdministrativeDivisionTwo`, {
        method: 'GET',
    });
}
management/src/requestErrorConfig.ts
@@ -26,7 +26,7 @@
 * @doc https://umijs.org/docs/max/request#配置
 */
export const errorConfig: RequestConfig = {
  baseURL: BASE_URL,
  // baseURL: BASE_URL,
  // 请求拦截器
  requestInterceptors: [