This commit is contained in:
2026-03-11 14:52:32 +08:00
commit 688e46e1a6
105 changed files with 16711 additions and 0 deletions

263
pages/index/addDelivery.vue Normal file
View File

@@ -0,0 +1,263 @@
<template>
<view class="container">
<z-paging ref="pagingRef" class="containerBox" v-model="listData" :default-page-size="queryParams.pageSize"
@query="queryList">
<view class="box">
<view v-for="(item, index) in listData" :key="index" @tap="changeSelect(index)" class="item"
:style="index == selectIndex ? 'border: #199793 4rpx solid;' : ''">
<uv-icon v-show="index == selectIndex" class="selectIcon" color="#199793" size="20"
name="checkbox-mark"></uv-icon>
<view class="title">单据号{{ item.billNo }}</view>
<view><text>出库类型</text>{{ item.operationTypeName }}</view>
<view><text>理货员</text>{{ item.operatorName }}</view>
<view><text>施工队</text>{{ item.teamName }}</view>
<view><text>出库时间</text>{{ parseTime(item.operationTime) }}</view>
<view class="more" v-show="!item.showMore" @tap="openMore(index)">
<view>详细信息</view>
<uv-icon name="arrow-right" color="#A4A6A7" size="28rpx" style="margin-left: 10rpx;"></uv-icon>
</view>
</view>
</view>
</z-paging>
<view class="btn">
<uv-button type="primary" text="确定" size="large" style="width: 48%;" @tap="toAddHandDelivery">
</uv-button>
<uv-button type="primary" text="确定" size="large" style="width: 48%;" class="mainBtn" @tap="openMore(selectIndex)">
</uv-button>
</view>
<uv-modal ref="modalRef" title="单据明细" class="modalInfo" confirmText="确定" showCancelButton @close="closeModal" @confirm="confirmSelected"
:closeOnClickOverlay="false" asyncClose>
<view class="box">
<view v-for="items in detailList" class="item" :style="items.selected ? 'border-color: #2979ff;' : ''" @tap="changeSelected(items)">
<uv-icon v-show="items.selected" class="selectIcon" color="#2979ff" size="20" name="checkbox-mark"></uv-icon>
<view><text>订单编号</text>{{ items.sapNo }}</view>
<view><text>项目描述</text>{{ items.xmMs }}</view>
<view><text>项目号</text>{{ items.xmNo }}</view>
<view><text>物料号</text>{{ items.wlNo }}</view>
<view><text>物料描述</text>{{ items.wlMs }}</view>
<view><text>供应商名称</text>{{ items.gysMc }}</view>
<view><text>存放位置</text>{{ items.pcode || "-" }}</view>
<view><text>身份码</text>{{ items.entityId || "-" }}</view>
<view><text>备注</text>{{ items.remark || '-' }}</view>
</view>
</view>
</uv-modal>
</view>
</template>
<script setup>
import { ref, computed } from "vue";
import { onLoad, onShow, onPageScroll } from "@dcloudio/uni-app";
import { selectInfo } from "@/api/index"
// selectInfoDetail
const selectIndex = ref(0)
const changeSelect = (index) => {
selectIndex.value = index
}
const queryParams = ref({
pageNum: 1,
pageSize: 10
})
const pagingRef = ref(null)
const listData = ref([])
// 获取列表
const queryList = (pageNo, pageSize) => {
queryParams.value.pageNum = pageNo
console.log(pageNo, pageSize, queryParams.value)
selectInfo(queryParams.value).then(res => {
console.log(res.data)
res.data.forEach(e => {
e.showMore = false;
e.recordList.forEach(e => {
e.totalWeight = e.realQty * e.weightKg
e.totalVolume = e.realQty * e.volumeM3
e.rkRecordId = e.id
e.selected = false
})
});
pagingRef.value.complete(res.data)
}).catch(res => {
pagingRef.value.complete(false)
})
}
// const getDetail = (index) => {
// return new Promise((resolve, reject) => {
// if (listData.value[index].detailList.length > 0) {
// detailList.value = listData.value[index].detailList
// resolve("完成")
// } else {
// selectInfoDetail({ billNoCk: listData.value[index].billNoCk }).then(res => {
// res.rows.forEach(e => {
// e.totalWeight = e.realQty * e.weightKg
// e.totalVolume = e.realQty * e.volumeM3
// })
// detailList.value = res.rows
// listData.value[index].detailList = res.rows
// resolve()
// }).catch(res => {
// reject(res)
// })
// }
// })
// }
// 跳转至录入货物信息页面
const toAddHandDelivery = () => {
uni.navigateTo({
url: '/pages/index/addHandDelivery',
})
}
// 打开弹窗
const detailList = ref([])
const openMore = async (index) => {
// await getDetail(index)
console.log(listData.value[index])
detailList.value = listData.value[index].recordList
modalRef.value.open()
};
// 弹窗
const modalRef = ref()
const changeSelected = (items) => {
items.selected = !items.selected
}
const closeModal = () => {
console.log("关闭")
listData.value[selectIndex.value].showMore = false
}
const confirmSelected = async () => {
// await getDetail(selectIndex.value)
let selectedList = JSON.parse(JSON.stringify(listData.value[selectIndex.value]))
selectedList.detailList = selectedList.recordList.filter(e => e.selected)
if (selectedList.detailList.length == 0) {
uni.showToast({
title: "请选择货物信息",
icon: "none"
})
modalRef.value.closeLoading()
return
}
uni.navigateTo({
url: '/pages/index/addDeliveryInfo',
success: function(res) {
// 通过eventChannel向被打开页面传送数据
res.eventChannel.emit('acceptData', { data: selectedList })
modalRef.value.close()
}
})
}
onLoad(() => {
})
onShow(() => {
pagingRef.value?.reload()
})
</script>
<style scoped lang="scss">
.container {
position: relative;
.containerBox {
padding: 32rpx;
padding-bottom: 120rpx;
}
.btn {
position: fixed;
width: calc(100vw - 64rpx);
bottom: 0;
left: 32rpx;
z-index: 99;
display: flex;
align-items: center;
justify-content: space-between;
}
}
.box {
// background-color: #ebebeb;
// background-color: #f5f5f5;
// padding-top: 20rpx;
.item {
border: #969696 4rpx solid;
padding: 20rpx;
margin-top: 20rpx;
line-height: 50rpx;
font-size: 28rpx;
border-radius: 8rpx;
color: #333;
position: relative;
.title {
font-size: 32rpx;
font-weight: bold;
display: flex;
align-items: center;
}
text {
color: #666;
}
.more {
color: #A4A6A7;
display: flex;
align-items: center;
justify-content: flex-end;
}
}
}
.selectIcon {
position: absolute;
top: 10rpx;
right: 10rpx;
}
.modalInfo {
.box {
width: 100%;
max-height: 60vh;
overflow: auto;
.item {
background: #efefef;
padding: 20rpx;
margin-top: 20rpx;
line-height: 50rpx;
font-size: 28rpx;
border-radius: 8rpx;
color: #333;
border: 2rpx solid #efefef;
position: relative;
}
}
}
::v-deep uni-toast {
z-index: 30000 !important;
}
</style>

View File

