feat:新增蓝牙功能和车辆上下架功能

This commit is contained in:
5g0Wp7Zy 2025-11-05 16:48:52 +08:00
parent 8981806b90
commit c342fea9a5
17 changed files with 1546 additions and 52 deletions

View File

@ -1,8 +1,16 @@
import type { IExecuteCommand } from '@/api/types/centerControl'
import { http } from '@/http/http'
/**
* SN或BikeCode
* 线
*/
export function checkSnOrBikeCodeAPI(query: { ecuSn: string, bikeCode: string }) {
return http.get<any>('/operations/ebikeEcuInfo/checkSnOrBikeCode', query)
return http.get<any>('/operations/ebikeEcuInfo/online', query)
}
/**
*
*/
export function executeCommandAPI(query: IExecuteCommand) {
return http.get<any>('/operations/ebikeEcuInfo/executeCommand', query)
}

View File

@ -0,0 +1,7 @@
// 中控执行命令
export interface IExecuteCommand {
ecuSn: string
bikeCode: string
commandCode: 'FIND_BIKE' | 'GPS' | 'OPEN_BATTERY_LOCK'
}

26
src/api/warehouse.ts Normal file
View File

@ -0,0 +1,26 @@
import { http } from '@/http/http'
// 批量上架
export function batchAddInventoryAPI(data: { bikeCodes: string[], regionId: string }) {
return http.post<any>('/operations/ebikeBikeInfo/batchLaunch', data)
}
// 批量下架
export function batchRemoveInventoryAPI(data: { bikeCodes: string[] }) {
return http.post<any>('/operations/ebikeBikeInfo/batchUnLaunch', data)
}
// 未上架车辆分页查询
export function getUnInventoryListAPI(query: { pageSize: number, pageNum: number }) {
return http.get<any>('/operations/ebikeBikeInfo/unLaunchPage', query)
}
// 上架车辆分页查询
export function getInventoryListAPI(query: { pageSize: number, pageNum: number }) {
return http.get<any>('/operations/ebikeBikeInfo/launchPage', query)
}
// 查询所有运营中区域表
export function getOperatoringAllListAPI() {
return http.get<any>('/operations/ebikeRegion/operationList')
}

View File

