Some checks are pending
continuous-integration/drone/push Build is running
- 在故障添加页面新增表显里程矫正输入框 - 在故障详情页面显示表显里程矫正信息 - 实现故障频次增加功能,允许用户通过按钮增加故障发生频次 - 更新后端服务以支持故障频次的增减和相关数据的返回
599 lines
14 KiB
Vue
599 lines
14 KiB
Vue
<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>
|