@@ -0,0 +1,372 @@
<template>
<view class="container">
<uv-form labelPosition="top" labelWidth="100" :model="formModel" :rules="deliveryRules" ref="deliveryFormRef">
<view class="formContent">
<view class="title">配送信息</view>
<uv-line color="#a8a8a8"></uv-line>
<uv-form-item label="发货地" prop="originName">
<uv-input placeholder="请输入内容" v-model="formModel.originName" clearable />
</uv-form-item>
<uv-form-item label="目的地" prop="destName">
<uv-input placeholder="请输入内容" v-model="formModel.destName" clearable />
</uv-form-item>
<uv-form-item label="配送日期" prop="deliveryDate">
<view class="carPickerText" :style="formModel.deliveryDate ? 'color: #303133;' : ''" @tap="openCalendar">{{ formModel.deliveryDate || '请选择配送日期'}}</view>
</uv-form-item>
<uv-form-item label="发货人" prop="shipperName">
<uv-input placeholder="请输入内容" v-model="formModel.shipperName" clearable />
</uv-form-item>
<uv-form-item label="发货人电话" prop="shipperPhone">
<uv-input placeholder="请输入内容" maxlength="11" type="number" v-model="formModel.shipperPhone" clearable />
</uv-form-item>
<uv-form-item label="接收人" prop="receiverName">
<uv-input placeholder="请输入内容" v-model="formModel.receiverName" clearable />
</uv-form-item>
<uv-form-item label="接收人电话" prop="receiverPhone">
<uv-input placeholder="请输入内容" maxlength="11" type="number" v-model="formModel.receiverPhone" clearable />
</uv-form-item>
<uv-form-item label="接收单位" prop="receiverOrgName">
<uv-input placeholder="请输入内容" v-model="formModel.receiverOrgName" clearable />
</uv-form-item>
</view>
<view class="formContent" style="margin-top: 20rpx;">
<view class="title">货物信息</view>
<uv-line color="#a8a8a8"></uv-line>
<view style="display: flex;align-items: center;">
<!-- <uv-form-item label="配送吨位" prop="deliveryTon">
<uv-input placeholder="请输入内容" v-model="formModel.deliveryTon"clearable />
</uv-form-item> -->
<view style="flex: 1;">
<!-- <uv-form-item label="高速费用" prop="tollFee">
<uv-input placeholder="请输入内容" type="digit" v-model="formModel.tollFee" clearable />
</uv-form-item> -->
<uv-form-item label="总公里数" prop="totalKm">
<uv-input placeholder="请输入内容" type="digit" v-model="formModel.totalKm" clearable />
</uv-form-item>
</view>
<view class="matchCar" @tap="autoComputed(true)">
<view>匹配</view>
<view>车型</view>
</view>
</view>
</view>
<view class="formContent" style="margin-top: 20rpx;">
<view class="title">配送车辆</view>
<uv-line color="#a8a8a8"></uv-line>
<uv-form-item label="车型" prop="vehicleTypeName">
<view class="carPickerText" :style="formModel.vehicleTypeName ? 'color: #303133;' : ''" @tap="openCarPicker">{{ formModel.vehicleTypeName || '请选择配送车型'}}</view>
</uv-form-item>
</view>
</uv-form>
<uv-picker ref="carPicker" :columns="carColumns" keyName="name" @confirm="carConfirm"></uv-picker>
<uv-calendar ref="calendar" @confirm="confirm"></uv-calendar>
<view class="priceBtn">
<view>预估价格:<text style="color: #D55A66;">¥</text><text class="price">{{ formModel.suggestFee || "0.00" }}</text></view>
<uv-button type="primary" size="large" class="mainBtn" @click="submitForm">发布配送</uv-button>
</view>
</view>
</template>
<script setup>
import { ref, computed, getCurrentInstance } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { autoCarAndPrice, sendOrder, getCarList } from "@/api/index"
const deliveryFormRef = ref()
const formModel = ref({
originName: '',
destName: '',
deliveryDate: '',
shipperName: '',
shipperPhone: '',
receiverName: '',
receiverPhone: '',
receiverOrgName: '',
tollFee: '',
totalKm: '',
vehicleTypeName: '',
// originName: '贾庵子',
// destName: '路北供电室',
// deliveryDate: '2025-10-28',
// shipperName: '林总',
// shipperPhone: '13000000000',
// receiverName: '王总',
// receiverPhone: '13100000000',
// receiverOrgName: '供电所',
// tollFee: '',
// totalKm: '10',
// vehicleTypeName: '',
})
const deliveryRules = ref({
'originName': {
type: 'string',
required: true,
message: '请输入起点位置',
trigger: ['blur', 'change']
},
'destName': {
type: 'string',
required: true,
message: '请输入终点位置',
trigger: ['blur', 'change']
},
'deliveryDate': {
type: 'string',
required: true,
message: '请选择配送日期',
trigger: ['blur', 'change']
},
'shipperName': {
type: 'string',
required: true,
message: '请输入发货人',
trigger: ['blur', 'change']
},
'shipperPhone': [
{
type: 'string',
required: true,
message: '请输入发货人电话',
trigger: ['blur', 'change']
},
{
validator: (rule, value, callback) => {
// 自定义校验逻辑
return uni.$uv.test.mobile(value)
},
message: '请输入正确的手机号',
trigger: ['blur', 'change']
}
],
'receiverName': {
type: 'string',
required: true,
message: '请输入接收人',
trigger: ['blur', 'change']
},
'receiverPhone': [
{
type: 'string',
required: true,
message: '请输入接收人电话',
trigger: ['blur', 'change']
},
{
validator: (rule, value, callback) => {
// 自定义校验逻辑
return uni.$uv.test.mobile(value)
},
message: '请输入正确的手机号',
trigger: ['blur', 'change']
}
],
'receiverOrgName': {
type: 'string',
required: true,
message: '请输入接收单位',
trigger: ['blur', 'change']
},
'tollFee': {
type: 'float',
required: true,
message: '请输入高速费用',
trigger: ['blur', 'change']
},
'totalKm': {
type: 'float',
required: true,
message: '请输入总公里数',
trigger: ['blur', 'change']
},
'vehicleTypeName': {
type: 'string',
required: true,
message: '请选择配送车型',
trigger: ['blur', 'change']
},
})
// 配送日期选择器
const openCalendar = () => {
calendar.value.open()
}
const calendar = ref()
const confirm = (e) => {
console.log(e)
formModel.value.deliveryDate = e[0]
}
// 配送车辆选择器
const openCarPicker = () => {
carPicker.value.open()
}
const carPicker = ref()
const carColumns = ref([])
const carConfirm = (e) => {
console.log(e)
// formModel.value.vehicleTypeId = e.indexs
formModel.value.vehicleTypeName = e.value[0].name
formModel.value.vehicleTypeId = e.value[0].id
autoComputed(false)
}
const submitForm = () => {
console.log(formModel.value)
deliveryFormRef.value.validate().then(res => {
let params = {
...formModel.value,
items:selectedInfo.value.detailList
}
console.log(params)
sendOrder(params).then(res => {
console.log(res)
uni.$uv.toast('发布成功')
uni.reLaunch({ url: '/pages/index/index' })
})
}).catch(errors => {
})
}
let selectedInfo = ref({})
const autoComputed = (status) => {
if (!formModel.value.totalKm) {
uni.$uv.toast('请输入总公里数')
return
}
let selectedParams = ref({
weightTon: 0,
volumeM3: 0,
distanceKm: 0,
})
selectedParams.value.weightTon = formModel.value.deliveryTon
selectedParams.value.volumeM3 = formModel.value.goodsSize
selectedParams.value.distanceKm = formModel.value.totalKm
if (!status) {
selectedParams.value.vehicleTypeId = formModel.value.vehicleTypeId
}
autoCarAndPrice(selectedParams.value).then(res => {
console.log(res)
if (res.data.errorMessage) {
uni.$uv.toast(res.data.errorMessage)
return
}
if (status) {
console.log("变")
carColumns.value[0] = res.data.candidates
formModel.value.vehicleTypeName = res.data.vehicleTypeName
formModel.value.vehicleTypeId = res.data.vehicleTypeId
}
formModel.value.suggestFee = res.data.suggestFee
})
}
// const getCarDataList = () => {
// let obj = {
// page: 1,
// pageSize: 100,
// }
// getCarList(obj).then(res => {
// console.log(res)
// // carColumns.value[0] = res.rows
// })
// }
onLoad(() => {
const instance = getCurrentInstance().proxy
const eventChannel = instance.getOpenerEventChannel();
eventChannel.on('acceptData', function(data) {
console.log('acceptData',data)
selectedInfo.value = data.data
if (data.totalWeightKg) {
formModel.value.deliveryTon = data.totalWeightKg / 1000
formModel.value.goodsSize = data.goodsSize / 1000
} else {
formModel.value.deliveryTon = selectedInfo.value.detailList.reduce((pre, cur) => pre + cur.totalWeight * 100000, 0) / 100000000
formModel.value.goodsSize = selectedInfo.value.detailList.reduce((pre, cur) => pre + cur.totalVolume * 100000, 0) / 100000000
}
console.log(formModel.value)
// getCarDataList()
})
})
onShow(() => {
})
</script>
<style scoped lang="scss">
.container {
padding: 32rpx;
background-color: #F6F8FA;
padding-bottom: 160rpx;
// ::v-deep .uv-input{
// border: unset;
// }
.title {
// font-size: 28rpx;
font-weight: bold;
// display: flex;
// align-items: center;
// margin-left: 20rpx;
// margin-bottom: 20rpx;
color: #333333;
padding: 20rpx 0;
}
.formContent {
background-color: white;
border-radius: 20rpx;
padding: 0 20rpx;
}
.matchCar{
width: 120rpx;
height: 120rpx;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background-color: #3c9cff;
border-radius: 10rpx;
color: white;
margin-left: 20rpx;
}
.carPickerText{
padding: 16rpx 18rpx;
padding-left: 20rpx;
color: #c0c4cc;
border: #dadbde 1px solid;
border-radius: 8rpx;
width: 100%;
}
.priceBtn{
position: fixed;
width: 100vw;
padding: 16rpx 32rpx;
bottom: 0;
left: 0;
z-index: 99;
display: flex;
align-items: center;
justify-content: space-between;
background-color: white;
box-sizing: border-box;
.price {
font-size: 42rpx;
color: #D55A66;
}
::v-deep .uv-button--large{
padding: 0 60rpx !important;
}
}
}
</style>

View File

@@ -0,0 +1,190 @@
<template>
<view class="container">
<uv-form labelPosition="top" style="margin-bottom: 20rpx;" labelWidth="100" :model="item" :rules="deliveryRules" :ref="(el) => el && (deliveryFormRef[index] = el)" v-for="(item, index) in listData" :key="index">
<view class="formContent">
<uv-form-item label="订单编号:" prop="sapNo">
<uv-input placeholder="请输入内容" v-model="item.sapNo" clearable />
</uv-form-item>
<uv-form-item label="项目描述:" prop="xmMs">
<uv-input placeholder="请输入内容" v-model="item.xmMs" clearable />
</uv-form-item>
<uv-form-item label="项目号:" prop="xmNo">
<uv-input placeholder="请输入内容" v-model="item.xmNo" clearable />
</uv-form-item>
<uv-form-item label="物料号:" prop="wlNo">
<uv-input placeholder="请输入内容" v-model="item.wlNo" clearable />
</uv-form-item>
<uv-form-item label="物料描述:" prop="wlMs">
<uv-input placeholder="请输入内容" v-model="item.wlMs" clearable />
</uv-form-item>
<uv-form-item label="配送数量:" prop="realQty">
<uv-input placeholder="请输入内容" type="digit" v-model="item.realQty" clearable />
</uv-form-item>
<uv-form-item label="单位" prop="dw">
<uv-input placeholder="请输入内容" v-model="item.dw" clearable />
</uv-form-item>
<uv-form-item label="供应商名称:" prop="gysMc">
<uv-input placeholder="请输入内容" v-model="item.gysMc" clearable />
</uv-form-item>
<uv-form-item label="备注:" prop="remark">
<uv-input placeholder="请输入内容" v-model="item.remark" clearable />
</uv-form-item>
<template v-if="index !== 0">
<view style="display: flex; justify-content: flex-end;">
<uv-button type="error" text="删除" style="width: 200rpx;" @tap="removeForm(index)"> </uv-button>
</view>
</template>
</view>
</uv-form>
<view style="display: flex; justify-content: flex-end;margin-right: 20rpx;">
<uv-button type="primary" text="添加" style="width: 200rpx;" @tap="addForm"> </uv-button>
</view>
<view class="btn">
<uv-button type="primary" text="确定" size="large" style="width: 100%;" class="mainBtn" @tap="submitForm">
</uv-button>
</view>
</view>
</template>
<script setup>
import { ref, computed } from "vue";
import { onLoad, onShow, onPageScroll } from "@dcloudio/uni-app";
import { getGoodsDict } from "@/api/index.js";
const listData = ref([
{
sapNo: "",
xmMs: "",
xmNo: "",
wlNo: "",
wlMs: "",
realQty: null,
dw: "",
gysMc: "",
remark: ""
}
])
const deliveryFormRef = ref([])
const deliveryRules = ref({
sapNo: [
{ required: true, message: "请输入订单编号", trigger: "blur" }
],
xmMs: [
{ required: true, message: "请输入项目描述", trigger: "blur" }
],
xmNo: [
{ required: true, message: "请输入项目号", trigger: "blur" }
],
wlNo: [
{ required: true, message: "请输入物料号", trigger: "blur" }
],
wlMs: [
{ required: true, message: "请输入物料描述", trigger: "blur" }
],
realQty: [
{ required: true, type: 'float', message: "请输入配送数量", trigger: "blur" }
],
dw: [
{ required: true, message: "请输入单位", trigger: "blur" }
],
gysMc: [
{ required: true, message: "请输入供应商名称", trigger: "blur" }
],
})
const removeForm = (index) => {
listData.value.splice(index, 1)
}
const addForm = () => {
listData.value.push({
sapNo: "",
xmMs: "",
xmNo: "",
wlNo: "",
wlMs: "",
realQty: "",
dw: "",
gysMc: "",
remark: ""
})
}
const submitForm = async () => {
try {
// 遍历所有表单实例,逐个校验
for (const formRef of deliveryFormRef.value) {
if (formRef) {
// 等待单个表单校验完成,失败则抛出异常
await formRef.validate();
}
}
// 所有表单校验通过后的逻辑
uni.$uv.toast('校验通过,准备提交');
console.log("提交数据:", listData.value);
// 这里写你的提交逻辑
let arr = listData.value.map(item => {
return {
wlNo: item.wlNo,
qty: item.realQty,
}
})
getGoodsDict({ items: arr }).then(res => {
console.log(res)
uni.navigateTo({
url: '/pages/index/addDeliveryInfo',
success: function(res1) {
// 通过eventChannel向被打开页面传送数据
res1.eventChannel.emit('acceptData', { data: {detailList: listData.value}, totalWeightKg: res.data.totalWeightKg, goodsSize: res.data.totalVolumeM3 })
}
})
})
} catch (errors) {
// 校验失败的处理
console.log("校验失败:", errors);
uni.$uv.toast('请完善必填项后提交');
}
}
onLoad(() => {
})
</script>
<style scoped lang="scss">
.container {
padding: 32rpx;
background-color: #F6F8FA;
padding-bottom: 160rpx;
.formContent {
background-color: white;
border-radius: 20rpx;
padding: 0 20rpx 20rpx;
}
.btn {
position: fixed;
width: calc(100vw - 64rpx);
bottom: 0;
left: 32rpx;
z-index: 99;
display: flex;
align-items: center;
justify-content: space-between;
}
}
::v-deep uni-toast {
z-index: 30000 !important;
}
</style>

View File