@ -0,0 +1,847 @@
<script lang="ts" setup>
import { AES128ECBEncrypt, bufTohex } from '@/utils/aesTools'
definePage({
style: {
navigationBarTitleText: '蓝牙连接',
navigationBarBackgroundColor: '#1488f5',
navigationBarTextStyle: 'white',
},
})
const list_item_style = computed(() => {
return {
padding: '20rpx 0',
borderBottom: '2rpx solid #cccccc',
fontSize: '28rpx',
color: '#6a6a6a',
}
})
const list_item_btn_style = computed(() => {
return {
height: '35px',
fontSize: '24rpx',
}
})
//
const config = reactive({
TIMEOUT: 5000, //
ISOPEN: false, //
})
//
const bluetoothConfig = reactive({
deviceId: '', // id
status: -1, // -1 0 1
RSSI: 0, //
localName: '', /// /
serviceId: '', // id
characteristicId: '', // id
writeCharacteristicId: '', // id
btnIsLoading: false, //
isCloseBlue: false, // (使)
currentInstruct: '', //
})
//
const notifyRef = ref(null)
const notifyConfig = reactive({
message: 'asdasdas',
type: 'primary', // primarysuccesswarning,error
})
//
const commandList = reactive({
//
openLock: {
title: '车锁开',
key: 'openLock',
status: 'none', // none,loading,success,fail
btnText: '执行',
btnLoadingText: '执行中',
btnLoading: false,
msg: '执行车锁开命令:',
},
})
const codeType = ref<'car' | 'sn'>('car')
const code = ref<string>('')
function handleType() {
code.value = ''
codeType.value = codeType.value === 'car' ? 'sn' : 'car'
}
function handleScan() {
openBluetoothAdapter()
}
//
function openBluetoothAdapter() {
return new Promise((resolve, reject) => {
uni.showLoading({
title: '正在打开蓝牙适配器中。。。',
})
uni.openBluetoothAdapter({
success(res) {
const { errno } = res
uni.hideLoading()
if (errno !== 0) {
reject(new Error('蓝牙适配器打开失败'))
}
config.ISOPEN = true
resolve(res)
console.log('蓝牙适配器------>', res)
},
fail(err) {
uni.hideLoading()
console.error(err)
reject(new Error('打开蓝牙适配器失败'))
},
})
})
}
//
function closeBluetoothAdapter() {
config.ISOPEN = false
bluetoothConfig.deviceId = ''
uni.closeBluetoothAdapter({
success(res) {
console.log(res)
},
fail(err) {
console.error(err)
},
})
}
//
function setBluetoothDevice(device: any) {
bluetoothConfig.RSSI = device.RSSI
bluetoothConfig.localName = device.localName
}
//
function stopBluetoothDevicesDiscovery() {
uni.stopBluetoothDevicesDiscovery({
success(res) {
console.log(res)
},
fail(err) {
console.error(err)
},
})
}
/**
* 搜索蓝牙设备
*/
async function handleSearch() {
try {
bluetoothConfig.btnIsLoading = true
bluetoothConfig.isCloseBlue = false
//
if (!config.ISOPEN) {
await openBluetoothAdapter()
}
const device = await startBluetoothDevicesDiscovery()
console.log('搜索成功的设备:', device)
await createBLEConnection()
await getServiceAndCharacteristicId()
bluetoothConfig.status = 1
setBluetoothDevice(device)
console.log(bluetoothConfig)
bluetoothConfig.btnIsLoading = false
}
catch (error) {
console.error(error)
bluetoothConfig.btnIsLoading = false
closeBluetoothAdapter()
clearBuletoothConfig()
uni.showToast({
title: error.message || '未知错误',
icon: 'none',
duration: 1500,
})
}
}
//
async function startBluetoothDevicesDiscovery(targetName = 'ECU-2370171956', timeout = config.TIMEOUT): Promise<any> {
return new Promise((resolve, reject) => {
let isCompleted = false // resolve/reject
const cleanup = () => {
if (!isCompleted) {
isCompleted = true
stopBluetoothDevicesDiscovery()
uni.hideLoading()
}
}
//
uni.startBluetoothDevicesDiscovery({
success: (res) => {
//
if (res.errno !== 0) {
cleanup()
return reject(new Error('启动蓝牙搜索失败'))
}
//
uni.showLoading({ title: '正在搜索蓝牙设备...' })
uni.onBluetoothDeviceFound((e: any) => {
if (isCompleted) {
return
};
const devices = e.devices
for (let i = 0; i < devices.length; i++) {
if (devices[i].localName === targetName) {
bluetoothConfig.deviceId = devices[i].deviceId
cleanup()
return resolve(devices[i]) //
}
}
})
//
setTimeout(() => {
if (!isCompleted) {
cleanup()
reject(new Error('设备搜索超时'))
}
}, timeout)
},
fail: (err) => {
cleanup()
reject(new Error(`获取蓝牙设备失败: ${err.errMsg || err}`))
},
})
})
}
//
function createBLEConnection() {
return new Promise((resolve, reject) => {
if (!bluetoothConfig.deviceId) {
return reject(new Error('蓝牙设备id未知'))
}
//
uni.createBLEConnection({
deviceId: bluetoothConfig.deviceId,
success: (res: any) => {
const { errCode } = res
if (errCode === 0) {
console.log('创建低功率蓝牙成功', res)
return resolve(res)
}
else {
bluetoothConfig.status = 0
reject(new Error(res.errMsg || '创建低功率蓝牙失败'))
}
},
fail: (err) => {
bluetoothConfig.status = 0
reject(new Error(`创建低功率蓝牙失败: ${err.errMsg || err}`))
},
})
})
}
/**
* 寻找匹配的ids
* @param services 服务列表
* @param targetSubServiceIds 目标子服务ID
* @param returnFirst 是否返回第一个匹配项
*/
function findMatchingIds(services: any[] = [], targetSubServiceIds: string[] = [], returnFirst = true) {
const matches = []
for (let i = 0; i < services.length; i++) {
const serviceId = services[i].uuid
const subServiceId = serviceId.substring(0, 8).toUpperCase() //
if (targetSubServiceIds.includes(subServiceId)) {
matches.push(services[i])
if (returnFirst) {
return matches[0] //
}
}
}
return returnFirst ? null : matches // null
}
// idid
function getServiceAndCharacteristicId() {
return new Promise((resolve, reject) => {
if (!bluetoothConfig.deviceId) {
reject(new Error('初始化服务id和特征值id失败'))
}
// (service)
uni.getBLEDeviceServices({
deviceId: bluetoothConfig.deviceId,
success: (res: any) => {
const { services } = res
if (services && services.length > 0) {
const serviceObj = findMatchingIds(services, ['0000FFF0', '6E400001'])
if (serviceObj) {
bluetoothConfig.serviceId = serviceObj.uuid
console.log('服务id', bluetoothConfig.serviceId)
// (characteristic)
uni.getBLEDeviceCharacteristics({
deviceId: bluetoothConfig.deviceId,
serviceId: bluetoothConfig.serviceId,
success: (rs: any) => {
const { characteristics } = rs
if (characteristics && characteristics.length > 0) {
//
const characteristicIdObj = findMatchingIds(characteristics, ['0000FFF6', '6E400003'])
if (characteristicIdObj) {
bluetoothConfig.characteristicId = characteristicIdObj.uuid
}
//
const writeCharacteristicIdObj = findMatchingIds(characteristics, ['6E400002'])
if (writeCharacteristicIdObj) {
bluetoothConfig.writeCharacteristicId = writeCharacteristicIdObj.uuid
}
console.log('特征值获取', bluetoothConfig.characteristicId, bluetoothConfig.writeCharacteristicId)
}
startNotify()
resolve({
serviceId: bluetoothConfig.serviceId,
characteristicId: bluetoothConfig.characteristicId,
writeCharacteristicId: bluetoothConfig.writeCharacteristicId,
})
},
fail: () => {
reject(new Error('获取蓝牙设备所有特征值失败------'))
},
})
}
}
},
fail: () => {
reject(new Error('获取蓝牙设备所有服务失败------'))
},
})
})
}
//
function clearBuletoothConfig() {
bluetoothConfig.status = -1
bluetoothConfig.RSSI = 0
bluetoothConfig.characteristicId = ''
bluetoothConfig.serviceId = ''
bluetoothConfig.deviceId = ''
bluetoothConfig.localName = ''
bluetoothConfig.writeCharacteristicId = ''
bluetoothConfig.isCloseBlue = false
bluetoothConfig.currentInstruct = ''
}
//
function closeBLEConnection() {
if (!bluetoothConfig.deviceId)
return
uni.closeBLEConnection({
deviceId: bluetoothConfig.deviceId,
success: (res: any) => {
console.log('关闭低功率蓝牙成功', res)
bluetoothConfig.isCloseBlue = true
clearBuletoothConfig()
},
fail: (err) => {
clearBuletoothConfig()
console.log('关闭低功率蓝牙失败', err)
},
})
}
//
async function handleCommand(value: any, key: string, index: number) {
const comm = value.key
bluetoothConfig.currentInstruct = key
try {
commandList[key].btnLoading = true
switch (comm) {
case 'openLock': { //
const buffer = new ArrayBuffer(5)
const dataView = new DataView(buffer)
dataView.setUint32(0, 0x67740082)
dataView.setUint8(4, 0x82)
await writeBLECharacteristicValueWithBuff(buffer)
break
}
}
commandList[key].btnLoading = false
}
catch (err) {
commandList[key].btnLoading = false
console.error(err)
}
}
function writeBLECharacteristicValueWithBuff(buffer: any) {
return new Promise((resolve, reject) => {
if (
!buffer
&& !bluetoothConfig.writeCharacteristicId
&& !bluetoothConfig.serviceId
&& !bluetoothConfig.deviceId) {
return
}
console.log('writeBLECharacteristicValueWithBuff', buffer)
uni.writeBLECharacteristicValue({
deviceId: bluetoothConfig.deviceId,
serviceId: bluetoothConfig.serviceId,
characteristicId: bluetoothConfig.writeCharacteristicId,
value: buffer,
success(res) {
resolve(res)
console.log('writeBLECharacteristicValue success', res.errMsg)
},
fail(err: any) {
reject(new Error(err.errMsg))
},
complete(res) {},
})
})
}
// notify
function startNotify() {
uni.notifyBLECharacteristicValueChange({
state: true,
deviceId: bluetoothConfig.deviceId,
serviceId: bluetoothConfig.serviceId,
characteristicId: bluetoothConfig.characteristicId,
success() {
console.log('notifyBLECharacteristicValueChange success')
verification()
},
fail(err) {
console.log('notifyBLECharacteristicValueChange fail', err)
},
})
}
async function verification(mac: any = undefined, key1: any = undefined, key2: any = undefined) {
if (mac && key1 && key2) {
// 2120
const buffer = new ArrayBuffer(20)
const dataView = new DataView(buffer)
dataView.setUint32(0, 0x6774108A)
const sourceKey1 = mac + key1
const AESEncrypt = AES128ECBEncrypt(sourceKey1, key2)
if (AESEncrypt.length !== 32) {
return
}
for (let i = 0; i < 4; i++) {
const subStr = AESEncrypt.substring(i * 8, i * 8 + 8, 16)
dataView.setUint32(i * 4 + 4, Number.parseInt(subStr, 16)) // 401523947(16)-->>10-->dateView
}
let i
let bcc // 1716+1
for (i = 0; i < 17; i++) {
bcc ^= dataView.getInt8(i + 3)
}
const buffer2 = new ArrayBuffer(1)
const dataView2 = new DataView(buffer2)
dataView2.setUint8(0, bcc)
console.log('验证---------------->', bufTohex(buffer))
writeBLECharacteristicValueWithBuff(buffer)
setTimeout(() => {
writeBLECharacteristicValueWithBuff(buffer2)
}, 300)
}
else {
const buffer = new ArrayBuffer(5)
const dataView = new DataView(buffer)
dataView.setUint32(0, 0x6774008A)
dataView.setUint8(4, 0x8A)
await writeBLECharacteristicValueWithBuff(buffer)
}
}
let verificationTimes = 0
function distributeCommandsWithDataStr(hex: string) {
const cmd = hex.substring(6, 8)
const code = hex.substring(8, 10)
let errCode: any = code
const desDic: any = {}
let des = ''
//
if (code === '00') {
des = '成功'
errCode = 0
}
else if (code === 'ff') {
// -1
des = '锁已打开'
errCode = -1
}
else if (code === 'fe') {
// -2
des = '电量不足'
errCode = -2
}
else if (code === 'fd') {
// -3
des = '加密验证失败'
errCode = -3
}
else if (code === 'fc') {
// -4
des = '内存空间不足'
errCode = -4
}
else if (code === 'fb') {
// -5
des = '格式错误'
errCode = -5
}
else if (code === 'fa') {
// -6
des = '没有验证或验证失败'
errCode = -6
desDic.mac = hex.substring(10, 22)
desDic.key = hex.substring(22, 30)
}
else if (code === 'f9') {
// -7
des = 'flash写入失败'
errCode = -7
}
else if (code === 'f8') {
// -8
des = '数据空'
errCode = -8
}
else if (code === 'f7') {
// -9
des = '操作失败'
errCode = -9
}
else if (code === 'f6') {
// -10
des = '运输模式下无法执行该指令'
errCode = -10
}
else if (code === 'f5') {
// -11
des = '锁车失败,车辆未停止'
errCode = -11
}
else if (code === 'f4') {
// -12
des = '未检测到道钉信息'
errCode = -12
}
else if (Number.parseInt(code, 16) === 149) {
//
des = '摄像头正在工作中'
errCode = Number.parseInt(code, 16)
}
else if (Number.parseInt(code, 16) === 150) {
//
des = '摄像头离线'
errCode = Number.parseInt(code, 16)
}
else if (Number.parseInt(code, 16) === 151) {
//
des = '车辆不在使用中(临时停车或是还车中)无法使用摄像头还车'
errCode = Number.parseInt(code, 16)
}
else if (Number.parseInt(code, 16) === 152) {
//
des = '执行命令超时'
errCode = Number.parseInt(code, 16)
}
else if (Number.parseInt(code, 16) === 153) {
//
des = '摄像头执行失败'
errCode = Number.parseInt(code, 16)
}
else if (Number.parseInt(code, 16) === 154) {
//
des = '不在站点中或没有垂直停车'
errCode = Number.parseInt(code, 16)
}
else {
des = `未知错误码${code}`
errCode = code
}
console.log('distributeCommandsWithDataStr', hex)
desDic.errCode = errCode
desDic.des = des
//
if (bluetoothConfig.currentInstruct) {
const message = commandList[bluetoothConfig.currentInstruct].msg + desDic.des
notifyRef.value.show({
type: 'success',
message,
safeAreaInsetTop: true,
})
}
if (cmd === '4a') {
//
if (verificationTimes >= 1) {
verificationTimes = 0
if (errCode === -6) {
verificationTimes = 1
verification(desDic.mac, desDic.key, '6666660000888888')
return
}
}
else {
verificationTimes += 1
}
if (errCode === -6) {
verification(desDic.mac, desDic.key, '6666660000888888')
}
}
}
//
uni.onBLEConnectionStateChange(async (res) => {
console.log('蓝牙连接状态改变', res)
try {
const { connected } = res
if (!connected && !bluetoothConfig.isCloseBlue) {
bluetoothConfig.status = 0
await createBLEConnection()
await getServiceAndCharacteristicId()
bluetoothConfig.status = 1
// if (reconnectNum < 3) {
// await createBLEConnection()
// reconnectNum += 1
// }
// else {
// closeBLEConnection()
// }
// }
}
}
catch (err) {
console.log('蓝牙连接状态改变失败', err)
}
})
//
uni.onBLECharacteristicValueChange((res) => {
console.log('onBLECharacteristicValueChange-----------》', res)
const hex = bufTohex(res.value)
if (hex.length >= 10) {
const header = hex.substring(0, 4)
if (header === '6774') {
const contentL1 = hex.substring(4, 6)
const contentL = Number.parseInt(contentL1, 16)
if ((contentL + 5) * 2 === hex.length) {
distributeCommandsWithDataStr(hex)
}
else if ((contentL + 5) * 2 > hex.length) {
// self.responseStr = hex
}
}
}
})
onBeforeUnmount(() => {
if (config.ISOPEN) {
closeBluetoothAdapter()
}
closeBLEConnection()
clearBuletoothConfig()
})
</script>
<template>
<z-paging
ref="paging"
bg-color="#f3f3f3"
>
<view class="container">
<!-- 编号输入框 -->
<view class="card_bar">
<!-- 车辆编号 -->
<view class="card_item card_border">
<view class="card_item_title">
<uv-button
plain style="margin-right: 25rpx;"
@click="handleType"
>
<uv-icon
name="jiantou_shangxiaqiehuan"
custom-prefix="custom-icon"
size="20"
/>
<text>{{ codeType === 'car' ? '车辆编号' : "中控SN" }}</text>
</uv-button>
</view>
<view class="card_item_value">
<uv-input
v-model="code"
placeholder="请输入或扫码"
>
<template #suffix>
<uv-icon name="scan" size="22" @click="handleScan" />
</template>
</uv-input>
</view>
</view>
<!-- 设备信息 -->
<view class="card_item">
<view class="card_item_title">
设备编号
</view>
<view class="card_item_value">
{{ bluetoothConfig.localName || '--' }}
</view>
</view>
<view class="device_bar">
<text
:style="{
color: bluetoothConfig.status === 0 ? '#f00' : bluetoothConfig.status === 1 ? '#04b604' : '',
}"
>
状态{{ bluetoothConfig.status === -1 ? '未连接' : bluetoothConfig.status === 0 ? '连接失败' : '连接成功' }}
</text>
<text>信号强度{{ bluetoothConfig.RSSI || 0 }}</text>
</view>
</view>
<!-- 配置信息栏 -->
<view class="card_bar u-m-t-30">
<uv-list>
<uv-list-item
v-for="(value, key, index) in commandList"
:key="key"
:custom-style="list_item_style"
>
<view class="list-item">
<view class="text_bar">
<view>{{ value.title }} </view>
</view>
<view class="btn_bar">
<uv-button
type="primary"
:loading="value.btnLoading"
:loading-text="value.btnLoadingText"
:text="value.btnText"
:disabled="bluetoothConfig.status !== 1"
:custom-style="list_item_btn_style"
@click="handleCommand(value, key, index)"
/>
</view>
</view>
</uv-list-item>
</uv-list>
</view>
</view>
<template #bottom>
<view class="bottom-button-zaping">
<uv-button
v-if="bluetoothConfig.status === -1"
type="primary"
text="连接设备"
:loading="bluetoothConfig.btnIsLoading"
loading-text="正在连接中"
@click="handleSearch"
/>
<uv-button
v-if="bluetoothConfig.status === 1"
type="error"
text="断开连接"
@click="closeBLEConnection"
/>
<uv-button
v-if="bluetoothConfig.status === 0"
type="primary"
text="连接设备"
:loading="true"
loading-text="正在重连"
/>
</view>
</template>
<uv-notify ref="notifyRef" />
</z-paging>
</template>
<style lang="scss" scoped>
.container {
width: calc(100% - 40rpx);
padding: 10rpx 20rpx;
.card_bar {
width: calc(100% - 40rpx);
background-color: #fff;
border-radius: 10rpx;
box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.1);
padding: 20rpx;
.card_item {
padding: 20rpx 0;
display: flex;
justify-content: space-between;
align-items: center;
.card_item_title {
width: 100rpx;
font-size: 30rpx;
color: $font-gray;
flex: 1;
}
.card_item_value {
flex: 2;
}
}
}
.device_bar {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 28rpx;
color: $font-gray;
}
}
.card_border {
border-bottom: 2rpx solid #d5d0d0;
}
.list-item {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
.text_bar {
font-size: 28rpx;
display: flex;
align-items: center;
.result {
margin-left: 30rpx;
}
}
}
</style>

