Prechádzať zdrojové kódy

feat(equipment): 新增点检日历及报修优化多项功能

- 新增近期点检日历页面,按月展示设备保养完成记录并支持月切换
- 设备详情页添加"近期点检日历"入口及对应路由注册
- 新增点检日历查询接口 getSbCheckCalendarIgnores
- 新增查询未完结报修单接口 queryUncompletedRepairs
- 报修提交前增加重复报修提醒,存在未完结工单时弹窗确认
- 保养列表与报修列表添加关键词搜索功能,默认限定近三个月范围
dazhaxie 1 deň pred
rodič
commit
8e8d01aea4

+ 14 - 0
api/ignore/index.js

@@ -209,6 +209,20 @@ export function getSbTaskIgnores (parameter) {
         }
     })
 }
+
+/**
+ * 设备近期点检日历(数据源为保养任务),按月返回已完成的保养任务,已完成的日期在日历上标绿打勾
+ * parameter: { sbId, month } month 形如 2026-10,不传默认当月
+ */
+export function getSbCheckCalendarIgnores (parameter) {
+    return http({
+        url: '/ignores/sb/check-calendar/' + parameter.sbId + '?' + stringify({ month: parameter.month }),
+        method: 'get',
+        headers: {
+            'Content-Type': 'application/json;charset=UTF-8'
+        }
+    })
+}
 // /**
 //  * page func
 //  * parameter: { }

+ 13 - 0
api/repair/index.js