@@ -0,0 +1,682 @@
<template>
<view class="page" :style="formModel.scene == 'COMPLETE' ? '' : 'padding-bottom: 160rpx;'">
<uv-form labelPosition="left" :model="formModel" :rules="deliveryRules" ref="deliveryFormRef">
<!-- 标题 -->
<view class="title">
国网唐山供电公司贾庵子仓库货物配送单
</view>
<!-- 副标题/说明 -->
<!-- <view class="subtitle">
现场仓库接物配送单
</view> -->
<!-- 表单整体 -->
<view class="form-box">
<!-- 第一行配变项目名称项目编号 -->
<view class="row">
<view class="cell">
配送项目名称{{ goodsData?.[0]?.xmMs }}
</view>
<view class="cell">
项目编号{{ goodsData?.[0]?.xmNo }}
</view>
</view>
<!-- 配送地址配送日期 -->
<view class="row">
<view class="cell">
配送地址{{ goodsData?.[0]?.originName }} {{ goodsData?.[0]?.destName }}
</view>
<view class="cell">
配送日期{{ goodsData?.[0]?.deliveryDate }}
</view>
</view>
<!-- 配送车号司机签名 -->
<view class="row">
<view class="cell">
<uv-form-item label="配送车牌:" prop="plateNo" v-if="formModel.scene == 'ORIGIN'">
<uv-input border="bottom" v-model="formModel.plateNo" />
</uv-form-item>
<text v-else> 配送车号{{ formModel.plateNo }}</text>
</view>
<view class="cell">
<uv-form-item label="司机签名:" prop="SIGN_DRIVER">
<uv-button type="warning" size="small" text="去签名" v-if="formModel.scene == 'ORIGIN' && !formModel.SIGN_DRIVER.length" @tap="goSign('SIGN_DRIVER')"></uv-button>
<uv-upload :fileList="formModel.SIGN_DRIVER" v-else :maxCount="1" @delete="deleteImg(index, 'SIGN_DRIVER')" :deletable="formModel.scene == 'ORIGIN'" :previewFullImage="true"></uv-upload>
</uv-form-item>
<uv-form-item label="联系方式:" prop="driverPhone" v-if="formModel.scene == 'ORIGIN'">
<uv-input border="bottom" maxlength="11" type="number" v-model="formModel.driverPhone" />
</uv-form-item>
<view v-else>联系方式{{ formModel.driverPhone }}</view>
</view>
</view>
<!-- 接收单位配送吨数 -->
<view class="row">
<view class="cell">
接收单位{{ goodsData?.[0]?.receiverOrgName }}
</view>
<view class="cell">
配送吨数{{ goodsData?.[0]?.deliveryTon }}
</view>
</view>
<!-- 发货人联系方式 -->
<view class="row">
<view class="cell">
发货人/联系方式
<view>{{ goodsData?.[0]?.shipperName }}/{{ goodsData?.[0]?.shipperPhone }}</view>
</view>
<view class="cell">
制单{{ goodsData?.[0]?.makerUserName }}
</view>
</view>
<!-- 接收人联系方式 -->
<view class="row">
<view class="cell">
接收人/联系方式
<view>{{ goodsData?.[0]?.receiverName }}/{{ goodsData?.[0]?.receiverPhone }}</view>
</view>
<view class="cell">
<uv-form-item label="配送人签名:" prop="SIGN_COURIER">
<uv-button type="warning" text="去签名" size="small" v-if="formModel.scene == 'ORIGIN' && !formModel.SIGN_COURIER.length" @tap="goSign('SIGN_COURIER')"></uv-button>
<uv-upload :fileList="formModel.SIGN_COURIER" v-else :maxCount="1" @delete="deleteImg(index, 'SIGN_COURIER')" :deletable="formModel.scene == 'ORIGIN'" :previewFullImage="true"></uv-upload>
</uv-form-item>
</view>
</view>
<!-- ======= 可左右滑动的明细表格 ======= -->
<view class="table-scroll">
<view class="table">
<!-- 表头 -->
<view class="table-header">
<view class="th col-seq">序号</view>
<view class="th col-wlno">物料编码</view>
<view class="th col-wlname">物资名称</view>
<view class="th col-qty">数量(单位)</view>
<view class="th col-order">订单号</view>
<view class="th col-supplier">供应商名称</view>
<view class="th col-remark">备注</view>
</view>
<!-- 表体 -->
<view class="table-body">
<view class="tr" v-for="(item, index) in goodsData" :key="index">
<view class="td col-seq">{{ index + 1 }}</view>
<view class="td col-wlno">{{ item.wlNo }}</view>
<view class="td col-wlname">{{ item.wlMs }}</view>
<view class="td col-qty">{{ item.realQty }}({{ item.dw }})</view>
<view class="td col-order">{{ item.sapNo }}</view>
<view class="td col-supplier">{{ item.gysMc }}</view>
<view class="td col-remark">{{ item.remark }}</view>
</view>
</view>
</view>
</view>
<!-- ======= 明细表格结束 ======= -->
<!-- 收货确认 -->
<view class="row">
<view class="cell">
<uv-form-item label="接收物资状态:" prop="receiveStatus">
<view style="display: inline-block;">
<uv-radio-group v-model="formModel.receiveStatus">
<uv-radio :name="1" label="数量齐全、状态完好" shape="square" :disabled="formModel.scene !== 'DEST'"></uv-radio>
<span style="vertical-align: middle; font-size: 26rpx;line-height: 14px;">&nbsp;/&nbsp;</span>
<uv-radio :name="2" label="存在问题" shape="square" :disabled="formModel.scene !== 'DEST'"></uv-radio>
</uv-radio-group>
</view>
</uv-form-item>
<uv-form-item label="" v-if="formModel.scene == 'DEST'" prop="receiveProblem">
<uv-input border="bottom" v-model="formModel.receiveProblem" />
</uv-form-item>
<view v-else>{{ formModel.receiveProblem }}</view>
</view>
</view>
<view class="row" style="justify-content: space-between;">
<view class="cell" style="border-right: unset;">
<uv-form-item label="接收人签名:" prop="SIGN_RECEIVER">
<uv-button type="warning" text="去签名" size="small" v-if="formModel.scene == 'DEST' && !formModel.SIGN_RECEIVER.length" @tap="goSign('SIGN_RECEIVER')"></uv-button>
<uv-upload :fileList="formModel.SIGN_RECEIVER" v-else-if="formModel.SIGN_RECEIVER.length" :maxCount="1" :deletable="formModel.scene == 'DEST'" @delete="deleteImg(index, 'SIGN_RECEIVER')" :previewFullImage="true"></uv-upload>
<view v-else></view>
</uv-form-item>
</view>
<view class="cell">
日期{{ goodsData?.[0]?.deliveryDate.split("-")[0] }}{{ goodsData?.[0]?.deliveryDate.split("-")[1] }}{{ goodsData?.[0]?.deliveryDate.split("-")[2] }}
</view>
</view>
<!-- <view class="row">
<view class="cell">
<uv-form-item label="日期:" prop="SIGN_RECEIVER">
<uv-input border="bottom" type="number" style="width: 120rpx;flex: unset;" readonly="formModel.scene !== 'DEST'" v-model="formModel.receiveYear" />
<uv-input border="bottom" type="number" style="width: 80rpx;flex: unset;" :readonly="formModel.scene !== 'DEST'" v-model="formModel.receiveMonth" />
<uv-input border="bottom" type="number" style="width: 80rpx;flex: unset;" :readonly="formModel.scene !== 'DEST'" v-model="formModel.receiveDay" />
</uv-form-item>
</view>
</view> -->
</view>
<uv-form-item label="开始配送现场照片" prop="ORIGIN_PHOTO_SITE" style="margin-top: 16rpx;" class="form-item">
<uv-upload
:fileList="formModel.ORIGIN_PHOTO_SITE"
name="ORIGIN_PHOTO_SITE"
:maxCount="formModel.scene == 'ORIGIN' ? 10 : formModel.ORIGIN_PHOTO_SITE.length"
:deletable="formModel.scene == 'ORIGIN'"
@afterRead="afterRead($event, 'ORIGIN_PHOTO_SITE')"
@delete="deleteImg(index, 'ORIGIN_PHOTO_SITE')"
></uv-upload>
</uv-form-item>
<uv-form-item label="单据照片" prop="PHOTO_BILL">
<uv-upload
:fileList="formModel.PHOTO_BILL"
name="PHOTO_BILL"
:maxCount="1"
:deletable="formModel.scene == 'ORIGIN'"
@afterRead="afterRead($event, 'PHOTO_BILL')"
@delete="deleteImg(index, 'PHOTO_BILL')"
></uv-upload>
</uv-form-item>
<uv-form-item label="完成配送现场照片" prop="DEST_PHOTO_SITE" v-show="formModel.scene !== 'ORIGIN'" class="form-item">
<uv-upload
:fileList="formModel.DEST_PHOTO_SITE"
:deletable="formModel.scene == 'DEST'"
name="DEST_PHOTO_SITE"
:maxCount="formModel.scene == 'DEST' ? 10 : formModel.DEST_PHOTO_SITE.length"
@afterRead="afterRead($event, 'DEST_PHOTO_SITE')"
@delete="deleteImg(index, 'DEST_PHOTO_SITE')"
></uv-upload>
</uv-form-item>
</uv-form>
<view class="btn" v-show="formModel.scene !== 'COMPLETE'">
<uv-button type="primary" text="确定" size="large" style="width: 100%;" class="mainBtn" @tap="startForm">{{ formModel.scene == 'ORIGIN' ? ' ' : ' '}}</uv-button>
</view>
</view>
</template>
<script setup>
import { ref, onUnmounted } from 'vue'
import { onLoad } from "@dcloudio/uni-app"
import { getOrderDetail, uploadDeliveryAttachment, startOrder } from "@/api/index"
const form = ref({
// projectName: '冀北唐山路南区10kV广鑫环网柜等6座环网柜配电自动化改造工程',
// projectNo: '1801032401LD',
// address: '唐山市路南区女织寨赵田庄村民源电气',
// date: '2025年12月2日',
// carNo: '冀B1947L',
// receiveCompany: '路南供电中心',
// senderName: '林生',
// senderPhone: '15531558587',
// receiverName: '王小强',
// receiverPhone: '18733306661',
// driverSign: '',
// deliveryWeight: '',
// maker: '孟利红',
// checker: '',
// // 明细 items字段**齐全**:物料编码、名称、数量、单位、订单号、供应商名称、备注
// items: [
// {
// wlNo: '500138342',
// wlName: '二次融合成套环网箱AC10kV, 630A固体二进四出',
// qty: 1,
// unit: '套',
// orderNo: '101378434',
// supplierName: '宁波奥克斯智能科技股份有限公司',
// remark: 'ID: 169736945'
// },
// {
// wlNo: '500138342',
// wlName: '二次融合成套环网箱AC10kV, 630A固体二进四出',
// qty: 1,
// unit: '套',
// orderNo: '101378434',
// supplierName: '宁波奥克斯智能科技股份有限公司',
// remark: 'ID: 169736952'
// }
// ]
})
const formModel = ref({
orderNo: null,
scene: '',
lng: '',
lat: '',
SIGN_DRIVER: [],
SIGN_COURIER: [],
ORIGIN_PHOTO_SITE: [],
PHOTO_BILL: [],
DEST_PHOTO_SITE: [],
SIGN_RECEIVER: [],
receiveStatus: '',
receiveProblem: '',
// SIGN_DRIVER: [{"url":
// "http://192.168.1.5:8087/delivery/2025-12-03/origin/sign_driver/8f5253492a63469a90406fd9c1d702b8.png"}
// ],
})
const deliveryFormRef = ref()
const deliveryRules = ref({
'plateNo': [
{
type: 'string',
required: true,
message: '请输入配送车牌',
trigger: ['blur', 'change']
},
{
validator: (rule, value, callback) => {
// 自定义校验逻辑
return uni.$uv.test.carNo(value)
},
message: '请输入正确的车牌',
trigger: ['blur', 'change']
}
],
'driverPhone': [
{
type: 'string',
required: true,
message: '请输入司机电话',
trigger: ['blur', 'change']
},
{
validator: (rule, value, callback) => {
// 自定义校验逻辑
return uni.$uv.test.mobile(value)
},
message: '请输入正确的手机号',
trigger: ['blur', 'change']
}
],
'SIGN_DRIVER': {
type: 'array',
required: true,
message: '请司机签名',
trigger: ['blur', 'change']
},
'SIGN_COURIER': {
type: 'array',
required: true,
message: '请配送人签名',
trigger: ['blur', 'change']
},
'ORIGIN_PHOTO_SITE': {
type: 'array',
required: true,
message: '请上传现场照片',
trigger: ['blur', 'change']
},
'PHOTO_BILL': {
type: 'array',
required: true,
message: '请上传单据照片',
trigger: ['blur', 'change']
},
})
const bizTypeMap = {
'ORIGIN_PHOTO_SITE': { scene: 'ORIGIN', bizType: 'PHOTO_SITE' },
'DEST_PHOTO_SITE': { scene: 'DEST', bizType: 'PHOTO_SITE' },
'SIGN_DRIVER': { scene: 'ORIGIN', bizType: 'SIGN_DRIVER' },
'SIGN_COURIER': { scene: 'ORIGIN', bizType: 'SIGN_COURIER' },
'PHOTO_BILL': { scene: 'ORIGIN', bizType: 'PHOTO_BILL' },
'SIGN_RECEIVER': { scene: 'DEST', bizType: 'SIGN_RECEIVER' }
};
// 遍历form中的数组字段
// Object.keys(form).forEach(key => {
// if (Array.isArray(form[key]) && bizTypeMap[key]) {
// const matchRule = bizTypeMap[key];
// // 筛选符合条件的元素并添加
// const matchedItems = arr.filter(item => {
// return item.bizType === matchRule.bizType &&
// (!matchRule.scene || item.scene === matchRule.scene);
// });
// form[key].push(...matchedItems);
// }
// });
const goodsData = ref([])
const getDetail = () => {
getOrderDetail({ orderNo: formModel.value.orderNo }).then(res => {
goodsData.value = res.data
formModel.value.plateNo = res.data[0].plateNo
formModel.value.driverPhone = res.data[0].driverPhone
formModel.value.receiveStatus = res.data[0].receiveStatus
formModel.value.receiveProblem = res.data[0].receiveProblem
// res.data[0].attachments.forEach(item => {
// if (formModel.value.hasOwnProperty(item.bizType) && Array.isArray(formModel.value[item.bizType])) {
// formModel.value[item.bizType].push(item);
// }
// if (item.bizType === 'PHOTO_SITE') {
// const targetKey = item.scene === 'ORIGIN' ? 'ORIGIN_PHOTO_SITE' : 'DEST_PHOTO_SITE';
// if (Array.isArray(formModel.value[targetKey])) {
// formModel.value[targetKey].push(item);
// }
// }
// })
Object.keys(formModel.value).forEach(key => {
if (Array.isArray(formModel.value[key]) && bizTypeMap[key]) {
const matchRule = bizTypeMap[key];
// 筛选符合条件的元素并添加
const matchedItems = res.data[0].attachments.filter(item => {
return item.bizType === matchRule.bizType &&
(!matchRule.scene || item.scene === matchRule.scene);
});
formModel.value[key].push(...matchedItems);
}
});
console.log(formModel.value)
})
}
// 去签名页面
const goSign = (type) => {
// 移除之前的事件监听器避免重复
uni.$off('getSignImg')
uni.navigateTo({
url: '/pages/index/signature?bizType=' + type + '&scene=' + formModel.value.scene,
success: () => {
// 监听签名返回事件
const eventHandler = (e) => {
console.log('getSignImg', e)
// signBase64.value = e.base64
// signTempimg.value = e.path
formModel.value[type] = [{url: e.url[0], scene: formModel.value.scene, bizType: type}]
}
// 监听一次自定义事件
uni.$once('getSignImg', eventHandler)
// 页面卸载时移除监听(避免内存泄漏)
// uni.$off('getSignImg', eventHandler)
}
})
}
// 上传照片
const afterRead = async (event, type) => {
// 当设置 multiple 为 true 时, file 为数组格式,否则为对象格式
console.log(event)
let files = event.file.url
let formData = {
scene: formModel.value.scene,
bizType: type
}
console.log(files)
uploadDeliveryAttachment(files, formData).then(res => {
console.log(res)
if (!formModel.value[type] || formModel.value[type].length == 0) {
formModel.value[type] = []
}
res.data.forEach(item => {
formModel.value[type].push({url: item, scene: formModel.value.scene, bizType: type})
})
})
}
// 删除图片
const deleteImg = (index, type) => {
formModel.value[type].splice(index, 1)
}
onLoad((options) => {
formModel.value.orderNo = options.orderNo
formModel.value.scene = options.type
formModel.value.billNoCk = options.billNoCk
if (formModel.value.scene == "DEST") {
deliveryRules.value.receiveStatus = {
type: 'number',
required: true,
validator: (rule, value, callback) => {
if (value > 0) {
callback()
} else {
callback(new Error('请选择接收状态'))
}
},
message: '请选择接收状态',
trigger: ['blur', 'change']
}
deliveryRules.value.receiveProblem = {
type: 'string',
validator: (rule, value, callback) => {
if (formModel.value.receiveStatus == 2 && value.length == 0) {
callback(new Error('请描述存在问题'))
} else {
callback()
}
},
message: '请描述存在问题',
trigger: ['blur', 'change']
}
deliveryRules.value.SIGN_RECEIVER = {
type: 'array',
required: true,
message: '请接收人签名',
trigger: ['blur', 'change']
}
deliveryRules.value.DEST_PHOTO_SITE = {
type: 'array',
required: true,
message: '请上传现场照片',
trigger: ['blur', 'change']
}
}
getDetail()
})
const startForm = () => {
deliveryFormRef.value.validate().then(res => {
console.log("验证通过")
uni.showLoading({
title: '正在编辑表单中...'
})
uni.getLocation({
type: 'gcj02', // 高德地图使用gcj02坐标系
success: (res) => {
console.log(res)
// uni.$uv.toast('获取当前位置成功')
formModel.value.lng = res.longitude
formModel.value.lat = res.latitude
let attachments = []
for (const key in bizTypeMap) {
if (bizTypeMap[key].scene === formModel.value.scene) {
const formattedData = formModel.value[key].map(item => ({
...item,
bizType: bizTypeMap[key].bizType,
}))
attachments.push(...formattedData);
}
}
let params = {
...formModel.value,
attachments: attachments,
}
console.log(params)
startOrder(params).then(res => {
console.log(res)
uni.$uv.toast('开始配送')
uni.navigateBack({ delta: 1 })
uni.hideLoading()
})
},
fail: (err) => {
reject(err);
}
});
}).catch(errors => {
})
}
// 组件卸载时移除事件监听器
onUnmounted(() => {
console.log("协助")
uni.$off('getSignImg')
})
</script>
<style scoped lang="scss">
.page {
padding: 20rpx;
background-color: #fff;
min-height: calc(100vh - 192rpx);
}
/* 标题 */
.title {
text-align: center;
font-size: 38rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.subtitle {
text-align: center;
font-size: 30rpx;
margin-bottom: 30rpx;
}
/* 外框 */
.form-box {
border: 2rpx solid #000;
padding: 20rpx;
font-size: 26rpx;
}
/* 每一行 */
.row {
display: flex;
border-bottom: 2rpx solid #000;
}
.row:last-child {
border-bottom: 0;
}
:deep(.uv-form-item__body) {
padding: 0 !important;
}
:deep(.uv-border-bottom) {
border-color: #000 !important;
padding: 0 !important;
border-bottom-width: 2rpx !important;
}
:deep(.uv-form-item__body__left) {
width: unset !important;
}
:deep(.uv-form-item__body__left__content__label) {
font-size: 26rpx !important;
color: #000 !important;
}
:deep(.uv-form-item__body__right__message) {
margin-left: 0 !important;
}
:deep(.form-item .uv-form-item__body__left) {
width: 120rpx !important;
}
:deep(.uv-radio__label-wrap text) {
font-size: 26rpx !important;
color: #000 !important;
}
:deep(.uv-radio__icon-wrap--square) {
width: 30rpx !important;
height: 30rpx !important;
}
.cell {
flex: 1;
padding: 10rpx;
border-right: 2rpx solid #000;
}
.cell:last-child {
border-right: 0;
}
/* ====== 明细表格滚动区域 ====== */
.table-scroll {
margin-top: 20rpx;
border-top: 2rpx solid #000;
overflow-x: auto;
}
/* 给表格一个最小宽度,超出屏幕时就可以左右拖动 */
.table {
/* min-width: 2800rpx; */
width: 1700rpx;
}
/* 表头 */
.table-header {
display: flex;
border-bottom: 2rpx solid #000;
background-color: #f5f5f5;
}
.th {
padding: 12rpx;
border-right: 2rpx solid #000;
font-weight: bold;
box-sizing: border-box;
}
.th:last-child {
border-right: 0;
}
/* 表体 */
.table-body .tr {
display: flex;
border-bottom: 2rpx solid #000;
box-sizing: border-box;
}
.td {
padding: 12rpx;
border-right: 2rpx solid #000;
box-sizing: border-box;
}
.td:last-child {
border-right: 0;
}
.col-seq {
width: 80rpx;
}
.col-wlno {
width: 180rpx;
}
.col-wlname {
width: 500rpx;
}
.col-qty {
width: 210rpx;
}
.col-order {
width: 180rpx;
}
.col-supplier {
width: 400rpx;
}
.col-remark {
width: 150rpx;
}
.btn {
position: fixed;
width: calc(100vw - 64rpx);
bottom: 0;
left: 32rpx;
z-index: 99;
// display: flex;
// align-items: center;
// justify-content: space-between;
}
</style>

View File

@@ -0,0 +1,307 @@
<template>
<view class="container">
<uv-form labelPosition="top" labelWidth="100" :model="formModel" :rules="deliveryRules" ref="deliveryFormRef">
<view class="formContent">
<view class="title">{{ formModel.scene == 'ORIGIN' ? '开始' : '完成' }}配送信息</view>
<uv-line color="#a8a8a8"></uv-line>
<uv-form-item label="司机姓名" prop="driverName" v-if="formModel.scene == 'ORIGIN'">
<uv-input placeholder="请输入内容" v-model="formModel.driverName" clearable />
</uv-form-item>
<uv-form-item label="配送车牌" prop="plateNo" v-if="formModel.scene == 'ORIGIN'">
<uv-input placeholder="请输入内容" v-model="formModel.plateNo" clearable />
</uv-form-item>
<uv-form-item label="司机电话" prop="driverPhone" v-if="formModel.scene == 'ORIGIN'">
<uv-input placeholder="请输入内容" maxlength="11" type="number" v-model="formModel.driverPhone" clearable />
</uv-form-item>
<uv-form-item label="位置信息" prop="lng">
<view class="carPickerText" :style="formModel.lng ? 'color: #303133;' : ''" @tap="getCurrentLocation">{{ formModel.lng ? '获取成功' : '请点击获取当前位置'}}</view>
</uv-form-item>
<uv-form-item label="司机签名" prop="SIGN_DRIVER" v-if="formModel.scene == 'ORIGIN'">
<uv-button type="warning" text="去签名" v-if="!formModel.SIGN_DRIVER" @tap="goSign('SIGN_DRIVER')"></uv-button>
<uv-image height="80" mode="heightFix" v-if="formModel.SIGN_DRIVER" @tap="previewImage(formModel.SIGN_DRIVER[0].url)" :src="formModel.SIGN_DRIVER[0].url"></uv-image>
</uv-form-item>
<uv-form-item label="配送人签名" prop="SIGN_COURIER" v-if="formModel.scene == 'ORIGIN'">
<uv-button type="warning" text="去签名" v-if="!formModel.SIGN_COURIER" @tap="goSign('SIGN_COURIER')"></uv-button>
<uv-image height="80" mode="heightFix" v-if="formModel.SIGN_COURIER" @tap="previewImage(formModel.SIGN_COURIER[0].url)" :src="formModel.SIGN_COURIER[0].url"></uv-image>
</uv-form-item>
<uv-form-item label="接收人签名" prop="SIGN_RECEIVER" v-if="formModel.scene == 'DEST'">
<uv-button type="warning" text="去签名" v-if="!formModel.SIGN_RECEIVER" @tap="goSign('SIGN_RECEIVER')"></uv-button>
<uv-image height="80" mode="heightFix" v-if="formModel.SIGN_RECEIVER" @tap="previewImage(formModel.SIGN_RECEIVER[0].url)" :src="formModel.SIGN_RECEIVER[0].url"></uv-image>
</uv-form-item>
<uv-form-item label="现场照片" prop="PHOTO_SITE">
<uv-upload
:fileList="formModel.PHOTO_SITE"
name="PHOTO_SITE"
:maxCount="10"
@afterRead="afterRead($event, 'PHOTO_SITE')"
@delete="deleteImg(index, 'PHOTO_SITE')"
></uv-upload>
</uv-form-item>
<uv-form-item label="单据照片" prop="PHOTO_BILL" v-if="formModel.scene == 'ORIGIN'">
<uv-upload
:fileList="formModel.PHOTO_BILL"
name="PHOTO_BILL"
:maxCount="1"
@afterRead="afterRead($event, 'PHOTO_BILL')"
@delete="deleteImg(index, 'PHOTO_BILL')"
></uv-upload>
</uv-form-item>
</view>
</uv-form>
<view class="btn">
<uv-button type="primary" text="确定" size="large" style="width: 100%;" class="mainBtn" @tap="startForm">{{ formModel.scene == 'ORIGIN' ? ' ' : ' '}}</uv-button>
</view>
</view>
</template>
<script setup>
import { ref, onUnmounted, getCurrentInstance } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { uploadDeliveryAttachment, startOrder } from "@/api/index"
const deliveryFormRef = ref()
const formModel = ref({
orderNo: null,
scene: '',
lng: '',
lat: '',
})
const deliveryRules = ref({
'driverName': {
type: 'string',
required: true,
message: '请输入司机姓名',
trigger: ['blur', 'change']
},
'plateNo': [
{
type: 'string',
required: true,
message: '请输入配送车牌',
trigger: ['blur', 'change']
},
{
validator: (rule, value, callback) => {
// 自定义校验逻辑
return uni.$uv.test.carNo(value)
},
message: '请输入正确的车牌',
trigger: ['blur', 'change']
}
],
'driverPhone': [
{
type: 'string',
required: true,
message: '请输入司机电话',
trigger: ['blur', 'change']
},
{
validator: (rule, value, callback) => {
// 自定义校验逻辑
return uni.$uv.test.mobile(value)
},
message: '请输入正确的手机号',
trigger: ['blur', 'change']
}
],
'lng': {
type: 'number',
required: true,
message: '请点击获取当前位置',
trigger: ['blur', 'change']
},
'SIGN_DRIVER': {
type: 'array',
required: true,
message: '请司机签名',
trigger: ['blur', 'change']
},
'SIGN_COURIER': {
type: 'array',
required: true,
message: '请配送人签名',
trigger: ['blur', 'change']
},
'SIGN_RECEIVER': {
type: 'array',
required: true,
message: '请接收人签名',
trigger: ['blur', 'change']
},
'PHOTO_SITE': {
type: 'array',
required: true,
message: '请上传现场照片',
trigger: ['blur', 'change']
},
'PHOTO_BILL': {
type: 'array',
required: true,
message: '请上传单据照片',
trigger: ['blur', 'change']
}
})
const getCurrentLocation = () => {
if (formModel.value.lng) {
return
}
uni.showLoading({
title: '获取当前位置中...'
})
uni.getLocation({
type: 'gcj02', // 高德地图使用gcj02坐标系
success: (res) => {
console.log(res)
uni.$uv.toast('获取当前位置成功')
formModel.value.lng = res.longitude
formModel.value.lat = res.latitude
deliveryFormRef.value.validateField("lng")
uni.hideLoading()
},
fail: (err) => {
reject(err);
}
});
}
// 去签名页面
const goSign = (type) => {
// 移除之前的事件监听器避免重复
uni.$off('getSignImg')
uni.navigateTo({
url: '/pages/index/signature?bizType=' + type + '&scene=' + formModel.value.scene,
success: () => {
// 监听签名返回事件
const eventHandler = (e) => {
console.log('getSignImg', e)
// signBase64.value = e.base64
// signTempimg.value = e.path
formModel.value[type] = [{url: e.url[0], scene: formModel.value.scene, bizType: type}]
}
// 监听一次自定义事件
uni.$once('getSignImg', eventHandler)
// 页面卸载时移除监听(避免内存泄漏)
// uni.$off('getSignImg', eventHandler)
}
})
}
// 预览
const previewImage = (img) => {
let arr = []
arr.push(img)
uni.previewImage({
urls: arr,
})
}
// 上传照片
const afterRead = async (event, type) => {
// 当设置 multiple 为 true 时, file 为数组格式,否则为对象格式
console.log(event)
let files = event.file.url
let formData = {
scene: formModel.value.scene,
bizType: type
}
console.log(files)
uploadDeliveryAttachment(files, formData).then(res => {
console.log(res)
if (!formModel.value[type] || formModel.value[type].length == 0) {
formModel.value[type] = []
}
res.data.forEach(item => {
formModel.value[type].push({url: item, scene: formModel.value.scene, bizType: type})
})
})
}
// 删除图片
const deleteImg = (index, type) => {
formModel.value[type].splice(index, 1)
}
const startForm = () => {
deliveryFormRef.value.validate().then(res => {
let attachments = []
if (formModel.value.scene == "ORIGIN") {
attachments = [...formModel.value.SIGN_DRIVER, ...formModel.value.SIGN_COURIER, ...formModel.value.PHOTO_SITE, ...formModel.value.PHOTO_BILL]
} else {
attachments = [...formModel.value.SIGN_RECEIVER, ...formModel.value.PHOTO_SITE]
}
let params = {
...formModel.value,
attachments: attachments,
}
console.log(params)
startOrder(params).then(res => {
console.log(res)
uni.$uv.toast('开始配送')
uni.navigateBack({ delta: 1 })
})
}).catch(errors => {
})
}
onLoad((options) => {
formModel.value.orderNo = options.orderNo
formModel.value.scene = options.type
formModel.value.billNoCk = options.billNoCk
})
onShow(() => {
})
// 组件卸载时移除事件监听器
onUnmounted(() => {
console.log("协助")
uni.$off('getSignImg')
})
</script>
<style scoped lang="scss">
.container {
padding: 32rpx;
background-color: #F6F8FA;
min-height: calc(100vh - 192rpx);
padding-bottom: 160rpx;
.title {
font-weight: bold;
color: #333333;
padding: 20rpx 0;
}
.formContent {
background-color: white;
border-radius: 20rpx;
padding: 0 20rpx;
}
.carPickerText{
padding: 16rpx 18rpx;
padding-left: 20rpx;
color: #c0c4cc;
border: #dadbde 1px solid;
border-radius: 8rpx;
width: 100%;
}
.btn {
position: fixed;
width: calc(100vw - 64rpx);
bottom: 0;
left: 32rpx;
z-index: 99;
display: flex;
align-items: center;
justify-content: space-between;
}
}
</style>

196
pages/index/index.js Normal file
View File

@@ -0,0 +1,196 @@
function getLocalFilePath(path) {
if (path.indexOf('_www') === 0 || path.indexOf('_doc') === 0 || path.indexOf('_documents') === 0 || path.indexOf('_downloads') === 0) {
return path
}
if (path.indexOf('file://') === 0) {
return path
}
if (path.indexOf('/storage/emulated/0/') === 0) {
return path
}
if (path.indexOf('/') === 0) {
var localFilePath = plus.io.convertAbsoluteFileSystem(path)
if (localFilePath !== path) {
return localFilePath
} else {
path = path.substr(1)
}
}
return '_www/' + path
}
function dataUrlToBase64(str) {
var array = str.split(',')
return array[array.length - 1]
}
var index = 0
function getNewFileId() {
return Date.now() + String(index++)
}
function biggerThan(v1, v2) {
var v1Array = v1.split('.')
var v2Array = v2.split('.')
var update = false
for (var index = 0; index < v2Array.length; index++) {
var diff = v1Array[index] - v2Array[index]
if (diff !== 0) {
update = diff > 0
break
}
}
return update
}
export function pathToBase64(path) {
return new Promise(function(resolve, reject) {
if (typeof window === 'object' && 'document' in window) {
if (typeof FileReader === 'function') {
var xhr = new XMLHttpRequest()
xhr.open('GET', path, true)
xhr.responseType = 'blob'
xhr.onload = function() {
if (this.status === 200) {
let fileReader = new FileReader()
fileReader.onload = function(e) {
resolve(e.target.result)
}
fileReader.onerror = reject
fileReader.readAsDataURL(this.response)
}
}
xhr.onerror = reject
xhr.send()
return
}
var canvas = document.createElement('canvas')
var c2x = canvas.getContext('2d')
var img = new Image
img.onload = function() {
canvas.width = img.width
canvas.height = img.height
c2x.drawImage(img, 0, 0)
resolve(canvas.toDataURL())
canvas.height = canvas.width = 0
}
img.onerror = reject
img.src = path
return
}
if (typeof plus === 'object') {
plus.io.resolveLocalFileSystemURL(getLocalFilePath(path), function(entry) {
entry.file(function(file) {
var fileReader = new plus.io.FileReader()
fileReader.onload = function(data) {
resolve(data.target.result)
}
fileReader.onerror = function(error) {
reject(error)
}
fileReader.readAsDataURL(file)
}, function(error) {
reject(error)
})
}, function(error) {
reject(error)
})
return
}
if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
wx.getFileSystemManager().readFile({
filePath: path,
encoding: 'base64',
success: function(res) {
resolve('data:image/png;base64,' + res.data)
},
fail: function(error) {
reject(error)
}
})
return
}
reject(new Error('not support'))
})
}
export function base64ToPath(base64) {
return new Promise(function(resolve, reject) {
if (typeof window === 'object' && 'document' in window) {
base64 = base64.split(',')
var type = base64[0].match(/:(.*?);/)[1]
var str = atob(base64[1])
var n = str.length
var array = new Uint8Array(n)
while (n--) {
array[n] = str.charCodeAt(n)
}
return resolve((window.URL || window.webkitURL).createObjectURL(new Blob([array], { type: type })))
}
var extName = base64.split(',')[0].match(/data\:\S+\/(\S+);/)
if (extName) {
extName = extName[1]
} else {
reject(new Error('base64 error'))
}
var fileName = getNewFileId() + '.' + extName
if (typeof plus === 'object') {
var basePath = '_doc'
var dirPath = 'uniapp_temp'
var filePath = basePath + '/' + dirPath + '/' + fileName
if (!biggerThan(plus.os.name === 'Android' ? '1.9.9.80627' : '1.9.9.80472', plus.runtime.innerVersion)) {
plus.io.resolveLocalFileSystemURL(basePath, function(entry) {
entry.getDirectory(dirPath, {
create: true,
exclusive: false,
}, function(entry) {
entry.getFile(fileName, {
create: true,
exclusive: false,
}, function(entry) {
entry.createWriter(function(writer) {
writer.onwrite = function() {
resolve(filePath)
}
writer.onerror = reject
writer.seek(0)
writer.writeAsBinary(dataUrlToBase64(base64))
}, reject)
}, reject)
}, reject)
}, reject)
return
}
var bitmap = new plus.nativeObj.Bitmap(fileName)
bitmap.loadBase64Data(base64, function() {
bitmap.save(filePath, {}, function() {
bitmap.clear()
resolve(filePath)
}, function(error) {
bitmap.clear()
reject(error)
})
}, function(error) {
bitmap.clear()
reject(error)
})
return
}
if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
var filePath = wx.env.USER_DATA_PATH + '/' + fileName
wx.getFileSystemManager().writeFile({
filePath: filePath,
data: dataUrlToBase64(base64),
encoding: 'base64',
success: function() {
resolve(filePath)
},
fail: function(error) {
reject(error)
}
})
return
}
reject(new Error('not support'))
})
}