View File

@ -1,4 +1,5 @@
<script lang="ts" setup>
import { executeCommandAPI } from '@/api/centerControl'
import { useCenterControlStore } from '@/store'
//
@ -20,25 +21,32 @@ const list_item_btn_style = computed(() => {
}
})
const type = ref<'car' | 'sn' | ''>('')
const code = ref<string>('')
const list = reactive({
online: {
msg: 'asdas',
name: '在线测试',
key: 'online',
findBike: {
msg: '',
name: '寻车铃',
key: 'FIND_BIKE',
status: 'none', // none,loading,success,fail
tip: '',
btnText: '执行',
btnLoadingText: '执行中',
btnLoading: false,
},
findEbike: {
msg: 'asdas',
name: '在线测试',
key: 'findEbike',
status: 'none', // none,loading,success,fail
gps: {
msg: '',
name: 'gps测试',
key: 'GPS',
status: 'none',
tip: '',
btnText: '执行',
btnLoadingText: '执行中',
btnLoading: false,
},
openLock: {
msg: '',
name: '打开电池锁',
key: 'OPEN_BATTERY_LOCK',
status: 'none',
tip: '',
btnText: '执行',
btnLoadingText: '执行中',
@ -52,24 +60,64 @@ function initListItem(key: string) {
list[key].msg = ''
}
function handleBtnClick(value: any, key: string, index: number) {
//
async function handleBtnClick(value: any, key: string, index: number) {
initListItem(key)
list[key].btnLoading = true
list[key].status = 'loading'
setTimeout(() => {
try {
const res = await executeCommandAPI({
ecuSn: centerStore.queryByBike.ecuSn,
bikeCode: centerStore.queryByBike.bikeCode,
commandCode: value.key,
})
}
catch (e) {
console.error('中控测试报错------>', e)
list[key].status = 'fail'
list[key].msg = e.message
list[key].btnLoading = false
list[key].status = 'success'
list[key].msg = '执行成功'
}, 2000)
}
}
watch(() => props.code, (newVal) => {
code.value = newVal
}, { immediate: true })
//
async function initAllTest() {
for (const key in list) {
list[key].status = 'none'
list[key].msg = ''
}
watch(() => props.type, (newVal) => {
type.value = newVal
}, { immediate: true })
// ()
for (const key in list) {
console.log(key)
list[key].btnLoading = true
list[key].status = 'loading'
try {
const res = await executeCommandAPI({
ecuSn: centerStore.queryByBike.ecuSn,
bikeCode: centerStore.queryByBike.bikeCode,
commandCode: list[key].key,
})
}
catch (e) {
console.error('中控测试报错------>', e)
list[key].status = 'fail'
list[key].msg = e.message
list[key].btnLoading = false
}
}
}
//
function loopExecuteCommand() {
initAllTest()
}
defineExpose({
loopExecuteCommand,
})
</script>
<template>
@ -113,8 +161,9 @@ watch(() => props.type, (newVal) => {
<uv-button
:loading="value.btnLoading"
:loading-text="value.btnLoadingText"
type="primary"
:text="value.btnText"
:disabled="centerStore.isOnline !== 1"
type="primary" :text="value.btnText"
:custom-style="list_item_btn_style"
@click="handleBtnClick(value, key, index)"
/>

View File

@ -3,8 +3,6 @@ import { checkSnOrBikeCodeAPI } from '@/api/centerControl'
import { useCenterControlStore } from '@/store'
import { scanCode } from '@/utils/tools'
const props = defineProps(['code', 'type'])
const emits = defineEmits(['change', 'update:code', 'update:type'])
const store = useCenterControlStore()
const code = ref('')
const btnLoading = ref(false)
@ -38,6 +36,14 @@ function handleInput(val: string) {
//
async function handleConnect() {
if (!code.value) {
uni.showToast({
title: '请输入或扫描编号',
icon: 'none',
})
return
}
btnLoading.value = true
try {
const isOnline = await checkSnOrBikeCodeAPI(store.queryByBike)
@ -49,6 +55,16 @@ async function handleConnect() {
btnLoading.value = false
}
}
watch(() => store.queryByBike, (newVal) => {
console.log('queryByBike', newVal)
if (newVal.bikeCode) {
return code.value = newVal.bikeCode
}
if (newVal.ecuSn) {
return code.value = newVal.ecuSn
}
}, { immediate: true, deep: true })
</script>
<template>

View File

@ -1,4 +1,5 @@
<script lang="ts" setup>
import { useCenterControlStore } from '@/store'
import executionList from './components/executionList.vue'
import scanByTest from './components/scanByTest.vue'
@ -9,6 +10,13 @@ definePage({
navigationBarTextStyle: 'white',
},
})
const executionListRef = ref(null)
const store = useCenterControlStore()
function handleTestClick() {
executionListRef.value?.loopExecuteCommand()
}
</script>
<template>
@ -21,12 +29,17 @@ definePage({
<scan-by-test />
<!-- 测试中控执行列表 -->
<executionList />
<executionList ref="executionListRef" />
</view>
<template #bottom>
<view class="bottom-button-zaping">
<uv-button type="primary" text="自动化测试" />
<uv-button
:disabled="store.isOnline !== 1"
type="primary"
text="自动化测试"
@click="handleTestClick"
/>
</view>
</template>
</z-paging>

View File

@ -0,0 +1,212 @@
<script lang="ts" setup>
import { batchRemoveInventoryAPI, getInventoryListAPI, getOperatoringAllListAPI } from '@/api/warehouse'
import { useAppStore, useOperatorStore } from '@/store'
definePage({
style: {
navigationBarTitleText: '运营车辆',
navigationBarBackgroundColor: '#1488f5',
navigationBarTextStyle: 'white',
},
})
const appStore = useAppStore()
const operatorStore = useOperatorStore()
const paging = ref<any>(null)
const list = ref<any>([])
const checkboxValue = ref<any>([])
const operatorAllList = ref<any>([])
const regionId_list = ref<any>([])
const modalRef = ref<any>(null)
//
function refreshList() {
list.value = []
}
//
async function queryList(pageNo: number, pageSize: number) {
try {
uni.showLoading({
title: '加载中',
})
const { records } = await getInventoryListAPI({
pageNum: pageNo,
pageSize,
})
uni.hideLoading()
paging.value.complete(records)
}
catch (e) {
uni.hideLoading()
paging.value.complete(false)
}
}
//
function vehicleHoisting() {
modalRef.value.open()
}
//
async function getOperatoringAllList() {
try {
const res = await getOperatoringAllListAPI()
if (res && res.length > 0) {
regionId_list.value = res.map((item) => {
return {
text: item.regionName,
value: item.regionId,
}
})
}
}
catch (e) {
console.error(new Error('获取运营区'))
}
}
//
async function submit() {
try {
uni.showLoading({
title: '正在提交中。。。',
})
const res = await batchRemoveInventoryAPI({
bikeCodes: checkboxValue.value,
})
uni.hideLoading()
modalRef.value.close()
modalRef.value.closeLoading()
paging.value.reload()
checkboxValue.value = []
}
catch (e) {
modalRef.value.closeLoading()
uni.hideLoading()
}
}
onMounted(async () => {
operatorAllList.value = await operatorStore.getOperatorList()
getOperatoringAllList()
})
</script>
<template>
<view>
<z-paging
ref="paging"
v-model="list"
:default-page-no="Number('1')"
:default-page-size="Number('10')"
:auto-show-back-to-top="Boolean(true)"
bg-color="#f3f3f3"
@query="queryList"
@refresher-touchend="refreshList"
>
<view class="container">
<uv-checkbox-group
v-model="checkboxValue"
size="20"
>
<view class="list-panel">
<view
v-for="(item) in list"
:key="item.bikeInfoId"
class="list-item"
>
<view class="header d-flex align-center justify-between">
<view class="d-flex align-center">
<uv-checkbox
:name="item.bikeCode"
/>
</view>
<uv-tags :text="appStore.translateDictValue(item.status, 'BIKE_STATUS')" size="mini" />
</view>
<view class="body">
<view class="icon">
<uv-icon
name="dianpingche"
custom-prefix="custom-icon"
size="34"
color="#1488f5"
/>
</view>
<view class="content fz-28">
<view class="brand">
车辆编号{{ item.bikeCode }}
</view>
<view class="brand">
运营商{{ operatorStore.translateOperatorId(item.operatorId, operatorAllList) }}
</view>
<view class="brand">
运营区域{{ item.regionName }}
</view>
</view>
</view>
</view>
</view>
</uv-checkbox-group>
</view>
<template #bottom>
<view class="bottom-button-zaping">
<uv-button :disabled="checkboxValue.length <= 0" type="primary" text="车辆下架" @click="vehicleHoisting" />
</view>
</template>
</z-paging>
<uv-modal ref="modalRef" title="车辆下架" confirm-text="下架" show-cancel-button :async-close="true" @confirm="submit">
<view class="tips">
当前选择下架的车辆{{ checkboxValue.join(",") }}
</view>
</uv-modal>
</view>
</template>
<style lang="scss" scoped>
.container {
.list-panel {
padding: 10px 5px 0px 0px;
width: 100%;
.list-item {
padding: 10px;
margin: 10px;
border-radius: 5px;
background-color: #fff;
box-shadow: 2px 2px 4px 2px rgba(212, 212, 212, 0.6);
.header {
width: 100%;
margin-bottom: 20rpx;
}
.body {
width: 100%;
display: flex;
align-items: center;
.content {
display: flex;
margin-left: 20rpx;
flex-direction: column;
justify-content: center;
padding-bottom: 10rpx;
.brand {
margin-bottom: 10rpx;
}
}
}
}
}
}
.tips {
width: 100%;
font-size: 28rpx;
margin-bottom: 20rpx;
}
</style>

View File

@ -0,0 +1,236 @@
<script lang="ts" setup>
import { batchAddInventoryAPI, getOperatoringAllListAPI, getUnInventoryListAPI } from '@/api/warehouse'
import { useAppStore, useOperatorStore } from '@/store'
definePage({
style: {
navigationBarTitleText: '整车仓库',
navigationBarBackgroundColor: '#1488f5',
navigationBarTextStyle: 'white',
},
})
const appStore = useAppStore()
const operatorStore = useOperatorStore()
const paging = ref<any>(null)
const list = ref<any>([])
const checkboxValue = ref<any>([])
const operatorAllList = ref<any>([])
const regionId_list = ref<any>([])
const modalRef = ref<any>(null)
const form = ref<any>(null)
const formData = reactive<any>({
regionId: '',
})
const rules = {
regionId: {
type: 'string',
required: true,
message: '请选择运营区域',
trigger: ['change'],
},
}
//
function refreshList() {
list.value = []
}
//
async function queryList(pageNo: number, pageSize: number) {
try {
uni.showLoading({
title: '加载中',
})
const { records } = await getUnInventoryListAPI({
pageNum: pageNo,
pageSize,
})
uni.hideLoading()
paging.value.complete(records)
}
catch (e) {
uni.hideLoading()
paging.value.complete(false)
}
}
//
function vehicleHoisting() {
modalRef.value.open()
}
//
async function getOperatoringAllList() {
try {
const res = await getOperatoringAllListAPI()
if (res && res.length > 0) {
regionId_list.value = res.map((item) => {
return {
text: item.regionName,
value: item.regionId,
}
})
}
}
catch (e) {
console.error(new Error('获取运营区'))
}
}
//
async function submit() {
try {
const status = await form.value.validate()
if (!status) {
return
}
uni.showLoading({
title: '正在提交中。。。',
})
const res = await batchAddInventoryAPI({
bikeCodes: checkboxValue.value,
regionId: formData.regionId,
})
uni.hideLoading()
modalRef.value.close()
modalRef.value.closeLoading()
paging.value.reload()
checkboxValue.value = []
form.value.resetFields()
}
catch (e) {
modalRef.value.closeLoading()
uni.hideLoading()
}
}
onMounted(async () => {
operatorAllList.value = await operatorStore.getOperatorList()
getOperatoringAllList()
})
</script>
<template>
<view>
<z-paging
ref="paging"
v-model="list"
:default-page-no="Number('1')"
:default-page-size="Number('10')"
:auto-show-back-to-top="Boolean(true)"
bg-color="#f3f3f3"
@query="queryList"
@refresher-touchend="refreshList"
>
<view class="container">
<uv-checkbox-group
v-model="checkboxValue"
size="20"
>
<view class="list-panel">
<view
v-for="(item) in list"
:key="item.bikeInfoId"
class="list-item"
>
<view class="header d-flex align-center justify-between">
<view class="d-flex align-center">
<uv-checkbox
:name="item.bikeCode"
/>
</view>
<uv-tags :text="appStore.translateDictValue(item.status, 'BIKE_STATUS')" type="warning" size="mini" />
</view>
<view class="body">
<view class="icon">
<uv-icon
name="dianpingche"
custom-prefix="custom-icon"
size="34"
color="#1488f5"
/>
</view>
<view class="content fz-28">
<view class="brand">
车辆编号{{ item.bikeCode }}
</view>
<view class="num">
运营商{{ operatorStore.translateOperatorId(item.operatorId, operatorAllList) }}
</view>
</view>
</view>
</view>
</view>
</uv-checkbox-group>
</view>
<template #bottom>
<view class="bottom-button-zaping">
<uv-button :disabled="checkboxValue.length <= 0" type="primary" text="车辆上架" @click="vehicleHoisting" />
</view>
</template>
</z-paging>
<uv-modal ref="modalRef" title="车辆上架" confirm-text="上架" show-cancel-button :async-close="true" @confirm="submit">
<view style="width: 100%;">
<uv-form ref="form" label-position="left" :model="formData" :rules="rules" label-width="100">
<uv-form-item label="车辆运营区:" prop="regionId" required>
<uni-data-select
v-model="formData.regionId"
:localdata="regionId_list"
placeholder="请选择"
clear
/>
</uv-form-item>
</uv-form>
</view>
</uv-modal>
</view>
</template>
<style lang="scss" scoped>
.container {
.list-panel {
padding: 10px 5px 0px 0px;
width: 100%;
.list-item {
padding: 10px;
margin: 10px;
border-radius: 5px;
background-color: #fff;
box-shadow: 2px 2px 4px 2px rgba(212, 212, 212, 0.6);
.header {
width: 100%;
margin-bottom: 20rpx;
}
.body {
width: 100%;
display: flex;
align-items: center;
.content {
display: flex;
margin-left: 20rpx;
flex-direction: column;
justify-content: center;
padding-bottom: 10rpx;
.brand {
margin-bottom: 10rpx;
}
}
}
}
}
}
.tips {
width: 100%;
font-size: 28rpx;
margin-bottom: 20rpx;
}
</style>

View File

@ -52,12 +52,12 @@ const rules = {
message: '请输入中控SN',
trigger: ['change', 'blur'],
},
batteryCode: {
type: 'string',
required: true,
message: '请输入电池编号',
trigger: ['change', 'blur'],
},
// batteryCode: {
// type: 'string',
// required: true,
// message: '',
// trigger: ['change', 'blur'],
// },
hasHelme: {
type: 'boolean',
required: true,
@ -191,7 +191,7 @@ getOperatorList()
</uni-section>
<!-- 电池 -->
<uni-section title="电池" type="line">
<uv-form-item label="电池" prop="batteryCode" required>
<uv-form-item label="电池" prop="batteryCode">
<uv-input v-model="formData.batteryCode" placeholder="请扫描或输入电池" border="surround">
<template #suffix>
<uv-icon name="scan" color="#cbcad0" size="28" @click="codeScan('batteryCode')" />

View File

@ -56,7 +56,7 @@ const btnList = ref([
{
key: 'bluetoothscan',
name: '蓝牙扫码',
path: '/pages/warehouse/bluetooth/bluetoothscan',
path: '/pages-sub/bluetooth/bluetoothscan/bluetoothscan',
customsrc: 'Bluetooth1',
},
{
@ -86,7 +86,7 @@ const btnList = ref([
{
key: 'ebikehouse',
name: '整车仓库',
path: '/pages/warehouse/ebikehouse/ebikehouse',
path: '/pages-sub/warehouse/vehicleWarehouse/vehicleWarehouse',
customsrc: 'wode-wodecheku',
},
{
@ -146,7 +146,7 @@ const btnList = ref([
{
key: 'vehicleoperation',
name: '运营车辆',
path: '/pages/warehouse/ebikehouse/vehicleoperation',
path: '/pages-sub/operation/carOperation/carOperation',
customsrc: 'daohang',
},
],

View File

@ -105,6 +105,27 @@ export const useAppStore = defineStore(
return list.length > 0 ? list[0].values : []
}
/**
* value值对字典数据进行翻译
* @param value
* @param code code
* @returns
*/
const translateDictValue = (value: any, code: string) => {
const dictData: any = CacheManager.get('dictData')
if (!dictData) {
return value
}
const list: dicType = dictData.filter((item: any) => item.dicCode === code)
if (list.length > 0) {
const values = list[0].values
if (values && values.length > 0) {
return values.find((item: any) => item.dicValueName === value.toString())?.dicValue || value
}
}
return value
}
return {
autoLogin,
loginJump,
@ -113,6 +134,7 @@ export const useAppStore = defineStore(
setUserInfo,
getUserInfo,
Logout,
translateDictValue,
}
},
)

View File

@ -42,7 +42,4 @@ export const useCenterControlStore = defineStore(
setCode,
}
},
{
persist: true,
},
)

View File

@ -1,27 +1,27 @@
/*
* UniApp 通用基础样式类 (基于 rpx)
* 命名规则:
* [p|m]-[t|r|b|l|a|h|v]-[size] => padding/margin - top/right/bottom/left/all/horizontal/vertical - size(rpx)
* [u][p|m]-[t|r|b|l|a|h|v]-[size] => padding/margin - top/right/bottom/left/all/horizontal/vertical - size(rpx)
* 示例: p-t-5 => padding-top: 5rpx;
*/
/* === Padding Classes === */
.p-a-0 {
.u-p-a-0 {
padding: 0;
}
.p-a-5 {
.u-p-a-5 {
padding: 5rpx;
}
.p-a-10 {
.u-p-a-10 {
padding: 10rpx;
}
.p-a-15 {
.u-p-a-15 {
padding: 15rpx;
}
.p-a-20 {
.u-p-a-20 {
padding: 20rpx;
}
.p-a-30 {
.u-p-a-30 {
padding: 30rpx;
}
.p-a-40 {
@ -235,10 +235,10 @@
.m-t-15 {
margin-top: 15rpx;
}
.m-t-20 {
.u-m-t-20 {
margin-top: 20rpx;
}
.m-t-30 {
.u-m-t-30 {
margin-top: 30rpx;
}
.m-t-40 {

View File

@ -9,3 +9,10 @@ $border-babyblue: #c5d0f0; //浅蓝色
//字体颜色
$font-blue: #5892e3; //蓝色
$font-gray: #999; //灰色
$font-black: #333; //黑色
$font-white: #fff; //白色
$font-red: #f00; //红色
$font-orange: #ffa500; //橙色
$font-green: #04b604; //绿色
$font-yellow: #ffff00; //黄色
$font-a1: #a1a1a1;

49
src/utils/aesTools.ts Normal file
View File

@ -0,0 +1,49 @@
import CryptoJS from 'crypto-js'
/**
* AES128ECBEncrypt
*/
export function AES128ECBEncrypt(inputText: any, key: any) {
const inputArr = []
while (inputText.length < 32) {
inputText += '0'
}
for (let i = 0; i < inputText.length / 2; i++) {
inputArr.push(Number.parseInt(inputText.substring(i * 2, i * 2 + 2), 16))
}
const inputParse = int8arrayParse(inputArr)
const keyParse = CryptoJS.enc.Utf8.parse(key)
const n = CryptoJS.AES.encrypt(inputParse, keyParse, {
iv: [],
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.NoPadding,
})
const w = int8arrayStringify(n.ciphertext)
const encryptStr = bufTohex(w.buffer)
return encryptStr
}
export function bufTohex(buffer) {
return Array.prototype.map.call(new Uint8Array(buffer), x => (`00${x.toString(16)}`).slice(-2)).join('')
}
export function int8arrayParse(e: any) {
const r = e.length
const i = []
let n = 0
for (; n < r; n++) {
i[n >>> 2] |= (255 & e[n]) << 24 - n % 4 * 8
}
return CryptoJS.lib.WordArray.create(i, r)
}
export function int8arrayStringify(t: any) {
const e = t.words
const r = t.sigBytes
const i = new Int8Array(r)
let n = 0
for (; n < r; n++) {
i[n] = e[n >>> 2] >> 24 - n % 4 * 8 & 255
}
return i
}

View File

@ -153,6 +153,11 @@ export default defineConfig(({ command, mode }) => {
__VITE_APP_PROXY__: JSON.stringify(VITE_APP_PROXY_ENABLE),
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/style/color.scss";`,
},
},
postcss: {
plugins: [
// autoprefixer({