@@ -43,6 +43,19 @@ export function addRepairApplicationForm (parameter) {
 }
 
 /**
+ * 查询设备未完结的报修单(排除已完成/已拒单),用于提交报修前的重复报修提醒
+ */
+export function queryUncompletedRepairs (parameter) {
+    return http({
+        url: '/repair/application-forms/uncompleted?' + stringify(parameter),
+        method: 'get',
+        headers: {
+            'Content-Type': 'application/json;charset=UTF-8'
+        }
+    })
+}
+
+/**
  * 派工
  */
 export function dispatchModelByDTO (parameter) {

+ 6 - 0
pages.json

@@ -123,6 +123,12 @@
 			}
 		},
 		{
+			"path": "pages/equipment-calendar/equipment-calendar",
+			"style": {
+				"navigationBarTitleText": "近期点检日历"
+			}
+		},
+		{
 			"path": "pages/running/running",
 			"style": {
 				"navigationBarTitleText": "运行记录列表",

+ 257 - 0
pages/equipment-calendar/equipment-calendar.vue

@@ -0,0 +1,257 @@
+<template>
+    <view class="px3 pb-3">
+        <!-- 月份切换:默认看当月,可往前翻历史月份 -->
+        <view class="d-flex a-center j-between bg-color-white p2 mt-2" style="border-radius: 16rpx">
+            <view class="month-btn" @tap="changeMonth(-1)">‹ 上一月</view>
+            <view class="f-size-32 f-weight-bold text-color-3">{{ monthLabel }}</view>
+            <view class="month-btn" @tap="changeMonth(1)">下一月 ›</view>
+        </view>
+
+        <!-- 当月汇总 -->
+        <view class="p2 mt-2 bg-color-white" style="border-radius: 16rpx">
+            <view class="d-flex a-center">
+                <text class="f-size-28 text-color-3">本月已点检</text>
+                <text class="f-size-36 f-weight-bold count-text mx2">{{ calendar.finishDays || 0 }}</text>
+                <text class="f-size-28 text-color-3">天,共 {{ calendar.finishNum || 0 }} 项任务</text>
+            </view>
+            <view class="f-size-26 text-color-6 mt-1">最后一次点检:{{ lastFinishText }}</view>
+        </view>
+
+        <!-- 日历:已完成的日期标绿打勾 -->
+        <view class="p2 mt-2 bg-color-white" style="border-radius: 16rpx">
+            <view class="d-flex">
+                <view class="day-cell week-head" v-for="(week, index) in weeks" :key="'week' + index">{{ week }}</view>
+            </view>
+            <view class="d-flex f-wrap">
+                <view v-for="(cell, index) in cells" :key="index"
+                      class="day-cell"
+                      :class="{ 'day-checked': cell.checked, 'day-today': cell.today }"
+                      @tap="handleDay(cell)">
+                    <text v-if="!cell.blank" class="day-num">{{ cell.day }}</text>
+                    <text v-if="cell.checked" class="day-mark">√</text>
+                </view>
+            </view>
+            <view class="d-flex a-center mt-2 f-size-24 text-color-6">
+                <text class="legend legend-checked"></text>
+                <text class="ml-1">已点检</text>
+                <text class="legend legend-normal ml-3"></text>
+                <text class="ml-1">未点检</text>
+                <text class="legend legend-today ml-3"></text>
+                <text class="ml-1">今天</text>
+            </view>
+        </view>
+
+        <view class="f-size-24 text-color-9 mt-2">
+            数据来源为本设备的保养(点检)任务,绿色打勾表示当天有已完成记录;点击任意日期可查看当天的任务名称、执行人与完成时间。
+        </view>
+
+        <u-popup v-model="dayShow" mode="bottom" border-radius="20">
+            <view class="p3">
+                <view class="f-size-30 f-weight-bold text-center">{{ selectedDate }} 点检记录</view>
+                <view v-if="selectedJobs.length === 0" class="f-size-28 text-color-9 mt-3">该日无已完成记录</view>
+                <view v-for="(job, index) in selectedJobs" :key="index" class="job-item p2 mt-2">
+                    <view class="f-size-28 text-color-3">{{ job.name || job.standardName || '保养任务' }}</view>
+                    <view class="f-size-24 text-color-6 mt-1">
+                        执行人:{{ job.actualUser || job.checkUserName || job.checkDeptName || '-' }}
+                    </view>
+                    <view class="f-size-24 text-color-6 mt-1">完成时间:{{ job.actualEndTime || '-' }}</view>
+                </view>
+                <u-button type="primary" @click="dayShow = false" style="margin-top: 20rpx">关闭</u-button>
+            </view>
+        </u-popup>
+    </view>
+</template>
+
+<script>
+export default {
+    data() {
+        return {
+            sbId: '',
+            // 设备详情页带过来的“上次保养完成时间”,当月无记录时兜底展示
+            deviceLastFinish: '',
+            month: '',
+            weeks: ['日', '一', '二', '三', '四', '五', '六'],
+            cells: [],
+            calendar: {},
+            dayShow: false,
+            selectedDate: '',
+            selectedJobs: []
+        };
+    },
+    computed: {
+        monthLabel() {
+            if (!this.month) {
+                return '';
+            }
+            const parts = this.month.split('-');
+            return parts[0] + '年' + Number(parts[1]) + '月';
+        },
+        lastFinishText() {
+            // 优先显示当月最后一次完成时间,当月没做过则显示设备上的历史最近时间
+            return this.calendar.lastFinishTime || this.deviceLastFinish || '暂无';
+        }
+    },
+    onLoad(options) {
+        this.sbId = options.sbId || options.detailId;
+        this.deviceLastFinish = options.last ? decodeURIComponent(options.last) : '';
+        this.month = this.currentMonth();
+        this.loadData();
+    },
+    methods: {
+        pad(value) {
+            return value < 10 ? '0' + value : '' + value;
+        },
+        currentMonth() {
+            const now = new Date();
+            return now.getFullYear() + '-' + this.pad(now.getMonth() + 1);
+        },
+        changeMonth(offset) {
+            const parts = this.month.split('-');
+            const target = new Date(Number(parts[0]), Number(parts[1]) - 1 + offset, 1);
+            const now = new Date();
+            // 未来月份没有意义,直接拦掉,避免现场误判为“漏保养”
+            if (
+                target.getFullYear() > now.getFullYear() ||
+                (target.getFullYear() === now.getFullYear() && target.getMonth() + 1 > now.getMonth() + 1)
+            ) {
+                uni.showToast({ title: '暂不支持查看未来月份', icon: 'none' });
+                return;
+            }
+            this.month = target.getFullYear() + '-' + this.pad(target.getMonth() + 1);
+            this.loadData();
+        },
+        loadData() {
+            this.$Http
+                .getSbCheckCalendarIgnores({
+                    sbId: this.sbId,
+                    month: this.month
+                })
+                .then((res) => {
+                    this.calendar = res.data || {};
+                    this.buildCells(res.data && res.data.dayList);
+                });
+        },
+        // 按月历格子生成:前置空格对齐星期,末尾补空格凑满整行
+        buildCells(dayList) {
+            const parts = this.month.split('-');
+            const year = Number(parts[0]);
+            const month = Number(parts[1]);
+            const finishedMap = {};
+            (dayList || []).forEach((item) => {
+                finishedMap[item.date] = item;
+            });
+            const todayStr = this.formatDate(new Date());
+            const cells = [];
+            const firstWeekday = new Date(year, month - 1, 1).getDay();
+            for (let i = 0; i < firstWeekday; i++) {
+                cells.push({ blank: true });
+            }
+            const daysOfMonth = new Date(year, month, 0).getDate();
+            for (let day = 1; day <= daysOfMonth; day++) {
+                const date = year + '-' + this.pad(month) + '-' + this.pad(day);
+                const finished = finishedMap[date];
+                cells.push({
+                    blank: false,
+                    date: date,
+                    day: day,
+                    checked: !!finished,
+                    today: date === todayStr,
+                    jobList: finished ? finished.jobList : []
+                });
+            }
+            while (cells.length % 7 !== 0) {
+                cells.push({ blank: true });
+            }
+            this.cells = cells;
+        },
+        formatDate(date) {
+            return date.getFullYear() + '-' + this.pad(date.getMonth() + 1) + '-' + this.pad(date.getDate());
+        },
+        handleDay(cell) {
+            if (cell.blank) {
+                return;
+            }
+            this.selectedDate = cell.date;
+            this.selectedJobs = cell.jobList || [];
+            this.dayShow = true;
+        }
+    }
+};
+</script>
+
+<style lang="less">
+.month-btn {
+    font-size: 26rpx;
+    color: #0f6fb9;
+    padding: 6rpx 16rpx;
+}
+
+.count-text {
+    color: #19be6b;
+}
+
+.day-cell {
+    width: 14.2857%;
+    height: 92rpx;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    box-sizing: border-box;
+    font-size: 26rpx;
+    color: #333333;
+}
+
+.week-head {
+    height: 50rpx;
+    font-size: 24rpx;
+    color: #999999;
+}
+
+.day-num {
+    line-height: 34rpx;
+}
+
+.day-mark {
+    font-size: 22rpx;
+    line-height: 24rpx;
+    color: #ffffff;
+}
+
+/* 已点检:绿底白字打勾 */
+.day-checked {
+    background-color: #19be6b;
+    border-radius: 10rpx;
+    color: #ffffff;
+}
+
+.day-today {
+    border: 2rpx solid #0f6fb9;
+    border-radius: 10rpx;
+}
+
+.legend {
+    display: inline-block;
+    width: 24rpx;
+    height: 24rpx;
+    border-radius: 6rpx;
+}
+
+.legend-checked {
+    background-color: #19be6b;
+}
+
+.legend-normal {
+    background-color: #f2f3f5;
+}
+
+.legend-today {
+    background-color: #ffffff;
+    border: 2rpx solid #0f6fb9;
+}
+
+.job-item {
+    background-color: #f7f8fa;
+    border-radius: 12rpx;
+}
+</style>

+ 18 - 0
pages/equipment-detail/equipment-detail.vue

@@ -148,6 +148,11 @@
                     <view class="iconfont icon-start main-color-text"></view>
                     <view class="grid-text">启用</view>
                 </u-grid-item>
+                <!-- 近期点检日历:只看历史已完成记录,扫码免登录即可查看 -->
+                <u-grid-item @tap="handleCheckCalendar">
+                    <view class="iconfont icon-xunjianguanli main-color-text"></view>
+                    <view class="grid-text">近期点检日历</view>
+                </u-grid-item>
                 <!-- 扫码进入的设备详情页隐藏停用按钮 -->
                 <!--<u-grid-item v-if="infoData.status != 7" @tap="handleStop()">
                     <view class="iconfont icon-stop1 main-color-text"></view>
@@ -404,6 +409,19 @@ export default {
                 url: '/pages/check/check?sbId=' + this.infoData.id
             });
         },