285
pages/index/index.vue Normal file
View File

@@ -0,0 +1,285 @@
<template>
<view class="container">
<z-paging ref="pagingRef" class="containerBox" v-model="listData" :default-page-size="queryParams.pageSize" @query="queryList">
<template #top>
<view style="padding: 20rpx 32rpx 0;">
<!-- 项目号/项目描述/物料号/物料描述/供应商编码/供应商名称/订单编号 -->
<uv-search v-model="queryParams.keyword" placeholder="请输入" :showAction="true" actionText="搜索" @search="searchList" @custom="searchList"></uv-search>
<uv-tabs :list="list" @click="click" lineColor="#199793"></uv-tabs>
</view>
</template>
<view class="box">
<view v-for="item in listData" class="item">
<view class="title">配送号{{ item.orderNo }}</view>
<view><text>发货人</text>{{ item.shipperName }}</view>
<view><text>发货地</text>{{ item.originName }}</view>
<view><text>接收人</text>{{ item.receiverName }}</view>
<view style="display: flex; align-items: center;">
<view><text>目的地</text>{{ item.destName }}</view>
<image :src="openMap" mode="scaleToFill" v-show="item.orderStatus == 2" style="width: 30rpx; height: 30rpx;margin-left: 20rpx;" @tap="toMap(item, 'location')" />
</view>
<view><text>货物数量</text>{{ item.items.length }}</view>
<view><text>配送日期</text>{{ item.deliveryDate }}</view>
<view class="statusBtn">
<!-- <uv-button :plain="true" shape="circle" text="查看详情" @tap="goDetail(item)" style="margin-right: 20rpx;"></uv-button> -->
<uv-button type="primary" shape="circle" v-show="item.orderStatus == 1" text="开始配送" @tap="changeOrder(item, 'ORIGIN')"></uv-button>
<uv-button type="primary" shape="circle" v-show="item.orderStatus == 2" text="完成配送" @tap="changeOrder(item, 'DEST')"></uv-button>
<uv-button type="primary" shape="circle" v-show="item.orderStatus == 3" text="查看单据" style="margin-right: 20rpx;" @tap="changeOrder(item, 'COMPLETE')"></uv-button>
<!-- <uv-button type="primary" shape="circle" v-show="item.orderStatus == 1" text="开始配送" @tap="change1(item, 'ORIGIN')"></uv-button>
<uv-button type="primary" shape="circle" v-show="item.orderStatus == 2" text="完成配送" @tap="change1(item, 'DEST')"></uv-button> -->
<uv-button type="primary" v-show="item.orderStatus == 3" :plain="true" shape="circle" text="历史轨迹" @tap="toMap(item, 'history')"></uv-button>
</view>
</view>
</view>
</z-paging>
<view class="btn">
<uv-button type="primary" text="确定" size="large" style="width: 100%;" class="mainBtn" @tap="handleAdd"> </uv-button>
</view>
<uv-modal ref="modalRef" title="单据明细" class="modalInfo" confirmText="关闭" confirmColor="#606266"
:closeOnClickOverlay="false">
<view class="box">
<view v-for="items in infoList" class="item">
<view><text>订单编号</text>{{ items.sapNo }}</view>
<view><text>项目描述</text>{{ items.xmMs }}</view>
<view><text>项目号</text>{{ items.xmNo }}</view>
<view><text>物料号</text>{{ items.wlNo }}</view>
<view><text>物料描述</text>{{ items.wlMs }}</view>
<view><text>供应商名称</text>{{ items.gysMc }}</view>
<view><text>存放位置</text>{{ items.pcode || "-" }}</view>
<view><text>身份码</text>{{ items.entityId || "-" }}</view>
<view><text>备注</text>{{ items.remark || '-' }}</view>
</view>
</view>
</uv-modal>
</view>
</template>
<script setup>
import { ref, computed } from "vue";
import { onLoad, onShow, onPageScroll } from "@dcloudio/uni-app";
import { getOrder, getOrderDetail } from "@/api/index"
import openMap from "@/static/openMap.png"
const list = ref([{
name: '待接单',
value: '0',
}, {
name: '待配送',
value: '1',
}, {
name: '配送中',
value: '2',
}, {
name: '已完成',
value: '3',
}])
const click = (item) => {
queryParams.value.orderStatus = item.value
pagingRef.value.reload()
console.log('item', item);
}
const queryParams = ref({
// keyword: "",
orderStatus: 0,
pageNum: 1,
pageSize: 10
})
const pagingRef = ref(null)
const listData = ref([])
// 获取列表
const queryList = (pageNo, pageSize) => {
queryParams.value.pageNum = pageNo
console.log(pageNo, pageSize, queryParams.value)
getOrder(queryParams.value).then(res => {
console.log(res.rows)
pagingRef.value.complete(res.rows)
}).catch(res => {
pagingRef.value.complete(false)
})
}
const searchList = () => {
pagingRef.value.reload()
}
onLoad(() => {
})
onShow(() => {
pagingRef.value?.reload()
})
// 弹窗
const modalRef = ref()
const infoList = ref([])
const goDetail = (item) => {
if (item.infoList && item.infoList.length > 0) {
infoList.value = item.infoList
modalRef.value.open()
return
}
getOrderDetail({ orderNo: item.orderNo }).then(res => {
infoList.value = res.data
item.infoList = res.data
modalRef.value.open()
})
}
// 跳转地图
const toMap = (item, type) => {
if (type === 'location') {
uni.navigateTo({
url: "/pages/index/map?orderNo=" + item.orderNo + "&plateNo=" + item.plateNo + "&type=" + type
})
} else {
uni.navigateTo({
url: "/pages/index/map?orderNo=" + item.orderNo + "&plateNo=" + item.plateNo + "&type=" + type
})
}
}
const changeOrder = (item, type) => {
uni.navigateTo({
url: "/pages/index/deliveryBill?type=" + type + "&orderNo=" + item.orderNo + "&billNoCk=" + item.billNoCk
})
}
const change1 = (item, type) => {
uni.navigateTo({
url: "/pages/index/editDelivery?type=" + type + "&orderNo=" + item.orderNo + "&billNoCk=" + item.billNoCk
})
}
const handleAdd = (item) => {
uni.navigateTo({
url: "/pages/index/addDelivery"
})
}
</script>
<style scoped lang="scss">
.container {
position: relative;
.containerBox{
// padding: 32rpx;
padding-bottom: 120rpx;
min-height: calc(100vh - 196rpx - 120rpx);
::v-deep .uv-tabs__wrapper__nav{
width: 100%;
}
::v-deep .uv-tabs__wrapper__nav__item{
flex: 1;
}
.dropDown{
padding: 0 30rpx;
border-bottom: 1px solid #dadbde;
.item_dropDown{
padding: 20rpx;
display: flex;
align-items: center;
::v-deep .uv-text{
width: unset !important;
flex: unset !important;
}
::v-deep .item_text{
max-width: 200rpx;
margin-right: 10rpx !important;
}
}
}
::v-deep .uv-search {
flex: unset;
}
::v-deep .zp-scroll-view-super {
background-color: #F3F4F8;
}
.box{
// background-color: #ebebeb;
// background-color: #f5f5f5;
// padding-top: 20rpx;
background-color: #F3F4F8;
padding: 0 32rpx;
box-sizing: border-box;
padding-top: 20rpx;
.item {
background: white;
padding: 20rpx;
margin-bottom: 20rpx;
line-height: 50rpx;
font-size: 28rpx;
border-radius: 8rpx;
color: #333;
position: relative;
.title {
font-size: 32rpx;
font-weight: bold;
display: flex;
align-items: center;
}
text{
color: #666;
}
.statusBtn {
display: flex;
justify-content: flex-end;
border-top: 1px solid #c7c7c7;
padding-top: 10rpx;
::v-deep .uv-button{
height: 60rpx !important;
}
}
}
}
}
::v-deep .uv-input__content{
flex-direction: column;
align-items: flex-start;
}
.btn {
position: fixed;
width: calc(100vw - 64rpx);
bottom: 0;
left: 32rpx;
z-index: 99;
display: flex;
align-items: center;
justify-content: space-between;
}
}
.modalInfo {
.box {
width: 100%;
max-height: 60vh;
overflow: auto;
.item {
background: #efefef;
padding: 20rpx;
margin-top: 20rpx;
line-height: 50rpx;
font-size: 28rpx;
border-radius: 8rpx;
color: #333;
border: unset;
}
}
}
::v-deep .zp-paging-container-content{
background-color: #F3F4F8;
}
</style>

