Procházet zdrojové kódy

feat(repair): 新增转派审批功能并统一操作登录校验

- 新增转派审批页面,支持同意/驳回操作及审批意见填写
- 新增转派审批通过、驳回接口及对应状态枚举(已拒单、转派审批中、委外维修中)
- 抽取 requireLogin、goLogin 统一登录校验,各操作类按钮接入登录拦截
- 登录前记录来源页路径,登录成功后自动回跳原页面
- 新增 isLogin、hasRoleCode、isExamineRole 方法,基于 roleCodes 优化角色判断
- 免登录通道写操作接口改为登录后携带 token,确保后端留痕真实操作人
- 修复扫码免登录进入设备状态页时 getUserInfo 为空导致报错的问题
- 修复转派按钮仅在维修中状态显示,与后端状态校验保持一致
- "我的"页面新增转派审批入口,审核任务入口扩展至审核角色可见
dazhaxie před 3 týdny
rodič
revize
b310ee6f38

+ 13 - 2
api/ignore/index.js

@@ -1,4 +1,4 @@
-import http from '@/utils/http';
+import http, { takeBackUrl } from '@/utils/http';
 import DictCache from '@/utils/dict';
 import { stringify } from 'qs';
 
@@ -89,7 +89,18 @@ export function mobileLogin(data) {
                     success: function () {
                         // 获取词典
                         getDictData().then(response => {
-                            uni.switchTab({ url: '/pages/tidings/tidings' });
+                            // 登录前由操作拦截/401 记录了来源页(如扫码进入的设备详情),优先回跳,避免用户重新扫码
+                            const backUrl = takeBackUrl();
+                            if (backUrl) {
+                                uni.redirectTo({
+                                    url: backUrl,
+                                    fail: function () {
+                                        uni.switchTab({ url: '/pages/tidings/tidings' });
+                                    }
+                                });
+                            } else {
+                                uni.switchTab({ url: '/pages/tidings/tidings' });
+                            }
                             uni.setStorage({
                                 key: DictCache.BASE_DATA_CACHE,
                                 data: response.data,

+ 2 - 0
api/index.js

@@ -25,4 +25,6 @@ export * from '@/api/upms/dept'
 export * from '@/api/upms/user-dept'
 export * from '@/api/store/storecheckjob'
 export * from '@/api/wechat/index'
+// 操作类按钮的登录校验与登录方式选择弹框,页面统一用 this.$Http.requireLogin(...) 调用
+export { requireLogin, goLogin } from '@/utils/http'
 /* export * from '@/api/upload/index' */

+ 20 - 0
api/repair/index.js

@@ -221,6 +221,26 @@ export function transferRepair (parameter) {
     })
 }
 /**
+ * 转派审批通过
+ */
+export function transferApprove (parameter) {
+    return http({
+        url: '/repair/application-forms/transfer-approve/' + parameter.id,
+        method: 'PUT',
+        data: parameter
+    })
+}
+/**
+ * 转派审批驳回
+ */
+export function transferReject (parameter) {
+    return http({
+        url: '/repair/application-forms/transfer-reject/' + parameter.id,
+        method: 'PUT',
+        data: parameter
+    })
+}
+/**
    * update func
    * parameter: { }
    * @param parameter

+ 7 - 0
pages.json

@@ -398,6 +398,13 @@
 			}
 		},
 		{
+			"path": "pages/transfer-approval/transfer-approval",
+			"style": {
+				"navigationBarTitleText": "转派审批",
+				"enablePullDownRefresh": true
+			}
+		},
+		{
 			"path": "pages/edit-name/edit-name",
 			"style": {
 				"navigationBarTitleText": "",

+ 40 - 35
pages/equipment-detail/equipment-detail.vue

@@ -403,40 +403,43 @@ export default {
                 url: '/pages/check/check?sbId=' + this.infoData.id
             });
         },
-        // 常用操作跳转
+        // 常用操作跳转(报修/运行填报/巡检填报均会提交数据,未登录先弹登录)
         handleAdd(index) {
-            if (index === 1) {
-                uni.navigateTo({
-                    url:
-                        '/pages/repair-add/repair-add?detailId=' +
-                        this.detailId
-                });
-            } else if (index === 2) {
-                uni.navigateTo({
-                    url: '/pages/running/running-edit?sbId=' + this.detailId
-                });
-            } else if (index === 3) {
-                uni.navigateTo({
-                    url:
-                        '/pages/inspection/inspection-edit?sbId=' +
-                        this.detailId
-                });
-            }
+            this.$Http.requireLogin(() => {
+                if (index === 1) {
+                    uni.navigateTo({
+                        url:
+                            '/pages/repair-add/repair-add?detailId=' +
+                            this.detailId
+                    });
+                } else if (index === 2) {
+                    uni.navigateTo({
+                        url: '/pages/running/running-edit?sbId=' + this.detailId
+                    });
+                } else if (index === 3) {
+                    uni.navigateTo({
+                        url:
+                            '/pages/inspection/inspection-edit?sbId=' +
+                            this.detailId
+                    });
+                }
+            }, '发起报修/填报需要先登录,是否登录?');
         },
         change(index) {
             this.current = index;
         },
         handleStart() {
             // this.infoData.status = 2;
-            var that = this;
-            uni.navigateTo({
-                url:
-                    '/pages/equipment-detail/equipment-status?sbId=' +
-                    this.detailId + '&sbNo=' + this.infoData.no +
-                    '&sbName=' + this.infoData.name +
-                    '&preStatus=' + this.infoData.status +
-                    '&afterStatus=' + 2
-            });
+            this.$Http.requireLogin(() => {
+                uni.navigateTo({
+                    url:
+                        '/pages/equipment-detail/equipment-status?sbId=' +
+                        this.detailId + '&sbNo=' + this.infoData.no +
+                        '&sbName=' + this.infoData.name +
+                        '&preStatus=' + this.infoData.status +
+                        '&afterStatus=' + 2
+                });
+            }, '启用设备需要先登录,是否登录?');
             /* this.$Http.updateSbInfo(this.infoData).then((res) => {
                 that.$refs.uToast.show({
                     title: '已启用',
@@ -446,15 +449,17 @@ export default {
             }); */
         },
         handleStop() {
-            this.infoData.status = 7;
-            var that = this;
-            this.$Http.updateSbInfo(this.infoData).then((res) => {
-                that.$refs.uToast.show({
-                    title: '已停用',
-                    type: 'default',
-                    icon: false
+            this.$Http.requireLogin(() => {
+                this.infoData.status = 7;
+                var that = this;
+                this.$Http.updateSbInfo(this.infoData).then((res) => {
+                    that.$refs.uToast.show({
+                        title: '已停用',
+                        type: 'default',
+                        icon: false
+                    });
                 });
-            });
+            }, '停用设备需要先登录,是否登录?');
         }
     }
 };

+ 10 - 1
pages/equipment-detail/equipment-status.vue

@@ -110,7 +110,11 @@ export default {
             this.form.sbName = options.sbName;
             this.form.preStatus = options.preStatus;
             this.form.afterStatus = options.afterStatus;
-            this.form.actualUser = this.$BaseTool.UserUtil.getUserInfo().realName;
+            // 扫码免登录进入时 getUserInfo() 为空,不能直接取 realName(会报错导致表单打不开)
+            const userInfo = this.$BaseTool.UserUtil.getUserInfo();
+            if (userInfo != null) {
+                this.form.actualUser = userInfo.realName;
+            }
         }
     },
     onReady() {
@@ -130,6 +134,11 @@ export default {
             this.form.changeTime = null
         },
         handleSubmit() {
+            this.$Http.requireLogin(() => {
+                this.doSubmit();
+            }, '提交设备启用/停用需要先登录,是否登录?');
+        },
+        doSubmit() {
             console.log('点击')
             let time = null;
             if (this.form.changeTime != null) {

+ 8 - 0
pages/equipment-file/equipment-img.vue

@@ -10,6 +10,7 @@
             :max-size="5 * 100 * 1024"
             @on-success="uploadSuccess"
             @on-remove="uploadRemove"
+            :before-upload="beforeUpload"
             :header="header">
         </u-upload>
         <view class="d-flex j-around a-center footer">
@@ -66,7 +67,14 @@ export default {
             this.imagesList.push(data.data)
             console.log(this.imagesList);
         },
+        // 上传前登录校验:u-upload 直传不走统一请求拦截,未登录不会触发 401 登录弹框,需在此拦住
+        beforeUpload() {
+            return this.$Http.requireLogin(null, '上传设备图片需要先登录,是否登录?');
+        },
         handleSubmit() {
+            if (!this.$Http.requireLogin(null, '保存设备图片需要先登录,是否登录?')) {
+                return
+            }
             const params = {
                 sbFileList: this.imagesList,
                 id: this.detailId

+ 9 - 7
pages/maintain-add/maintain-add-ignore.vue

@@ -133,14 +133,16 @@ export default {
     methods: {
         // 提交
         submit() {
-            this.$Http.finishJobIgnores(this.form).then((res) => {
-                uni.showToast({
-                    title: res.message
+            this.$Http.requireLogin(() => {
+                this.$Http.finishJobIgnores(this.form).then((res) => {
+                    uni.showToast({
+                        title: res.message
+                    });
+                    uni.navigateTo({
+                        url: '../maintain/maintain-ignore'
+                    });
                 });
-                uni.navigateTo({
-                    url: '../maintain/maintain-ignore'
-                });
-            });
+            }, '完成保养任务需要先登录,是否登录?');
         },
         initData() {
             this.$Http

+ 16 - 10
pages/maintain-detail/maintain-detail-ignore.vue

@@ -76,20 +76,26 @@ export default {
             });
         },
         handleExecute() {
-            this.$Http.executeJobIgnores(this.infoData).then((res) => {
-                this.initData();
-                uni.showToast({ title: '已接收,请及时完成任务' });
-            });
+            this.$Http.requireLogin(() => {
+                this.$Http.executeJobIgnores(this.infoData).then((res) => {
+                    this.initData();
+                    uni.showToast({ title: '已接收,请及时完成任务' });
+                });
+            }, '接收保养任务需要先登录,是否登录?');
         },
         handleFinish() {
-            uni.navigateTo({
-                url: '/pages/maintain-add/maintain-add-ignore?detailId=' + this.detailId
-            });
+            this.$Http.requireLogin(() => {
+                uni.navigateTo({
+                    url: '/pages/maintain-add/maintain-add-ignore?detailId=' + this.detailId
+                });
+            }, '完成保养任务需要先登录,是否登录?');
         },
         handleRepair() {
-            uni.navigateTo({
-                url: '/pages/repair-add/repair-add?detailId=' + this.infoData.sbId
-            });
+            this.$Http.requireLogin(() => {
+                uni.navigateTo({
+                    url: '/pages/repair-add/repair-add?detailId=' + this.infoData.sbId
+                });
+            }, '发起报修需要先登录,是否登录?');
         },
         initData() {
             this.$Http

+ 20 - 14
pages/maintain/maintain-ignore.vue

@@ -190,15 +190,19 @@ export default {
       })
     },
     handleExecute(item) {
-      this.$Http.executeJobIgnores(item).then(res => {
-        item.status = this.$DictCache.VALUE.CHECK_JOB_STATUS.EXECUTING
-        uni.showToast({ title: '已开始,请及时完成任务' })
-      })
+      this.$Http.requireLogin(() => {
+        this.$Http.executeJobIgnores(item).then(res => {
+          item.status = this.$DictCache.VALUE.CHECK_JOB_STATUS.EXECUTING
+          uni.showToast({ title: '已开始,请及时完成任务' })
+        })
+      }, '接收保养任务需要先登录,是否登录?')
     },
     handleFinish(item) {
-      uni.navigateTo({
-        url: '/pages/maintain-add/maintain-add?detailId=' + item.id
-      })
+      this.$Http.requireLogin(() => {
+        uni.navigateTo({
+          url: '/pages/maintain-add/maintain-add?detailId=' + item.id
+        })
+      }, '完成保养任务需要先登录,是否登录?')
     },
     // tab
     change(index, value) {
@@ -243,14 +247,16 @@ export default {
     },
     // 提交
     submit(item) {
-      item.actualStartTime = uni.getStorageSync('check-job-start')
-      item.status = 3
-      this.$Http.finishJobIgnores(item).then(res => {
-        uni.showToast({
-          title: '已完成'
+      this.$Http.requireLogin(() => {
+        item.actualStartTime = uni.getStorageSync('check-job-start')
+        item.status = 3
+        this.$Http.finishJobIgnores(item).then(res => {
+          uni.showToast({
+            title: '已完成'
+          })
+          this.initData()
         })
-        this.initData()
-      })
+      }, '完成保养任务需要先登录,是否登录?')
     },
     initData() {
       const that = this

+ 12 - 2
pages/my/my.vue

@@ -49,9 +49,12 @@
                 v-show="$BaseTool.UserUtil.getUserInfo() != null && ($BaseTool.UserUtil.isRepairRole() || $BaseTool.UserUtil.isManagerRole())"
                 icon="icon-weixiu" title="维修任务" :tipNum="gatherTask.repairTask"
                 @handleLubrication="handleService"></my-card>
-            <my-card v-show="$BaseTool.UserUtil.getUserInfo() != null && $BaseTool.UserUtil.isManagerRole()"
+            <my-card v-show="$BaseTool.UserUtil.getUserInfo() != null && ($BaseTool.UserUtil.isManagerRole() || $BaseTool.UserUtil.isExamineRole())"
                 icon="icon-tubiaozhizuomoban-135" title="审核任务" :tipNum="gatherTask.repairCheckTask"
                 @handleLubrication="handleRepairCheck"></my-card>
+            <my-card v-show="$BaseTool.UserUtil.getUserInfo() != null && ($BaseTool.UserUtil.isManagerRole() || $BaseTool.UserUtil.isExamineRole())"
+                icon="icon-tubiaozhizuomoban-135" title="转派审批" :tipNum="gatherTask.transferApproveTask"
+                @handleLubrication="handleTransferApproval"></my-card>
             <!--            <my-card
                 icon="icon-tubiaozhizuomoban-135"
                 title="盘点任务"
@@ -84,7 +87,8 @@ export default {
                 lubricationTaskMonth: 0,
                 oilTask: 0,
                 repairTask: 0,
-                repairCheckTask: 0
+                repairCheckTask: 0,
+                transferApproveTask: 0
             },
             myAvatar: '',
             tabbar: [
@@ -176,6 +180,12 @@ export default {
                 url: '../repair-check/repair-check'
             })
         },
+        // 转派审批
+        handleTransferApproval() {
+            uni.navigateTo({
+                url: '../transfer-approval/transfer-approval'
+            })
+        },
         // 盘点任务
         handleStoreCheck() {
             uni.navigateTo({

+ 3 - 0
pages/repair-add/repair-add.vue

@@ -543,6 +543,9 @@ export default {
             this.form.needStop = value
         },
         handleSubmit() {
+            if (!this.$Http.requireLogin(null, '提交报修单需要先登录,是否登录?')) {
+                return
+            }
             const params = {
                 content: this.form.content,
                 actualUser: this.form.actualUser,

+ 6 - 1
pages/service/service.vue

@@ -28,7 +28,12 @@
                             $DictCache.VALUE.REPAIR_APPLICATION_FORM_STATUS.NOT_ALLOCATED">
                             <view @click.stop="handleDealEdit(item)">接收</view>
                         </view>
-                        <view class="text-color-3 f-size-32 edit" v-if="!item.fourRepairUser && item.repairUserId">
+                        <!-- 仅维修中(状态2)可转派,与后端 transfer 接口状态校验保持一致;审核中(状态4)等状态不显示 -->
+                        <view class="text-color-3 f-size-32 edit" v-if="
+                            item.status ===
+                            $DictCache.VALUE.REPAIR_APPLICATION_FORM_STATUS.PROCESSING &&
+                            !item.fourRepairUser && item.repairUserId
+                        ">
                             <view @click.stop="handleDispatching(item)">转派</view>
                         </view>
                         <view class="text-color-3 f-size-32 edit" v-if="

+ 192 - 0
pages/transfer-approval/transfer-approval.vue

@@ -0,0 +1,192 @@
+<template>
+    <view>
+        <view class="">
+            <template v-if="listData.length > 0">
+                <view
+                    class="mx3 my2 p3 bg-color-white box p-relative"
+                    v-for="(item, index) in listData"
+                    :key="index"
+                    @tap="handleDetail(item.id)"
+                >
+                    <view class="d-flex j-between border-bottom pb-2 a-center">
+                        <view class="text-color-b f-size-26">{{ item.sbName }}</view>
+                        <my-badge text="转派审批中" status="processing" />
+                    </view>
+                    <view class="mt-2 d-flex j-between">
+                        <view class="text-color-3 f-size-32 f-weight-bold">{{
+                            item.userName
+                        }}</view>
+                    </view>
+                    <view class="f-size-26 text-color-6 f-weight-4 mt-2">{{
+                        item.applyTime
+                    }}</view>
+                    <view class="d-flex j-end edit-box">
+                        <view class="text-color-3 f-size-32 edit">
+                            <view @click.stop="handleApprove(item)">同意</view>
+                        </view>
+                        <view class="text-color-3 f-size-32 edit">
+                            <view @click.stop="handleReject(item)">驳回</view>
+                        </view>
+                    </view>
+                </view>
+                <view class="py3">
+                    <u-loadmore :status="status" />
+                </view>
+            </template>
+            <template v-else>
+                <view class="empty">
+                    <u-empty text="暂无待审批的转派工单" mode="list"></u-empty>
+                </view>
+            </template>
+        </view>
+    </view>
+</template>
+
+<script>
+export default {
+    data() {
+        return {
+            // 加载更多
+            status: 'loadmore',
+            page: 1,
+            limit: 10,
+            loadMore: true,
+            listData: []
+        };
+    },
+    onPullDownRefresh() {
+        this.page = 1;
+        this.loadMore = true;
+        this.listData = [];
+        this.initData();
+        uni.stopPullDownRefresh();
+    },
+    onReachBottom() {
+        if (this.loadMore) {
+            this.page++;
+            this.initData();
+        }
+    },
+    onShow() {
+        this.page = 1;
+        this.loadMore = true;
+        this.listData = [];
+        this.initData();
+    },
+    methods: {
+        // 同意转派
+        handleApprove(item) {
+            const that = this;
+            uni.showModal({
+                title: '同意转派',
+                editable: true,
+                placeholderText: '可填写审批意见(选填)',
+                success: function (res) {
+                    if (res.confirm) {
+                        that.$Http
+                            .transferApprove({
+                                id: item.id,
+                                repairDispatchRemark: res.content || ''
+                            })
+                            .then(() => {
+                                uni.showToast({ title: '已同意', icon: 'success' });
+                                that.refresh();
+                            });
+                    }
+                }
+            });
+        },
+        // 驳回转派
+        handleReject(item) {
+            const that = this;
+            uni.showModal({
+                title: '驳回转派',
+                editable: true,
+                placeholderText: '请填写驳回原因',
+                success: function (res) {
+                    if (res.confirm) {
+                        if (!res.content || !res.content.trim()) {
+                            uni.showToast({ title: '请填写驳回原因', icon: 'none' });
+                            return;
+                        }
+                        that.$Http
+                            .transferReject({
+                                id: item.id,
+                                repairDispatchRemark: res.content
+                            })
+                            .then(() => {
+                                uni.showToast({ title: '已驳回', icon: 'success' });
+                                that.refresh();
+                            });
+                    }
+                }
+            });
+        },
+        // 跳转详情(复用报修审核详情,含转派记录展示)
+        handleDetail(id) {
+            uni.navigateTo({
+                url: '/pages/repair-check/repair-detail?detailId=' + id
+            });
+        },
+        refresh() {
+            this.page = 1;
+            this.loadMore = true;
+            this.listData = [];
+            this.initData();
+        },
+        // 初始化:查询转派审批中(searchType=4)的工单
+        initData() {
+            const that = this;
+            that.status = 'loading';
+            this.$Http
+                .getRepairApplicationFormPage({
+                    pageNum: this.page,
+                    pageSize: this.limit,
+                    searchType: 4,
+                    filter: 0,
+                    dataScope: {
+                        sortBy: 'desc',
+                        sortName: 'update_time'
+                    }
+                })
+                .then((res) => {
+                    const data = res.data.rows;
+                    if (data && data.length > 0 && data.length < that.limit) {
+                        that.status = 'nomore';
+                        that.loadMore = false;
+                    } else if (data.length == that.limit) {
+                        that.status = 'loadmore';
+                    } else {
+                        that.status = 'nomore';
+                    }
+                    that.listData = [...that.listData, ...data];
+                });
+        }
+    }
+};
+</script>
+
+<style lang="less">
+page {
+  background-color: #f4f4f4;
+}
+
+.box {
+  border-radius: 20rpx;
+  padding-right: 0;
+}
+
+.edit-box {
+  padding: 0 30rpx;
+  margin-top: 40rpx;
+
+  .edit {
+    padding: 8rpx 40rpx;
+    border: 1px solid #ddd;
+    border-radius: 20rpx;
+    font-size: 26rpx;
+    color: #666;
+    margin-left: 30rpx;
+  }
+}
+</style>

+ 8 - 5
pages/work-order/work-order.vue

@@ -44,21 +44,24 @@
                         <view
                             class="text-color-3 f-size-32 edit"
                             v-if="
-
-                                item.status ===
+                                [
                                     $DictCache.VALUE
-                                        .REPAIR_APPLICATION_FORM_STATUS.REBACK
+                                        .REPAIR_APPLICATION_FORM_STATUS.NOT_ALLOCATED,
+                                    $DictCache.VALUE
+                                        .REPAIR_APPLICATION_FORM_STATUS.REBACK,
+                                ].includes(item.status)
                             "
                         >
                             <view @click.stop="handleDealEdit(item)">接收</view>
                         </view>
+                        <!-- 仅维修中(状态2)可转派,与后端 transfer 接口状态校验保持一致 -->
                         <view
                             class="text-color-3 f-size-32 edit"
                             v-if="
 
                                 item.status ===
                                     $DictCache.VALUE
-                                        .REPAIR_APPLICATION_FORM_STATUS.REBACK
+                                        .REPAIR_APPLICATION_FORM_STATUS.PROCESSING
                             "
                         >
                             <view
@@ -74,7 +77,7 @@
                                         .REPAIR_APPLICATION_FORM_STATUS
                                         .PROCESSING,
                                     $DictCache.VALUE
-                                        .REPAIR_APPLICATION_FORM_STATUS.RETURN,
+                                        .REPAIR_APPLICATION_FORM_STATUS.REBACK,
                                 ].includes(item.status)
                             "
                         >

+ 8 - 2
utils/dict.js

@@ -557,7 +557,10 @@ DictCache.VALUE = {
         WAIT_SUBMIT: 3, // 待审核
         NOT_ACCEPTANCE: 4, // 审核中
         REBACK: 5, // 已驳回
-        FINISHED: 6 // 已完成
+        FINISHED: 6, // 已完成
+        REFUSED: 7, // 已拒单
+        TRANSFER_APPROVAL: 8, // 转派审批中
+        ENTRUST_REPAIR: 9 // 委外维修中
     },
     /**
      * 维修状态
@@ -893,7 +896,10 @@ DictCache.COLOR = {
         3: 'success', // 已完成
         4: 'default', // 已撤销
         5: 'warning', // 待分配
-        6: 'success' // 已分配
+        6: 'success', // 已分配
+        7: 'error', // 已拒单
+        8: 'processing', // 转派审批中
+        9: 'processing' // 委外维修中
 
     },
     /**

+ 77 - 20
utils/http.js

@@ -3,6 +3,82 @@ import BaseTool from '@/utils/tool';
 import { loginQuickly } from '@/api/upms/user';
 // 根据开发环境或生产环境破设置服务器根域名
 let ajaxTimes = 0;
+
+/**
+ * 记录登录前的页面路径,供登录成功后回跳
+ * 扫码场景:免登录查看设备详情 → 点操作弹登录 → 登录后应回到原页面继续操作,而不是重新扫码
+ */
+const recordBackUrl = () => {
+    try {
+        const pages = getCurrentPages();
+        if (!pages || pages.length === 0) {
+            return;
+        }
+        const cur = pages[pages.length - 1];
+        const opts = cur.options || (cur.$page && cur.$page.options) || {};
+        const query = Object.keys(opts)
+            .map(k => k + '=' + encodeURIComponent(opts[k]))
+            .join('&');
+        uni.setStorageSync('login_back_url', '/' + cur.route + (query ? '?' + query : ''));
+    } catch (e) {
+        // 取不到当前页面时忽略,登录后走默认首页
+    }
+};
+
+/**
+ * 取出并清除登录回跳地址(由登录成功逻辑调用)
+ */
+export const takeBackUrl = () => {
+    const url = uni.getStorageSync('login_back_url');
+    uni.removeStorageSync('login_back_url');
+    return url;
+};
+
+/**
+ * 登录方式选择弹框:账号登录 / 微信快捷登录
+ * 供两处复用:1) 接口返回 401 登录过期;2) 未登录点击操作类按钮时的前置拦截
+ */
+export const goLogin = (content) => {
+    recordBackUrl();
+    uni.showModal({
+        content: content || '该操作需要登录后进行,请先登录',
+        showCancel: true,
+        cancelText: '账号登录',
+        confirmText: '快捷登录',
+        success: res => {
+            if (res.confirm) {
+                loginQuickly().then(res => {
+                    const url = res.data
+
+                    if (url !== '' && url !== null && url != undefined) {
+                      // 请求用户授权
+                      window.location.href = url
+                    }
+                })
+            } else if (res.cancel) {
+                uni.reLaunch({ url: '/pages/login/login' })
+            }
+        }
+    })
+};
+
+/**
+ * 操作类按钮统一登录校验(扫码查看设备信息无需登录,但提交类操作必须先登录)
+ * @param {Function} next 已登录时执行的实际操作
+ * @param {String} content 未登录时的提示文案
+ * @return {Boolean} 是否已登录
+ */
+export const requireLogin = (next, content) => {
+    if (BaseTool.UserUtil.isLogin()) {
+        if (typeof next === 'function') {
+            next();
+        }
+        return true;
+    }
+    goLogin(content);
+    return false;
+};
+
 export default ({ url, method, data }) => {
     let closeLoading = true;
     return new Promise((resolve, reject) => {
@@ -28,26 +104,7 @@ export default ({ url, method, data }) => {
                         return reject(res.data)
                     }
                     // #endif
-                    uni.showModal({
-                        content: res.data.message,
-                        showCancel: true,
-                        cancelText: '账号登录',
-                        confirmText: '快捷登录',
-                        success: res => {
-                            if (res.confirm) {
-                                loginQuickly().then(res => {
-                                    const url = res.data
-
-                                    if (url !== '' && url !== null && url != undefined) {
-                                      // 请求用户授权
-                                      window.location.href = url
-                                    }
-                                })
-                            } else if (res.cancel) {
-                                uni.reLaunch({ url: '/pages/login/login' })
-                            }
-                        }
-                    })
+                    goLogin(res.data.message);
                     return reject(res.data)
                 }
                 if (res.data.code === 403) {

+ 45 - 14
utils/tool.js

@@ -43,26 +43,52 @@ BaseTool.UserUtil = {
         '/upms/users/register',
         '/upms/users/check/',
         '/ignores/'],
+    /**
+     * /ignores 免登录通道中的写操作接口
+     * 这些接口后端 permitAll,但已登录时必须携带 token,
+     * 否则后端 checkAndGetUser 取不到登录人,只能拿设备保管人/表单姓名兜底留痕
+     */
+    WRITE_IGNORES: [
+        '/ignores/sb/status-logs',
+        '/ignores/sb/status/',
+        '/ignores/sb/images/',
+        '/ignores/check/jobs/execute',
+        '/ignores/check/jobs/finish'],
+    isLogin: () => {
+        return !!uni.getStorageSync('accessToken');
+    },
     getUserInfo: () => {
         return uni.getStorageSync('userInfo').userInfo;
     },
-    isRepairRole: () => {
-        if (uni.getStorageSync('userInfo').userInfo == null) {
-            return false;
-        } else if (uni.getStorageSync('userInfo').userInfo.roleName === '维修人员') {
-            return true;
-        } else {
+    /**
+     * 判断当前登录用户是否拥有指定角色码之一
+     * 基于 userInfo.roleCodes 完整列表,不依赖单一 roleName 字符串(多角色账号 roleName 只取排序最前的一个,易误判)
+     * @param {Array|String} codes 角色码,如 ['Maintenance','MM']
+     */
+    hasRoleCode: codes => {
+        const info = uni.getStorageSync('userInfo').userInfo;
+        if (info == null) {
             return false;
         }
+        const roleCodes = info.roleCodes || [];
+        const list = Array.isArray(codes) ? codes : [codes];
+        return list.some(code => roleCodes.indexOf(code) !== -1);
+    },
+    isRepairRole: () => {
+        // 维修人员(Maintenance) 或 维修主管(MM):与后端接单权限保持一致
+        return BaseTool.UserUtil.hasRoleCode(['Maintenance', 'MM']);
     },
     isManagerRole: () => {
-        if (uni.getStorageSync('userInfo').userInfo == null) {
-            return false;
-        } else if (uni.getStorageSync('userInfo').userInfo.roleName === '超级管理员') {
-            return true;
-        } else {
+        const info = uni.getStorageSync('userInfo').userInfo;
+        if (info == null) {
             return false;
         }
+        // 超级管理员:优先用 superAdmin 标记,兼容旧的 roleName 判断
+        return info.superAdmin === 1 || info.superAdmin === true || info.roleName === '超级管理员';
+    },
+    isExamineRole: () => {
+        // 维修审核(REPAIR_EXAMINE):手机端“审核任务”入口
+        return BaseTool.UserUtil.hasRoleCode(['REPAIR_EXAMINE']);
     },
     isUseRole: () => {
         if (uni.getStorageSync('userInfo').userInfo == null) {
@@ -77,9 +103,14 @@ BaseTool.UserUtil = {
         return that.$store.state.userInfo.userInfo;
     },
     getToken: url => {
-        for (const notUrl of BaseTool.UserUtil.NOT_LOGIN) {
-            if (url.indexOf(notUrl) !== -1) {
-                return '';
+        // 写操作接口不在免 token 名单内:登录后携带 token,便于后端留痕真实操作人
+        const isWriteApi = (url || '').indexOf('/ignores/') !== -1
+            && BaseTool.UserUtil.WRITE_IGNORES.some(item => url.indexOf(item) !== -1);
+        if (!isWriteApi) {
+            for (const notUrl of BaseTool.UserUtil.NOT_LOGIN) {
+                if (url.indexOf(notUrl) !== -1) {
+                    return '';
+                }
             }
         }
         let token = '';