手机辅助定位

This commit is contained in:
磷叶 2025-05-13 11:14:07 +08:00
parent a4807d524a
commit d8b8dde7b6
7 changed files with 245 additions and 82 deletions

View File

@ -1,4 +1,5 @@
// 视图
import { plusHours } from '@/utils/date';
import { getLastDate, getLastDateTimeEnd, getLastDateTimeStart, getLastMonth, getLastMonthTimeEnd } from '@/utils/index';
export const views = {
@ -88,6 +89,49 @@ export const DatePickerOptions = {
}
}
export const DateTimePickerOptions = {
// 默认
DEFAULT: {
shortcuts: [{
text: '1小时内',
onClick(picker) {
const end = new Date();
const start = plusHours(end, -1);
picker.$emit('pick', [start, end]);
}
}, {
text: '3小时内',
onClick(picker) {
const end = new Date();
const start = plusHours(end, -3);
picker.$emit('pick', [start, end]);
}
}, {
text: '6小时内',
onClick(picker) {
const end = new Date();
const start = plusHours(end, -6);
picker.$emit('pick', [start, end]);
}
}, {
text: '12小时内',
onClick(picker) {
const end = new Date();
const start = plusHours(end, -12);
picker.$emit('pick', [start, end]);
}
},
{
text: '24小时内',
onClick(picker) {
const end = new Date();
const start = plusHours(end, -24);
picker.$emit('pick', [start, end]);
}
}]
},
}
// 进度颜色
export const ProgressColors = [
{color: '#5cb87a', percentage: 90},

View File

@ -114,3 +114,14 @@ export function toDescriptionFromSecond(seconds) {
desc.text += desc.second + ' 秒';
return desc;
}
/**
* 增加小时
* @param {*} date
* @param {*} hours
* @returns
*/
export function plusHours(date, hours) {
return new Date(date.getTime() + hours * 3600 * 1000);
}

View File

@ -9,26 +9,30 @@
<span class="label">位置</span>
<span class="value">{{currentLog.longitude | dv}},{{currentLog.latitude | dv}}</span>
</el-col>
<el-col :span="12" class="info-item">
<span class="label">定位类型</span>
<span class="value"><dict-tag :options="dict.type.device_location_type" :value="currentLog.type" size="mini"/></span>
</el-col>
<el-col :span="12" class="info-item">
<span class="label">状态</span>
<span class="value"><dict-tag :options="dict.type.device_status" :value="currentLog.status" size="mini"/></span>
</el-col>
<el-col :span="12" class="info-item">
<span class="label">速度</span>
<span class="value">{{ currentLog.speed }} KM/h</span>
<span class="value">{{ currentLog.speed | dv}} KM/H</span>
</el-col>
<el-col :span="12" class="info-item">
<span class="label">电压</span>
<span class="value">{{ currentLog.voltage | dv}} V</span>
</el-col>
<el-col :span="12" class="info-item">
<span class="label">信号</span>
<span class="label">4G</span>
<span class="value">{{ currentLog.signal | dv}}</span>
</el-col>
<el-col :span="12" class="info-item">
<span class="label">卫星</span>
<span class="value">{{ currentLog.satellites | dv}}</span>
</el-col>
<el-col :span="12" class="info-item">
<span class="label">状态</span>
<span class="value"><dict-tag :options="dict.type.device_status" :value="currentLog.status" size="mini"/></span>
</el-col>
<el-col :span="12" class="info-item">
<span class="label">锁状态</span>
<span class="value"><dict-tag :options="dict.type.device_lock_status" :value="currentLog.lockStatus" size="mini"/></span>
@ -69,7 +73,7 @@
<script>
export default {
name: 'PlaybackPanel',
dicts: ['device_status', 'device_lock_status', 'device_quality'],
dicts: ['device_status', 'device_lock_status', 'device_quality', 'device_location_type'],
props: {
currentLog: {
type: Object,

View File

@ -11,21 +11,26 @@
/>
</el-col>
<el-col :span="6">
<el-date-picker
v-if="queryParams.timeRange != null && queryParams.timeRange[0] != null && queryParams.timeRange[1] != null"
v-model="queryParams.timeRange"
value-format="yyyy-MM-dd HH:mm:ss"
type="datetimerange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
size="mini"
:clearable="false"
@change="getLocationLogList"
style="width: 100%;"
/>
<el-row type="flex">
<el-date-picker
v-if="queryParams.timeRange != null && queryParams.timeRange[0] != null && queryParams.timeRange[1] != null"
v-model="queryParams.timeRange"
value-format="yyyy-MM-dd HH:mm:ss"
type="datetimerange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
size="mini"
:clearable="false"
@change="getLocationLogList"
:default-time="['00:00:00', '23:59:59']"
style="flex: 1;"
:picker-options="DateTimePickerOptions.DEFAULT"
/>
<el-button icon="el-icon-refresh" size="mini" @click="getLocationLogList">刷新</el-button>
</el-row>
<div class="location-log-list">
<el-collapse v-model="activeGroup" accordion>
<el-collapse v-model="activeGroup" accordion @change="handleCollapseChange">
<el-collapse-item
v-for="(group, groupIndex) in groupedLocationLogs"
:key="groupIndex"
@ -35,18 +40,19 @@
<div class="group-header">
<span class="group-time">{{ group.startTime }} - {{ group.endTime }}</span>
<dict-tag :options="dict.type.device_status" :value="group.status" size="mini"/>
<span class="group-count">({{ group.logs.length }})</span>
<span class="group-count">({{ group.totalCount }})</span>
</div>
</template>
<div class="group-content">
<div class="group-content" v-if="group.isLoaded">
<div
v-for="(log, logIndex) in group.logs"
v-for="(log, logIndex) in group.displayLogs"
:key="logIndex"
@click="handleLogClick(log, getLogIndex(groupIndex, logIndex))"
class="log-item"
>
<div class="log-time">
<dict-tag :options="dict.type.device_status" :value="log.status" size="mini"/>
<dict-tag :options="dict.type.device_location_type" :value="log.type" size="mini"/>
{{ log.at }}
</div>
<div class="log-info">
@ -61,6 +67,12 @@
<span class="info-item">{{ log.sn }}</span>
</div>
</div>
<div v-if="group.hasMore" class="load-more" @click="loadMoreLogs(groupIndex)">
<el-button type="text" size="small">加载更多</el-button>
</div>
</div>
<div v-else class="loading-placeholder">
<el-skeleton :rows="3" animated />
</div>
</el-collapse-item>
</el-collapse>
@ -71,15 +83,16 @@
</template>
<script>
import AreaMap from '@/views/bst/areaSub/components/AreaMap.vue';
import { getArea } from '@/api/bst/area';
import { listAreaSubByAreaId } from '@/api/bst/areaSub';
import { listAllLocation } from '@/api/bst/locationLog';
import { getArea } from '@/api/bst/area';
import { DateTimePickerOptions } from '@/utils/constants';
import { parseTime } from '@/utils/ruoyi.js';
import AreaMap from '@/views/bst/areaSub/components/AreaMap.vue';
export default {
name: 'DeviceLocation',
dicts: ['device_status', 'device_lock_status', 'device_quality'],
dicts: ['device_status', 'device_lock_status', 'device_quality', 'device_location_type'],
components: {
AreaMap
},
@ -95,49 +108,24 @@ export default {
},
data() {
return {
DateTimePickerOptions,
area: {},
areaSubList: [],
locationLogList: [],
processedGroups: [], //
queryParams: {
orderByColumn: "at",
isAsc: "asc",
timeRange: [parseTime(new Date(), '{y}-{m}-{d} 00:00:00'), parseTime(new Date(), '{y}-{m}-{d} 23:59:59')]
},
loading: false,
activeGroup: '', //
activeGroup: '',
pageSize: 20,
}
},
computed: {
groupedLocationLogs() {
if (!this.locationLogList || this.locationLogList.length === 0) {
return [];
}
const groups = [];
let currentGroup = {
status: this.locationLogList[0].status,
logs: [this.locationLogList[0]],
startTime: this.locationLogList[0].at,
endTime: this.locationLogList[0].at
};
for (let i = 1; i < this.locationLogList.length; i++) {
const currentLog = this.locationLogList[i];
if (currentLog.status === currentGroup.status) {
currentGroup.logs.push(currentLog);
currentGroup.endTime = currentLog.at;
} else {
groups.push(currentGroup);
currentGroup = {
status: currentLog.status,
logs: [currentLog],
startTime: currentLog.at,
endTime: currentLog.at
};
}
}
groups.push(currentGroup);
return groups;
return this.processedGroups;
}
},
created() {
@ -172,6 +160,7 @@ export default {
this.loading = true;
listAllLocation(this.queryParams).then(res => {
this.locationLogList = res.data;
this.processLocationLogs(); //
if (this.$refs.map) {
this.$nextTick(() => {
this.$refs.map.initPlayback();
@ -181,6 +170,107 @@ export default {
this.loading = false;
});
},
//
processLocationLogs() {
if (!this.locationLogList || this.locationLogList.length === 0) {
this.processedGroups = [];
return;
}
const groups = [];
let currentGroup = {
status: this.locationLogList[0].status,
startTime: this.locationLogList[0].at,
endTime: this.locationLogList[0].at,
totalCount: 1,
isLoaded: true, //
displayLogs: [this.locationLogList[0]], //
allLogs: [this.locationLogList[0]],
hasMore: false,
currentPage: 1
};
for (let i = 1; i < this.locationLogList.length; i++) {
const log = this.locationLogList[i];
if (log.status !== currentGroup.status) {
//
this.initializeGroupData(currentGroup);
groups.push(currentGroup);
//
currentGroup = {
status: log.status,
startTime: log.at,
endTime: log.at,
totalCount: 1,
isLoaded: true,
displayLogs: [log],
allLogs: [log],
hasMore: false,
currentPage: 1
};
} else {
//
currentGroup.endTime = log.at;
currentGroup.totalCount++;
currentGroup.allLogs.push(log);
//
if (currentGroup.totalCount <= this.pageSize) {
currentGroup.displayLogs.push(log);
}
}
}
//
this.initializeGroupData(currentGroup);
groups.push(currentGroup);
this.processedGroups = groups;
},
//
initializeGroupData(group) {
//
group.hasMore = group.allLogs.length > this.pageSize;
if (!group.hasMore) {
group.displayLogs = [...group.allLogs];
} else {
group.displayLogs = group.allLogs.slice(0, this.pageSize);
}
},
//
async loadMoreLogs(groupIndex) {
const group = this.processedGroups[groupIndex];
if (!group || !group.hasMore) return;
const start = group.currentPage * this.pageSize;
const end = start + this.pageSize;
group.currentPage++;
group.displayLogs = [...group.displayLogs, ...group.allLogs.slice(start, end)];
group.hasMore = group.allLogs.length > end;
this.$set(this.processedGroups, groupIndex, { ...group });
},
//
handleCollapseChange() {
//
},
// getLogIndex
getLogIndex(groupIndex, logIndex) {
let totalIndex = 0;
for (let i = 0; i < groupIndex; i++) {
totalIndex += this.processedGroups[i].totalCount;
}
const group = this.processedGroups[groupIndex];
if (!group || !group.displayLogs[logIndex]) return totalIndex;
return totalIndex + logIndex;
},
//
handleLogClick(log, index) {
if (this.$refs.map) {
@ -190,13 +280,6 @@ export default {
});
}
},
getLogIndex(groupIndex, logIndex) {
let totalIndex = 0;
for (let i = 0; i < groupIndex; i++) {
totalIndex += this.groupedLocationLogs[i].logs.length;
}
return totalIndex + logIndex;
},
getStatusType(status) {
const statusMap = {
0: 'info', // 线
@ -307,5 +390,19 @@ export default {
&::-webkit-scrollbar-track {
background: #F5F7FA;
}
.loading-placeholder {
padding: 16px;
}
.load-more {
text-align: center;
padding: 8px;
border-top: 1px solid #EBEEF5;
&:hover {
background-color: #F5F7FA;
}
}
}
</style>

View File

@ -88,7 +88,7 @@
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="locationLogList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="onSortChange">
<el-table size="mini" v-loading="loading" :data="locationLogList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="onSortChange">
<el-table-column type="selection" width="55" align="center" />
<template v-for="column of showColumns">
<el-table-column
@ -107,24 +107,25 @@
{{d.row[column.key]}}
</template>
<template v-else-if="column.key === 'status'">
<dict-tag :options="dict.type.device_status" :value="d.row[column.key]"/>
<dict-tag :options="dict.type.device_status" :value="d.row[column.key]" size="mini"/>
</template>
<template v-else-if="column.key === 'lockStatus'">
<dict-tag :options="dict.type.device_lock_status" :value="d.row[column.key]"/>
<dict-tag :options="dict.type.device_lock_status" :value="d.row[column.key]" size="mini"/>
</template>
<template v-else-if="column.key === 'quality'">
<dict-tag :options="dict.type.device_quality" :value="d.row[column.key]"/>
<dict-tag :options="dict.type.device_quality" :value="d.row[column.key]" size="mini"/>
</template>
<template v-else-if="column.key === 'iotStatus'">
<dict-tag :options="dict.type.device_iot_status" :value="d.row[column.key]"/>
<dict-tag :options="dict.type.device_iot_status" :value="d.row[column.key]" size="mini"/>
</template>
<template v-else-if="column.key === 'voltage'">
{{d.row[column.key] | fix2 | dv}} V
</template>
<template v-else-if="column.key === 'location'">
<el-link type="primary" size="mini" @click="handleLocation(d.row)">
{{d.row.longitude}}<br/>{{d.row.latitude}}
<el-link type="primary" style="font-size: 12px;" @click="handleLocation(d.row)">
{{d.row.longitude}},{{d.row.latitude}}
</el-link>
<dict-tag :options="dict.type.device_location_type" :value="d.row.type" size="mini" style="margin-left: 4px;"/>
</template>
<template v-else>
{{d.row[column.key]}}
@ -170,9 +171,9 @@
</template>
<script>
import { listLocationLog, delLocationLog} from "@/api/bst/locationLog";
import { $showColumns } from '@/utils/mixins';
import { delLocationLog, listLocationLog } from "@/api/bst/locationLog";
import FormCol from "@/components/FormCol/index.vue";
import { $showColumns } from '@/utils/mixins';
import LocationLogViewDialog from '@/views/bst/locationLog/components/LocationLogViewDialog.vue';
//
@ -184,7 +185,7 @@ const defaultSort = {
export default {
name: "LocationLog",
mixins: [$showColumns],
dicts: ['device_status', 'device_lock_status', 'device_iot_status', 'device_quality'],
dicts: ['device_status', 'device_lock_status', 'device_iot_status', 'device_quality', 'device_location_type'],
components: {FormCol, LocationLogViewDialog},
data() {
return {
@ -194,8 +195,8 @@ export default {
{key: 'id', visible: false, label: 'ID', minWidth: null, sortable: true, overflow: false, align: 'center', width: "80"},
{key: 'mac', visible: true, label: 'MAC', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'sn', visible: true, label: 'SN', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'location', visible: true, label: '定位', minWidth: null, sortable: false, overflow: false, align: 'center', width: null},
{key: 'at', visible: true, label: '消息时间', minWidth: null, sortable: true, overflow: false, align: 'center', width: "100"},
{key: 'location', visible: true, label: '定位', minWidth: "200", sortable: false, overflow: false, align: 'left', width: null},
{key: 'at', visible: true, label: '消息时间', minWidth: null, sortable: true, overflow: false, align: 'center', width: "160"},
{key: 'status', visible: true, label: '状态', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'iotStatus', visible: true, label: '电动车', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'lockStatus', visible: true, label: '锁', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},

View File

@ -145,9 +145,10 @@
<order-link :id="d.row.id" :text="d.row.no" size="mini"/>
</template>
<template v-else-if="column.key === 'suitName'">
{{d.row.suitName | dv}}<br/>
<dict-tag :options="dict.type.order_status" :value="d.row.status" size="mini"/>
{{d.row.suitName | dv}}
<br/>
<dict-tag :options="dict.type.suit_type" :value="d.row.suitType" size="mini"/>
<dict-tag :options="dict.type.order_status" :value="d.row.status" size="mini" style="margin-left: 4px;"/>
<dict-tag :options="dict.type.suit_riding_rule" :value="d.row.suitRidingRule" size="mini" style="margin-left: 4px;"/>
</template>
<template v-else-if="column.key === 'returnType'">
@ -193,7 +194,10 @@
<div v-if="d.row.endTime != null">结束{{d.row.endTime | dv}}</div>
</template>
<template v-else-if="column.key === 'device'">
<div v-if="d.row.deviceSn != null">SN<device-link :id="d.row.deviceId" :text="d.row.deviceSn" size="mini"/></div>
<div>
SN<device-link :id="d.row.deviceId" :text="d.row.deviceSn" size="mini"/>
<dict-tag :options="dict.type.device_status" :value="d.row.deviceStatus" size="mini" style="margin-left: 4px;"/>
</div>
<div v-if="d.row.deviceMac != null">MAC<device-link :id="d.row.deviceId" :text="d.row.deviceMac" size="mini"/></div>
<div v-if="d.row.deviceVehicleNum != null">车牌<device-link :id="d.row.deviceId" :text="d.row.deviceVehicleNum" size="mini"/></div>
</template>
@ -205,7 +209,7 @@
<template v-else-if="column.key === 'use'">
<div v-if="d.row.duration != null">
<i class="el-icon-timer" />
{{toDescriptionFromSecond(d.row.duration).text | dv}}
{{toDescriptionFromSecond(getOrderDuration(d.row)).text | dv}}
</div>
<div v-if="d.row.distance != null">
<i class="el-icon-position" />
@ -297,6 +301,7 @@ import { OrderStatus } from "@/utils/enums";
import { $showColumns } from '@/utils/mixins';
import OrderRefundDialog from "@/views/bst/order/components/OrderRefundDialog.vue";
import OrderVerifyDialog from "@/views/bst/order/components/OrderVerifyDialog.vue";
import { getOrderDuration } from '@/views/bst/order/util.js';
//
const defaultSort = {
@ -307,7 +312,7 @@ const defaultSort = {
export default {
name: "Order",
mixins: [$showColumns],
dicts: ['order_status', 'suit_type', 'order_return_type', 'order_return_mode', 'suit_rental_unit', 'suit_riding_rule'],
dicts: ['order_status', 'suit_type', 'order_return_type', 'order_return_mode', 'suit_rental_unit', 'suit_riding_rule', 'device_status'],
components: {FormCol, OrderRefundDialog, OrderVerifyDialog, UserLink, DeviceLink, OrderLink, AreaLink, AreaRemoteSelect},
props: {
query: {
@ -388,6 +393,7 @@ export default {
this.getList();
},
methods: {
getOrderDuration,
toDescriptionFromSecond,
handleVerify(row) {
this.row = row;

View File

@ -38,7 +38,7 @@ module.exports = {
[process.env.VUE_APP_BASE_API]: {
target: `http://localhost:4101`,
// target: `https://ele.ccttiot.com/prod-api`,
changeOrigin: true,
// changeOrigin: true,
pathRewrite: {
['^' + process.env.VUE_APP_BASE_API]: ''
}