439
pages/index/map.vue Normal file
View File

@@ -0,0 +1,439 @@
<template>
<view class="container">
<!-- {{ polyline[0]?.points.length }} - {{ pointArr.length }} -->
<!-- <button @tap="startWatch()">{{ polyline[0].points.length }}开始监听</button>
<button @click="fileWriter">存储</button> -->
<!-- 地图容器 -->
<view class="map-container">
<map id="amap" ref="mapRef" :latitude="latitude" :longitude="longitude" :scale="scale" :polyline="polyline"
:markers="markers" style="width: 100%; height: 100%;" />
<!-- <cover-image class="center-img" :src="center" v-show="reportParams.type == 'location'"></cover-image> -->
</view>
</view>
</template>
<script setup>
import { ref, onUnmounted, getCurrentInstance } from 'vue';
import { onLoad, onUnload, onReady } from '@dcloudio/uni-app';
import center from "../../static/mark.png";
import { locationReport, historyLocation } from '@/api/index'
// 地图相关变量
const mapRef = ref(null);
const latitude = ref(null); // 默认北京纬度
const longitude = ref(null); // 默认北京经度
const scale = ref(16)
// const scale = ref(20)
let reportParams = ref({})
const polyline = ref([
{
points: [],
color: '#007AFF', // 高德蓝,更醒目
width: 15, // 适当加粗
}
]);
const markers = ref([])
let mapContext = null;
onLoad((options) => {
reportParams.value = options
uni.setKeepScreenOn({
keepScreenOn: true
})
})
onReady(() => {
if (reportParams.value.type == 'location') {
startWatch()
} else if (reportParams.value.type == 'history') {
historyLocation(reportParams.value).then(async (res) => {
mapContext = uni.createMapContext('amap');
let arr = []
res.data.forEach(item => {
arr.push({
latitude: item.lat,
longitude: item.lng
})
})
polyline.value[0].points = JSON.parse(JSON.stringify(arr))
console.log("!!!!!",polyline)
// const centerPoint = await getCenterPoint(JSON.parse(JSON.stringify(arr)))
// console.log('centerPoint', centerPoint)
// const autoScale = await calculateScaleByPoints(JSON.parse(JSON.stringify(arr))); // 调用计算方法
// scale.value = autoScale.scale; // 替换固定的scale:20
mapContext.moveToLocation({
latitude: polyline.value[0].points[0].latitude,
longitude: polyline.value[0].points[0].longitude
});
})
}
})
const fileWriter = () => {
// 请求本地系统文件对象 plus.io.PRIVATE_WWW应用运行资源目录常量
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fobject) => {
// fs.root是根目录操作对象DirectoryEntry
fobject.root.getFile(formatTime(new Date()) + '.txt', {
create: true
}, (fileEntry) => {
fileEntry.file((file) => {
console.log('file :>> ', file)
// create a FileWriter to write to the file
fileEntry.createWriter((writer) => {
console.log('writer :>> ', writer)
// Write data to file.
// 返回页面的数据
writer.seek(file.size)
// 写入可以加入需要写入的数据
let content = "{points:" + JSON.stringify(polyline.value[0].points) + ",pointArr:" + JSON.stringify(pointArr.value) + "}"
writer.write(content)
const fileReader = new plus.io.FileReader()
fileReader.readAsText(file, 'utf-8')
fileReader.onloadend = (evt) => {
console.log('evt :>> ', evt)
}
console.log('file info :>> ', file)
// 获取对象
plus.io.resolveLocalFileSystemURL(file.fullPath, (res) => {
res.file((localFile) => {
const localFilePath = localFile.fullPath; // 本地文件路径
console.log("localFilePath :>> ", localFilePath)
// 直接保存并打开本地文件
uni.openDocument({
filePath: localFilePath,
success: () => console.log('打开成功')
});
});
})
}, (e) => {
console.log('e :>> ', e)
})
})
})
})
}
// 1. 计算中心点(经纬度平均值)
// const getCenterPoint = (points) => {
// const sumLng = points.reduce((sum, p) => sum + p.longitude, 0);
// const sumLat = points.reduce((sum, p) => sum + p.latitude, 0);
// return {
// longitude: sumLng / points.length,
// latitude: sumLat / points.length
// };
// };
// const calculateScaleByPoints = async (points) => {
// if (points.length <= 1) return 20; // 单点默认缩放18
// // 1. 获取经纬度极值
// const extremes = getLatLngExtremes(points);
// const centerLat = (extremes.minLat + extremes.maxLat) / 2; // 轨迹中心纬度
// // 2. 计算范围宽度(米)
// const rangeWidth = getRangeWidth(extremes);
// // 3. 获取地图容器宽度(像素)
// const containerWidth = await getMapContainerWidth();
// // 4. 目标让范围宽度占容器80%(留边距,避免轨迹贴边)
// const targetMeterPerPixel = rangeWidth / (containerWidth * 0.9);
// const scale = calculatePreciseScale(targetMeterPerPixel, centerLat);
// return { centerLat, scale };
// };
// const getLatLngExtremes = (points) => {
// if (!points.length) return null;
// const lats = points.map(p => p.latitude);
// const lngs = points.map(p => p.longitude);
// return {
// minLat: Math.min(...lats), // 最小纬度
// maxLat: Math.max(...lats), // 最大纬度
// minLng: Math.min(...lngs), // 最小经度
// maxLng: Math.max(...lngs), // 最大经度
// };
// };
// // 计算范围的东西向实际宽度(米)
// const getRangeWidth = (extremes) => {
// // 取纬度中点(减少纬度对经度距离计算的影响)
// const centerLat = (extremes.minLat + extremes.maxLat) / 2;
// // 计算同一纬度下,最小和最大经度的距离(米)
// return haversineDistance(centerLat, extremes.minLng, centerLat, extremes.maxLng);
// };
// // 2. Haversine公式计算两点经纬度的球面距离单位
// const haversineDistance = (lat1, lon1, lat2, lon2) => {
// const R = 6371000; // 地球半径(米)
// const φ1 = (lat1 * Math.PI) / 180;
// const φ2 = (lat2 * Math.PI) / 180;
// const Δφ = ((lat2 - lat1) * Math.PI) / 180;
// const Δλ = ((lon2 - lon1) * Math.PI) / 180;
// const a = Math.sin(Δφ / 2) ** 2 + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) ** 2;
// const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
// return R * c;
// };
// // 获取地图容器宽度(像素)- 适配uni-app
// const getMapContainerWidth = () => {
// // 获取容器节点信息
// return new Promise((resolve) => {
// uni.createSelectorQuery().in(getCurrentInstance())
// .select('.map-container')
// .boundingClientRect(rect => {
// resolve(rect.width); // 返回容器宽度(像素)
// })
// .exec();
// });
// };
// const calculatePreciseScale = (targetMeterPerPixel, centerLat) => {
// const earthRadius = 6378137; // 地球半径高德用的WGS84坐标系半径6378137米
// const centerLatRad = (centerLat * Math.PI) / 180; // 中心纬度转弧度
// // 高德官方公式:米/像素 = (2 * π * R * cos(lat)) / (2^(scale + 8))
// // 反向推导scale = log2( (2 * π * R * cos(lat)) / 米/像素 ) - 8
// const numerator = 2 * Math.PI * earthRadius * Math.cos(centerLatRad);
// const scaleLog = Math.log2(numerator / targetMeterPerPixel);
// let scale = scaleLog - 8;
// // scale必须是整数地图scale只支持整数同时限制范围10-20避免过大/过小)
// scale = Math.round(scale);
// console.log(scale, "scale")
// return Math.max(10, Math.min(20, scale));
// };
function formatTime(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // 补 0 为两位数(如 09
const day = String(date.getDate()).padStart(2, '0');
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
const second = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
let pointArr = ref([
// { longitude: 116.478935, latitude: 39.997761 },
// { longitude: 116.478939, latitude: 39.997825 },
// // {longitude:116.478912,latitude: 39.998549},
// { longitude: 116.478912, latitude: 39.998549 },
// // {longitude:116.478998,latitude: 39.998555},
// { longitude: 116.478998, latitude: 39.998555 },
// { longitude: 116.479282, latitude: 39.99856 },
// { longitude: 116.479658, latitude: 39.998528 },
// { longitude: 116.480151, latitude: 39.998453 },
// // {longitude:116.480784,latitude: 39.998302},
// { longitude: 116.480784, latitude: 39.998302 },
// { longitude: 116.481149, latitude: 39.998184 },
// { longitude: 116.481573, latitude: 39.997997 },
// { longitude: 116.481863, latitude: 39.997846 },
// { longitude:116.482072,latitude: 39.997718 },
// { longitude: 116.482362, latitude: 39.997718 },
// { longitude: 116.483633, latitude: 39.998935 },
// { longitude: 116.48367, latitude: 39.998968 },
// { longitude: 116.484648, latitude: 39.999861 }
])
// 检查并请求定位权限
const requestLocationPermission = async () => {
if (plus.os.name === "Android") {
// 检查后台定位权限Android 10+
const hasBgPermission = await plus.android.requestPermissions(
["android.permission.ACCESS_BACKGROUND_LOCATION"]
);
// if (!hasBgPermission.granted) {
// uni.showToast({ title: "请授予后台定位权限", icon: "none" });
// return false;
// }
} else if (plus.os.name === "iOS") {
// iOS 需引导用户开启"始终允许"定位
const authStatus = await uni.getLocationAuthStatus();
if (authStatus.authStatus !== 3) { // 3 表示"始终允许"
uni.showModal({
title: "提示",
content: "请在设置中开启始终允许定位,否则锁屏后无法跟踪",
success: () => {
plus.runtime.openURL("app-settings:"); // 打开应用设置页
}
});
return false;
}
}
return true;
};
let locationWatchId = null
// 初始化地图
const startWatch = (async () => {
// 先请求权限
const hasPermission = await requestLocationPermission();
if (!hasPermission) return;
// 获取地图上下文
mapContext = uni.createMapContext('amap');
try {
// 直接在组件中实现定位功能,不再依赖外部工具类
uni.startLocationUpdate({
type: 'gcj02',
// needFullAccuracy: true,
success: res => {
console.log('开启应用接收位置消息成功')
locationWatchId = uni.onLocationChange(function (res) {
let time = formatTime(new Date())
// 检查是否已有最后一个点,且与当前点经纬度相同
const points = polyline.value[0].points;
res.time = time
// console.log('points', formatTime(new Date()));
let hasSameLastPoint = false
if (points.length) {
hasSameLastPoint = points[points.length - 1].latitude === res.latitude && points[points.length - 1].longitude === res.longitude;
}
if (markers.value.length == 0) {
markers.value = [{
id: 1,
latitude: res.latitude,
longitude: res.longitude,
iconPath: center,
width: 10, // 小车图标宽度rpx/px根据需求调整
height: 10, // 小车图标高度
}]
} else {
markers.value[0].latitude = res.latitude
markers.value[0].longitude = res.longitude
}
// console.log('hasSameLastPoint', hasSameLastPoint);
if (!hasSameLastPoint) {
polyline.value[0].points.push({
latitude: res.latitude,
longitude: res.longitude,
accuracy: res.accuracy,
speed: res.speed,
altitude: res.altitude,
time: time
});
console.log(polyline.value[0].points)
latitude.value = res.latitude;
longitude.value = res.longitude;
mapContext.moveToLocation({
latitude: latitude.value,
longitude: longitude.value
});
reportParams.value.lat = res.latitude
reportParams.value.lng = res.longitude
// console.log('reportParams', reportParams.value);
locationReport(reportParams.value).then((res) => {
console.log("页面", res)
if (res.code == 401) {
console.log("停止")
stopLocationUpdate();
}
})
// console.log('纬度:' + res.latitude);
// console.log('经度:' + res.longitude);
}
// pointArr.value.push(res);
// console.log(pointArr.value)
});
},
fail: err => {
console.error('开启应用接收位置消息失败:', err)
},
complete: msg => console.log('调用开启应用接收位置消息 API 完成')
});
} catch (error) {
console.error('获取位置失败:', error);
uni.showToast({
title: '获取位置失败,使用默认位置',
icon: 'none'
});
}
});
// 页面卸载时清理资源
onUnmounted(() => {
stopLocationUpdate();
});
onUnload(() => {
stopLocationUpdate();
});
const stopLocationUpdate = () => {
if (locationWatchId) {
uni.offLocationChange();
locationWatchId = null;
}
uni.stopLocationUpdate({
success: () => console.log('已停止位置更新'),
fail: (err) => console.error('停止位置更新失败:', err)
});
};
</script>
<style scoped>
.container {
display: flex;
flex-direction: column;
height: calc(100vh - env(safe-area-inset-bottom));
}
.map-container {
flex: 1;
position: relative;
}
.center-img {
height: 80rpx;
left: 50%;
margin-left: -40rpx;
margin-top: -88rpx;
top: 50%;
width: 80rpx;
z-index: 9999;
position: absolute;
}
.position-img {
bottom: 32rpx;
height: 66rpx;
right: 16rpx;
width: 66rpx;
border-radius: 50%;
background-color: #fff;
overflow: hidden;
z-index: 9999;
position: absolute;
}
</style>

283
pages/index/signature.vue Normal file
View File

@@ -0,0 +1,283 @@
<template>
<view class="box">
<canvas v-if="showCanvas" id="canvas" canvas-id="canvas" style="width: 100vw;background-color: green;" :style="`height:${canvasHeight}px`" @touchstart="startDraw" @touchmove="drawing" @@touchend="endDraw"></canvas>
<cover-view class="tipTxt">请在空白处横屏签名</cover-view>
<view class="btn">
<uv-button size="large" style="width: 50%;" @tap="clearSign"> </uv-button>
<uv-button type="primary" size="large" class="mainBtn" style="width: 50%;" @tap="confirm"> </uv-button>
</view>
</view>
</template>
<script setup>
import { onReady, onUnload, onLoad } from '@dcloudio/uni-app';
import { ref, nextTick } from 'vue';
import { pathToBase64, base64ToPath } from './index.js'
import { uploadDeliveryAttachment } from "@/api/index"
const btnHeight = ref(null)
const BoxHeight = ref(null)
const canvasWidth = ref(null)
const canvasHeight = ref(null)
const showCanvas = ref(false)
const signFlag = ref(false)
const getBtnHeight = () => {
const query = uni.createSelectorQuery().in(this);
// 选择要查询的元素使用元素ID或类名
query.select('.btn').boundingClientRect(data => {
btnHeight.value = data.height
}).select('.box').boundingClientRect(res => {
// console.log('元素高度:', res);
// BoxHeight.value = res.height
// canvasWidth.value = res.width
}).exec(() => {
// 确保两个查询都完成后再初始化画布
if (btnHeight.value && BoxHeight.value && canvasWidth.value) {
console.log("获取",BoxHeight.value,btnHeight.value);
canvasHeight.value = BoxHeight.value - btnHeight.value
showCanvas.value = true
// 等待DOM更新完成后再初始化Canvas
nextTick(() => {
initCanvas();
});
} else {
console.error('获取元素尺寸失败,无法初始化画布');
}
}); // 执行查询
}
let canvasContext = null
const initCanvas = () => {
console.log(canvasWidth.value, canvasHeight.value)
canvasContext = uni.createCanvasContext('canvas');
canvasContext.setFillStyle('#F5F5F5'); // 设置填充颜色为红色
canvasContext.fillRect(0, 0, canvasWidth.value, canvasHeight.value); // 填充整个画板
canvasContext.draw(true)
}
const startX = ref(null)
const startY = ref(null)
const startDraw = (e) => {
startX.value = e.touches[0].x
startY.value = e.touches[0].y
canvasContext.beginPath(); // 开始创建一个路径
signFlag.value = true
}
const endDraw = (e) => {
startX.value = 0
startY.value = 0
}
const drawing = (e) => {
let temX = e.touches[0].x;
let temY = e.touches[0].y;
canvasContext.moveTo(startX.value, startY.value)
canvasContext.lineTo(temX, temY)
canvasContext.setStrokeStyle('#000') // 文字颜色
canvasContext.setLineWidth(4) // 文字宽度
canvasContext.setLineCap('round') // 文字圆角
canvasContext.stroke() // 文字绘制
startX.value = temX
startY.value = temY
canvasContext.draw(true) // 文字绘制到画布中
}
const clearSign = () => {
canvasContext.setFillStyle('#F5F5F5'); // 设置填充颜色为红色
canvasContext.fillRect(0, 0, canvasWidth.value, canvasHeight.value); // 填充整个画板
canvasContext.draw(true) // 文字绘制到画布中
signFlag.value = false
}
const confirm = () => {
// 确认按钮事件
if (!signFlag.value) {
uni.showToast({
title: '请签名后再点击确定',
icon: 'none',
duration: 2000
})
return
}
uni.showModal({
title: '确认',
content: '确认签名无误吗',
showCancel: true,
success: async ({ confirm }) => {
if (confirm) {
let tempFile
// if (this.horizontal) {
tempFile = await saveHorizontalCanvas()
// } else {
// tempFile = await this.saveCanvas()
// }
console.log(tempFile, "tempFile")
const base64 = await pathToBase64(tempFile)
const path = await base64ToPath(base64)
console.log(path, "path")
const res = await uploadDeliveryAttachment(path, uploadParams.value);
uni.navigateBack();
uni.$emit('getSignImg', { base64, path, url: res.data });
// uploadImg(path, {}).then(res=>{
// console.log(base64, path, res.url, "打印")
// let url = res.url
// uni.$emit('getSignImg', { base64, path, url })
// uni.navigateBack()
// })
}
}
})
}
const saveHorizontalCanvas = async () => {
return await new Promise((resolve, reject) => {
uni.canvasToTempFilePath(
{
canvasId: 'canvas',
fileType: "png", // 只支持png和jpg
success: (res) => {
if (!res.tempFilePath) {
uni.showModal({
title: '提示',
content: '保存签名失败',
showCancel: false
})
return
}
// #ifdef APP
uni.compressImage({
src: res.tempFilePath,
quality: 100, // 范围 0 - 100
rotate: 270,
success: (r) => {
console.log('==== compressImage :', r)
resolve(r.tempFilePath)
}
})
// #endif
// #ifndef APP
// uni.getImageInfo({
// src: res.tempFilePath,
// success: (r) => {
// // console.log('==== getImageInfo :', r)
// // 将signCanvas的内容复制到hsignCanvas中
// const hcanvasCtx = uni.createCanvasContext('hsignCanvas', this)
// // 横屏宽高互换
// hcanvasCtx.translate(this.canvasHeight / 2, this.canvasWidth / 2)
// hcanvasCtx.rotate(Math.PI * (-90 / 180))
// hcanvasCtx.drawImage(
// r.path,
// -this.canvasWidth / 2,
// -this.canvasHeight / 2,
// this.canvasWidth,
// this.canvasHeight
// )
// hcanvasCtx.draw(false, async () => {
// const hpathRes = await uni.canvasToTempFilePath(
// {
// canvasId: 'hsignCanvas',
// fileType: this.expFile.fileType, // 只支持png和jpg
// quality: this.expFile.quality // 范围 0 - 1
// },
// this
// )
// let tempFile = ''
// if (Array.isArray(hpathRes)) {
// hpathRes.some((item) => {
// if (item) {
// tempFile = item.tempFilePath
// return
// }
// })
// } else {
// tempFile = hpathRes.tempFilePath
// }
// resolve(tempFile)
// })
// }
// })
// #endif
},
fail: (err) => {
console.log('图片生成失败:' + err)
resolve(false)
}
},
this
)
})
}
onReady(() => {
// uni.showLoading({
// title: '正在加载,请稍后...',
// mask: true
// })
// 页面加载完成后设置横屏
// plus.screen.lockOrientation('landscape-primary')
// nextTick(() => {
// getBtnHeight();
// });
// setTimeout(() => {
// uni.hideLoading();
uni.getSystemInfo({
success: function (res) {
console.log(res)
BoxHeight.value = res.windowHeight
canvasWidth.value = res.windowWidth
}
});
getBtnHeight();
// }, 1500);
// nextTick(() => {
// getBtnHeight();
// });
})
const uploadParams = ref({})
onLoad((options) => {
uploadParams.value = options
})
onUnload(() => {
// 页面卸载时恢复竖屏
// plus.screen.lockOrientation('portrait-primary');
})
</script>
<style scoped lang="scss">
.box {
position: relative;
width: 100vw;
height: 100vh;
.tipTxt{
position: absolute;
top: 10rpx;
left: 10rpx;
color: #999;
font-size: 44rpx;
}
.btn {
position: fixed;
width: 100vw;
bottom: 0;
z-index: 99;
display: flex;
align-items: center;
justify-content: space-between;
}
}
</style>

128
pages/login/login.vue Normal file
View File

@@ -0,0 +1,128 @@
<template>
<view class="container">
<view class="huanying">HELLO</view>
<view class="huanyingText">
欢迎登录
<text>一公里配送</text>
</view>
<view class="huanyingDes">
近距离快响应即时补给线
</view>
<uv-form labelPosition="left" :model="formModel" labelWidth="80" :rules="rules" class="formContent" ref="formRef">
<uv-form-item label="用户名" prop="userInfo.username" borderBottom>
<uv-input v-model="formModel.userInfo.username" border="none" placeholder="请输入用户名" />
</uv-form-item>
<uv-form-item label="密码" prop="userInfo.password" borderBottom>
<uv-input v-model="formModel.userInfo.password" :type="showPass ? '' : 'password'" border="none" placeholder="请输入密码" />
<template v-slot:right>
<uv-icon name="eye" @tap="changePassStatus" v-show="!showPass"></uv-icon>
<uv-icon name="eye-off-outline" @tap="changePassStatus" v-show="showPass"></uv-icon>
</template>
</uv-form-item>
<uv-button type="primary" size="large" text="登 录" shape="circle" color="#199793" customStyle="margin-top: 50px" @click="submit"></uv-button>
</uv-form>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { userLogin } from "@/api/login"
const formModel = ref({
userInfo: {
username: '',
password: '',
},
})
const showPass = ref(false)
const formRef = ref()
const rules = ({
'userInfo.username': {
type: 'string',
required: true,
message: '输入用户名',
trigger: ['blur', 'change']
},
'userInfo.password': {
type: 'string',
required: true,
message: '请填写密码',
trigger: ['blur', 'change']
},
})
const changePassStatus = () => {
showPass.value = !showPass.value
}
// 提交
const submit = () => {
// 如果有错误会在catch中返回报错信息数组校验通过则在then中返回true
formRef.value.validate().then(res => {
userLogin(formModel.value.userInfo).then(res => {
console.log("登录成功", res)
uni.setStorageSync('token', res.data.token)
uni.setStorageSync('user', res.data.user)
uni.navigateTo({
url: "/pages/index/index"
})
})
}).catch(errors => {
// uni.showToast({
// icon: 'error',
// title: '校验失败'
// })
})
}
</script>
<style lang="scss" scoped>
.container{
// display: flex;
// align-items: center;
// justify-content: center;
// flex-direction: column;
padding: 0 56rpx;
.title{
font-size: 72rpx;
}
.formContent{
// width: 80%;
margin-top: 140rpx;
}
.huanyingText {
font-family: PingFang SC, PingFang SC;
font-weight: bold;
font-size: 34rpx;
color: #000000;
line-height: 44rpx;
font-style: normal;
margin-top: 38rpx;
text {
color: #199793;
}
}
.huanying {
margin-top: 130rpx;
font-family: PingFang SC, PingFang SC;
font-weight: bold;
font-size: 72rpx;
color: #000000;
}
.huanyingDes {
font-family: PingFang SC, PingFang SC;
font-weight: 400;
font-size: 28rpx;
color: #999999;
line-height: 44rpx;
font-style: normal;
margin-top: 24rpx;
}
}
::v-deep .uv-input__content{
flex-direction: column;
align-items: unset;
}
</style>