odf_new/odf-uniapp/pages/fault-add/index.vue
zpc a5039edcbb
Some checks are pending
continuous-integration/drone/push Build is running
feat: 添加表显里程矫正功能和故障频次管理
- 在故障添加页面新增表显里程矫正输入框
- 在故障详情页面显示表显里程矫正信息
- 实现故障频次增加功能,允许用户通过按钮增加故障发生频次
- 更新后端服务以支持故障频次的增减和相关数据的返回
2026-03-28 23:17:34 +08:00

599 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="fault-add-page">
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
<view class="content">
<!-- 顶部导航栏 -->
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-bar-inner">
<image
class="nav-icon"
src="/static/images/ic_back.png"
mode="aspectFit"
@click="goBack"
/>
<text class="nav-title">新增故障</text>
<view class="nav-icon-placeholder" />
</view>
</view>
<!-- 拍照区域 -->
<view class="photo-area">
<scroll-view class="photo-scroll" scroll-x>
<view class="photo-list">
<view class="photo-add-btn" @click="takePhoto">
<text class="plus-icon">+</text>
<text class="add-text">点击拍摄</text>
</view>
<image
class="photo-thumb"
v-for="(photo, index) in photoList"
:key="index"
:src="photo"
mode="aspectFill"
/>
</view>
</scroll-view>
</view>
<!-- 表单区域 -->
<view class="form-area">
<view class="form-group">
<text class="form-label">故障时间</text>
<view class="form-display">
<text class="display-text">{{ form.faultTime || '拍摄第一张照片后自动填充' }}</text>
</view>
</view>
<view class="form-group">
<text class="form-label">人员</text>
<input
class="form-input"
v-model="form.personnel"
placeholder="请输入"
placeholder-class="input-placeholder"
/>
</view>
<view class="form-group">
<text class="form-label">故障原因</text>
<input
class="form-input"
v-model="form.faultReason"
placeholder="请输入"
placeholder-class="input-placeholder"
/>
</view>
<view class="form-group">
<text class="form-label">表显故障里程</text>
<input
class="form-input"
v-model="form.mileage"
placeholder="请输入"
placeholder-class="input-placeholder"
/>
</view>
<view class="form-group">
<text class="form-label">表显里程矫正</text>
<input
class="form-input"
v-model="form.mileageCorrection"
placeholder="选填"
placeholder-class="input-placeholder"
/>
</view>
<view class="form-group">
<text class="form-label">所属光缆</text>
<view class="form-display">
<text class="display-text">{{ form.cableName }}</text>
</view>
</view>
<view class="form-group">
<text class="form-label">地点</text>
<view class="location-btn" @click="getLocation">
<text class="location-btn-text">点击获取当前经纬度</text>
</view>
<text class="location-text">当前经度:{{ form.longitude }} 当前纬度:{{ form.latitude }}</text>
</view>
<view class="form-group">
<text class="form-label">备注</text>
<textarea
class="form-textarea"
v-model="form.remark"
placeholder="请输入"
placeholder-class="input-placeholder"
/>
</view>
</view>
</view>
<!-- 底部固定提交按钮 -->
<view class="bottom-bar">
<view class="submit-btn" :class="{ 'submit-btn-disabled': submitting }" @click.stop="handleSubmit">
<text class="submit-btn-text">{{ submitting ? '提交中...' : '提交故障' }}</text>
</view>
</view>
<!-- 离屏 canvas 用于水印绘制APP端需要在屏幕内才能渲染 -->
<canvas
canvas-id="watermarkCanvas"
:style="{
position: 'fixed',
left: '0px',
top: '0px',
width: canvasW + 'px',
height: canvasH + 'px',
opacity: 0,
pointerEvents: 'none',
zIndex: -1
}"
/>
</view>
</template>
<script setup>
import { ref, reactive, getCurrentInstance, nextTick } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { addFault } from '@/services/trunk'
import { addWatermark } from '@/utils/watermark'
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
const photoList = ref([])
const cableId = ref('')
const submitting = ref(false)
const canvasW = ref(100)
const canvasH = ref(100)
const instance = getCurrentInstance()
const form = reactive({
faultTime: '',
personnel: '',
faultReason: '',
mileage: '',
mileageCorrection: '',
cableName: '',
latitude: 0,
longitude: 0,
remark: ''
})
function goBack() {
uni.navigateBack()
}
function takePhoto() {
uni.chooseImage({
count: 1,
sourceType: ['camera'],
success(res) {
const tempPath = res.tempFilePaths[0]
photoList.value.push(tempPath)
// 第一张照片自动填充故障时间
if (photoList.value.length === 1) {
const now = new Date()
const y = now.getFullYear()
const m = String(now.getMonth() + 1).padStart(2, '0')
const d = String(now.getDate()).padStart(2, '0')
const h = String(now.getHours()).padStart(2, '0')
const min = String(now.getMinutes()).padStart(2, '0')
form.faultTime = `${y}/${m}/${d} ${h}:${min}`
}
}
})
}
function getLocation() {
// #ifdef H5
uni.showLoading({ title: '定位中...', mask: true })
if (window.AMap) {
AMap.plugin('AMap.Geolocation', () => {
const geolocation = new AMap.Geolocation({
enableHighAccuracy: true,
timeout: 10000
})
geolocation.getCurrentPosition((status, result) => {
uni.hideLoading()
if (status === 'complete' && result.position) {
form.longitude = result.position.lng
form.latitude = result.position.lat
uni.showToast({ title: '获取成功', icon: 'success' })
} else {
console.error('[GPS-H5] 高德定位失败:', result)
uni.showToast({ title: '获取位置失败,请检查浏览器定位权限', icon: 'none' })
}
})
})
} else {
uni.hideLoading()
uni.showToast({ title: '地图SDK加载失败', icon: 'none' })
}
// #endif
// #ifndef H5
doGetLocation()
// #endif
}
// #ifndef H5
function doGetLocation() {
uni.showLoading({ title: '定位中...', mask: true })
console.log('[GPS] 开始获取位置, type=gcj02, isHighAccuracy=true')
uni.getLocation({
type: 'gcj02',
isHighAccuracy: true,
highAccuracyExpireTime: 10000,
success(res) {
console.log('[GPS] 获取成功:', JSON.stringify({
latitude: res.latitude,
longitude: res.longitude,
accuracy: res.accuracy
}))
form.latitude = res.latitude
form.longitude = res.longitude
uni.hideLoading()
uni.showToast({ title: '获取成功', icon: 'success' })
},
fail(err) {
console.error('[GPS] uni.getLocation 失败:', JSON.stringify(err))
// uni.getLocation 失败后,尝试 plus 原生定位
fallbackPlusLocation(err)
}
})
}
function fallbackPlusLocation(originalErr) {
// #ifdef APP-PLUS
if (typeof plus !== 'undefined' && plus.geolocation) {
console.log('[GPS] 尝试 plus.geolocation 回退定位')
plus.geolocation.getCurrentPosition(
(pos) => {
console.log('[GPS] plus定位成功:', JSON.stringify({
latitude: pos.coords.latitude,
longitude: pos.coords.longitude,
accuracy: pos.coords.accuracy
}))
form.latitude = pos.coords.latitude
form.longitude = pos.coords.longitude
uni.hideLoading()
uni.showToast({ title: '获取成功', icon: 'success' })
},
(plusErr) => {
console.error('[GPS] plus定位也失败:', JSON.stringify(plusErr))
uni.hideLoading()
handleLocationError(originalErr)
},
{ provider: 'system', coordsType: 'gcj02', timeout: 15000 }
)
} else {
console.error('[GPS] plus.geolocation 不可用')
uni.hideLoading()
handleLocationError(originalErr)
}
// #endif
// #ifndef APP-PLUS
uni.hideLoading()
handleLocationError(originalErr)
// #endif
}
function handleLocationError(err) {
const errMsg = (err.errMsg || '').toLowerCase()
if (errMsg.includes('deny') || errMsg.includes('auth') || errMsg.includes('permission')) {
console.warn('[GPS] 判断为权限问题,弹出设置引导')
uni.showModal({
title: '定位权限未开启',
content: '请在系统设置中允许本应用使用定位服务',
confirmText: '去设置',
success(modalRes) {
if (modalRes.confirm) {
uni.openSetting && uni.openSetting()
}
}
})
} else {
console.warn('[GPS] 非权限问题errMsg:', errMsg)
uni.showToast({ title: '获取位置失败请检查GPS是否开启', icon: 'none', duration: 3000 })
}
}
// #endif
async function handleSubmit() {
if (photoList.value.length === 0) {
uni.showToast({ title: '请至少拍摄一张照片', icon: 'none' })
return
}
if (!cableId.value) {
uni.showToast({ title: '所属光缆信息缺失,无法提交', icon: 'none' })
return
}
if (submitting.value) return
submitting.value = true
uni.showLoading({ title: '提交中...', mask: true })
try {
// 水印处理
const watermarkLines = [
`${form.faultTime} ${form.personnel}`,
`故障原因:${form.faultReason || ''}`,
`经度:${form.longitude} 纬度:${form.latitude}`
]
const watermarkedPhotos = []
for (const photo of photoList.value) {
try {
const result = await addWatermark(photo, watermarkLines, {
canvasId: 'watermarkCanvas',
proxy: instance.proxy,
setSize(w, h) {
canvasW.value = w
canvasH.value = h
}
})
watermarkedPhotos.push(result)
// 每张图处理完后等一下,让 canvas 状态重置
await nextTick()
} catch (err) {
watermarkedPhotos.push(photo)
}
}
// 构建上传数据
const files = watermarkedPhotos.map((path, index) => ({
name: 'images',
uri: path
}))
const formData = {
files,
data: {
cableId: cableId.value,
faultTime: form.faultTime,
personnel: form.personnel,
faultReason: form.faultReason,
mileage: form.mileage,
mileageCorrection: form.mileageCorrection,
latitude: String(form.latitude),
longitude: String(form.longitude),
remark: form.remark
}
}
const res = await addFault(formData)
if (res.code === 200) {
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
uni.showToast({ title: res.msg || '提交失败', icon: 'none' })
}
} catch (err) {
uni.showToast({ title: '网络异常,请重试', icon: 'none' })
} finally {
uni.hideLoading()
submitting.value = false
}
}
onLoad((options) => {
if (options.cableId) {
cableId.value = options.cableId
}
if (options.cableName) {
form.cableName = decodeURIComponent(options.cableName)
}
})
</script>
<style scoped>
.fault-add-page {
position: relative;
min-height: 100vh;
background-color: #F5F5F5;
padding-bottom: 120rpx;
}
.bg-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 500rpx;
z-index: 0;
}
.content {
position: relative;
z-index: 1;
}
.nav-bar {
width: 100%;
}
.nav-bar-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 24rpx;
}
.nav-icon {
width: 44rpx;
height: 44rpx;
}
.nav-icon-placeholder {
width: 44rpx;
height: 44rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #fff;
}
.photo-area {
padding: 24rpx;
}
.photo-scroll {
white-space: nowrap;
}
.photo-list {
display: inline-flex;
align-items: center;
}
.photo-add-btn {
width: 200rpx;
height: 200rpx;
background: #fff;
border: 2rpx dashed #CCCCCC;
border-radius: 12rpx;
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.plus-icon {
font-size: 48rpx;
color: #999;
}
.add-text {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.photo-thumb {
width: 200rpx;
height: 200rpx;
border-radius: 12rpx;
margin-left: 16rpx;
flex-shrink: 0;
}
.form-area {
padding: 0 24rpx;
}
.form-group {
margin-bottom: 32rpx;
}
.form-label {
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
font-weight: 500;
display: block;
}
.form-input {
height: 80rpx;
padding: 0 24rpx;
background: #fff;
border-radius: 12rpx;
border: 1rpx solid #E8E8E8;
font-size: 28rpx;
color: #333;
}
.form-display {
height: 80rpx;
padding: 0 24rpx;
background: #F5F5F5;
border-radius: 12rpx;
border: 1rpx solid #E8E8E8;
display: flex;
align-items: center;
}
.display-text {
font-size: 28rpx;
color: #333;
}
.form-textarea {
min-height: 200rpx;
padding: 24rpx;
background: #fff;
border-radius: 12rpx;
border: 1rpx solid #E8E8E8;
font-size: 28rpx;
color: #333;
width: 100%;
box-sizing: border-box;
position: relative;
z-index: 0;
}
.input-placeholder {
color: #999;
}
.location-btn {
background: #1A73EC;
border-radius: 12rpx;
padding: 16rpx 0;
text-align: center;
width: 100%;
}
.location-btn-text {
color: #fff;
font-size: 28rpx;
}
.location-text {
font-size: 26rpx;
color: #999;
margin-top: 12rpx;
display: block;
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
padding: 24rpx;
background: #fff;
box-sizing: border-box;
z-index: 9999;
}
.submit-btn {
width: 100%;
height: 88rpx;
background: #1A73EC;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 1;
}
.submit-btn-text {
color: #fff;
font-size: 32rpx;
pointer-events: none;
}
.submit-btn-disabled {
background: #93bdf5;
}
</style>