+        // 近期点检日历(数据源为保养任务,已完成的日期标绿打勾)
+        handleCheckCalendar() {
+            // 带上设备“上次保养完成时间”,当月未做保养时日历仍然能看到最近一次记录
+            const last = this.infoData.lastMaintainFinishTime
+                ? encodeURIComponent(this.infoData.lastMaintainFinishTime)
+                : '';
+            uni.navigateTo({
+                url:
+                    '/pages/equipment-calendar/equipment-calendar?sbId=' +
+                    this.detailId +
+                    '&last=' + last
+            });
+        },
         // 常用操作跳转(报修/运行填报/巡检填报均会提交数据,未登录先弹登录)
         handleAdd(index) {
             this.$Http.requireLogin(() => {

+ 22 - 0
pages/maintain/maintain.vue

@@ -1,5 +1,9 @@
 <template>
   <view class="container">
+    <!-- 关键词搜索栏:与设备台账(equipment.vue)一致,支持设备名称/编号/保养标准编号 -->
+    <div class="bg-color-white py2">
+      <u-search placeholder="设备名称/编号/保养标准编号" clearabled v-model="keyword" @search="search" @custom="search" @clear="search"></u-search>
+    </div>
     <common-tabs :list="list" :current="current" @change="change" />
     <view class="p-top8">
       <template v-if="listData.length > 0">
@@ -66,6 +70,7 @@ export default {
       nums: '开始计时',
       // 加载更多
       status: 'loadmore',
+      keyword: '',
       page: 1,
       timer: null,
       limit: 10,
@@ -242,13 +247,30 @@ export default {
       //     this.initData();
       // });
     },
+    // 点击搜索:重置分页后按关键词查询
+    search() {
+      this.page = 1
+      this.loadMore = true
+      this.listData = []
+      this.initData()
+    },
+    // 关键词搜索默认时间起点:近三个月,避免大表多列 LIKE 全表扫描,保证查询性能(后端未执行/执行中按 start_time,已完成按 actual_start_time 过滤)
+    getSearchStartTime() {
+      const d = new Date()
+      d.setMonth(d.getMonth() - 3)
+      return this.$BaseTool.Date.formatter(d, this.$BaseTool.Date.PICKER_NORM_DATETIME_PATTERN)
+    },
     initData() {
       const that = this
       that.status = 'loading'
+      const kw = (this.keyword || '').trim()
       this.$Http
         .getCheckJobPage({
           pageNum: this.page,
           pageSize: this.limit,
+          keyword: kw,
+          // 仅在关键词搜索时默认限定近三个月,普通列表加载行为保持不变
+          searchStartTime: kw ? this.getSearchStartTime() : null,
           type: 2,
           filter: this.filter,
           searchType: this.searchType,

+ 37 - 6
pages/middle/middle.vue

@@ -511,13 +511,44 @@ export default {
             // this.form.photo = a.join(',');
             this.$refs.uForm.validate(valid => {
                 if (valid) {
-                    this.$Http.addRepairApplicationForm(params).then(res => {
-                        this.resetForm()
-                        uni.navigateTo({
-                            url: '/pages/repair/repair'
-                        })
-                    })
+                    // 重复报修检查:该设备存在未完结报修单时需先确认再提交(允许一台设备多个报修单)
+                    this.checkDuplicateThenAdd(params)
+                }
+            })
+        },
+        // 提交前检查该设备是否存在未完结报修单,存在则弹确认框,确认后仍允许提交
+        checkDuplicateThenAdd(params) {
+            var that = this
+            this.$Http.queryUncompletedRepairs({ sbId: params.sbId }).then(res => {
+                const list = (res && res.data) || []
+                if (list.length === 0) {
+                    that.doAddRepair(params)
+                    return
                 }
+                const first = list[0]
+                const name = params.sbName || '该设备'
+                // 展示最新一笔未完结报修单的状态与报修内容,内容过长截断
+                const desc = (first.content || '').length > 30 ? `${first.content.slice(0, 30)}…` : first.content || ''
+                const detail = `${first.no} ${first.statusLabel || ''}${desc ? ',内容:' + desc : ''}`
+                uni.showModal({
+                    title: '重复报修提醒',
+                    content: `【${name}】已有 ${list.length} 个未完结报修单(最新:${detail}),是否继续提交新报修单?`,
+                    confirmText: '继续提交',
+                    cancelText: '取消',
+                    success: (r) => {
+                        if (r.confirm) {
+                            that.doAddRepair(params)
+                        }
+                    }
+                })
+            })
+        },
+        doAddRepair(params) {
+            this.$Http.addRepairApplicationForm(params).then(res => {
+                this.resetForm()
+                uni.navigateTo({
+                    url: '/pages/repair/repair'
+                })
             })
         },
         // 报修提交成功后清除表单数据

+ 46 - 14
pages/repair-add/repair-add.vue

@@ -568,21 +568,53 @@ export default {
             var that = this
             this.$refs.uForm.validate(valid => {
                 if (valid) {
-                    this.$Http.addRepairApplicationForm(params).then(res => {
-                        const sbId = that.form.sbId
-                        that.resetForm()
-                        uni.showToast({
-                            icon: 'none',
-                            title: '报修单已提交!',
-                            duration: 2000
-                        })
-                        setTimeout(function () {
-                            uni.navigateTo({
-                                url: '/pages/equipment-detail/equipment-detail?detailId=' + sbId
-                            })
-                        }, 2000)
-                    })
+                    // 重复报修检查:该设备存在未完结报修单时需先确认再提交(允许一台设备多个报修单)
+                    that.checkDuplicateThenAdd(params)
+                }
+            })
+        },
+        // 提交前检查该设备是否存在未完结报修单,存在则弹确认框,确认后仍允许提交
+        checkDuplicateThenAdd(params) {
+            var that = this
+            this.$Http.queryUncompletedRepairs({ sbId: params.sbId }).then(res => {
+                const list = (res && res.data) || []
+                if (list.length === 0) {
+                    that.doAddRepair(params)
+                    return
                 }
+                const first = list[0]
+                const name = params.sbName || '该设备'
+                // 展示最新一笔未完结报修单的状态与报修内容,内容过长截断
+                const desc = (first.content || '').length > 30 ? `${first.content.slice(0, 30)}…` : first.content || ''
+                const detail = `${first.no} ${first.statusLabel || ''}${desc ? ',内容:' + desc : ''}`
+                uni.showModal({
+                    title: '重复报修提醒',
+                    content: `【${name}】已有 ${list.length} 个未完结报修单(最新:${detail}),是否继续提交新报修单?`,
+                    confirmText: '继续提交',
+                    cancelText: '取消',
+                    success: (r) => {
+                        if (r.confirm) {
+                            that.doAddRepair(params)
+                        }
+                    }
+                })
+            })
+        },
+        doAddRepair(params) {
+            var that = this
+            this.$Http.addRepairApplicationForm(params).then(res => {
+                const sbId = that.form.sbId
+                that.resetForm()
+                uni.showToast({
+                    icon: 'none',
+                    title: '报修单已提交!',
+                    duration: 2000
+                })
+                setTimeout(function () {
+                    uni.navigateTo({
+                        url: '/pages/equipment-detail/equipment-detail?detailId=' + sbId
+                    })
+                }, 2000)
             })
         },
         // 报修提交成功后清除表单数据

+ 22 - 0
pages/repair/repair.vue

@@ -1,6 +1,10 @@
 <template>
     <view>
         <my-add-button @handleClick="handleClick" />
+        <!-- 关键词搜索栏:与设备台账(equipment.vue)一致,后端 selectPageList keyword 已覆盖报修内容/报修人/维修人(含二三四维修人)等字段 -->
+        <div class="bg-color-white py2">
+            <u-search placeholder="设备名称/编号/单号/报修内容/报修人/维修人" clearabled v-model="keyword" @search="search" @custom="search" @clear="search"></u-search>
+        </div>
         <!-- 	<view class="border-bottom">
 			<u-tabs class="p-fixed left0 right0 zIndex" :list="list" :is-scroll="false" :current="current" @change="change"></u-tabs>
 		</view> -->
@@ -79,6 +83,7 @@ export default {
             current: 0,
             // 加载更多
             status: 'loadmore',
+            keyword: '',
             page: 1,
             limit: 10,
             type: '',
@@ -195,14 +200,31 @@ export default {
                 url: '/pages/repair-detail/repair-detail?detailId=' + id
             });
         },
+        // 点击搜索:重置分页后按关键词查询
+        search() {
+            this.page = 1;
+            this.loadMore = true;
+            this.listData = [];
+            this.initData();
+        },
+        // 关键词搜索默认时间起点:近三个月,避免大表多列 LIKE 全表扫描,保证查询性能(后端按 apply_time 过滤)
+        getSearchStartTime() {
+            const d = new Date();
+            d.setMonth(d.getMonth() - 3);
+            return this.$BaseTool.Date.formatter(d, this.$BaseTool.Date.PICKER_NORM_DATETIME_PATTERN);
+        },
         // 初始化
         initData() {
             const that = this;
             that.status = 'loading';
+            const kw = (this.keyword || '').trim();
             this.$Http
                 .getRepairApplicationFormPage({
                     pageNum: this.page,
                     pageSize: this.limit,
+                    keyword: kw,
+                    // 仅在关键词搜索时默认限定近三个月,普通列表加载行为保持不变
+                    searchStartTime: kw ? this.getSearchStartTime() : null,
                     // status: that.$DictCache.VALUE.REPAIR_APPLICATION_FORM_STATUS.NOT_ALLOCATED,
                     dataScope: {
                       sortBy: 'asc, desc',