概述
上节完成在嘟宝项目中使用高德api,如何在地图上显示gps位置。这节将使用Copilot演示在地图山画围栏、曲线。 嘟宝是为监督孩子身边环境而创建的一套应用,它能够实现后台驻留长连接,随时接收嘟妈信令,建立音视频通道点对点通信,而不需要中转服务器。嘟妈是一套WEB 应用,使用vue3编写,能够兼容app、小程序开发 。而嘟宝采用原生Andorid开发,能够获取最大权限,保持在后台运行。 目前为止,嘟宝完成的基本功能设计包括
- MQTT 基础通信
- 后台驻留
- SQLite数据存储
- 二维码显示身份识别码
- 开启自启动前台服务
- 任务栏消息发送与点击
- 高德定位实时更新
- webrtc点对点通信
- mAgentWeb控件封装解决全屏与获取视频权限问题 嘟妈采用vue3编写它的核心需求已完成包含以下部分:
- 完成登录注册页面
- 完成二维码扫描完成与嘟宝绑定
- 与嘟妈建立音视频通信
- 查看嘟宝桌面共享视频 项目地址在github上。 嘟宝地址 嘟妈地址
地图上划线
//详情如何引入使用高德api,参考上节
function polyline(gpsData: any) {
gpsData = [
[116.397428, 39.90923], // 起点:天安门
[116.401234, 39.912345],
[116.403322, 39.915119], // 故宫
[116.407654, 39.91789],
[116.410691, 39.902737], // 王府井
[116.413456, 39.905678],
[116.416789, 39.908901], // 终点
]
const polyline = new AMap.Polyline({
path: gpsData,
strokeColor: '#3366FF',
strokeWidth: 6,
strokeOpacity: 0.8,
})
map.value.add(polyline)
map.value.setFitView([polyline])
const startMarker = new AMap.Marker({
position: gpsData[0],
title: '起点',
label: {
content:
'<div style="background:#4CAF50;color:#fff;padding:2px 10px;border-radius:12px;">起点</div>',
direction: 'top',
},
})
map.value.add(startMarker)
const endMarker = new AMap.Marker({
position: gpsData[gpsData.length - 1],
title: '终点',
label: {
content:
'<div style="background:#F44336;color:#fff;padding:2px 10px;border-radius:12px;">终点</div>',
direction: 'top',
},
})
map.value.add(endMarker)
}
地图上画围栏
该功能,使用Copilot完成,
实现功能如下:
gaode.vue源码
<template>
<div class="map-page">
<button class="back-btn" type="button" @click="goBack">
<van-icon name="arrow-left" size="18" />
</button>
<button class="more-btn" type="button" @click="goMore">
<van-icon name="ellipsis" size="25" />
</button>
<div id="container"></div>
<van-action-sheet
v-model:show="showMenu"
:actions="menuActions"
cancel-text="取消"
@select="onMenuSelect"
/>
<van-popup v-model:show="showHistoryPicker" round position="bottom">
<div class="history-picker">
<div class="history-picker-title">选择历史时间</div>
<div class="history-picker-row">
<label class="history-picker-label">开始时间</label>
<input class="history-picker-input" type="datetime-local" v-model="historyStart" />
</div>
<div class="history-picker-row">
<label class="history-picker-label">结束时间</label>
<input class="history-picker-input" type="datetime-local" v-model="historyEnd" />
</div>
<div class="history-picker-actions">
<van-button size="small" plain type="default" @click="showHistoryPicker = false">
取消
</van-button>
<van-button size="small" type="primary" @click="onHistoryQuery"> 查询 </van-button>
</div>
</div>
</van-popup>
<van-popup v-model:show="showFencePicker" round position="bottom">
<div class="history-picker">
<div class="history-picker-title">设置电子围栏</div>
<div class="history-picker-row">
<label class="history-picker-label">半径(米)</label>
<input
class="history-picker-input"
type="range"
min="50"
max="2000"
step="10"
v-model.number="fenceRadius"
/>
<div style="width: 60px; text-align: right">{{ fenceRadius }} m</div>
</div>
<div class="history-picker-row">
<div style="color: #666; font-size: 13px">
点击地图设置围栏中心,或再次点击地图移动中心。
</div>
</div>
<div class="history-picker-actions">
<van-button
size="small"
plain
type="default"
@click="((showFencePicker = false), clearFenceOverlay())"
>
取消
</van-button>
<van-button size="small" type="primary" @click="saveFence"> 保存 </van-button>
</div>
</div>
</van-popup>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, shallowRef, ref, watchEffect } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import AMapLoader from '@amap/amap-jsapi-loader'
import { showToast } from 'vant'
import { c } from '@/myconfig'
import { useMQTTStore } from '@/store/mqtt'
import { useDubaoStore } from '@/store/dubao'
import 'vant/es/toast/style'
import { watch } from 'fs'
const mqtt = useMQTTStore()
const dubao = useDubaoStore()
const showMenu = ref(false)
const showHistoryPicker = ref(false)
const historyStart = ref(formatDateTimeLocal(new Date(Date.now() - 1000 * 60 * 60 * 24 * 2)))
const historyEnd = ref(formatDateTimeLocal(new Date()))
// geofence state (top-level)
const showFencePicker = ref(false)
const fenceCenter = ref<[number, number] | null>(null)
const fenceRadius = ref(200) // meters
let fenceMarker: any = null
let fenceCircle: any = null
const menuActions = [
{ name: '定位', icon: 'aim', action: 'site' },
{ name: '运动轨迹', icon: 'guide-o', action: 'sport' },
{ name: '历史位置', icon: 'underway-o', action: 'his' },
{ name: '电子围栏', icon: 'notes-o', action: 'fence' },
]
function onMenuSelect(action: any) {
showMenu.value = false
switch (action.action) {
case 'site':
mqtt.pushmsg(dubaoId as string, 'gps', JSON.stringify({ page: 1, rows: 1 }))
break
case 'sport':
let stop = Date.now()
let start = stop - 1000 * 60 * 60 * 24 * 1
mqtt.pushmsg(
dubaoId as string,
'gpssport',
JSON.stringify({ page: 1, rows: 100, start: start, stop: stop }),
)
break
case 'his':
showHistoryPicker.value = true
break
case 'fence':
showFencePicker.value = true
break
default:
break
}
}
function formatDateTimeLocal(date: Date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day}T${hour}:${minute}`
}
function onHistoryQuery() {
if (!historyStart.value || !historyEnd.value) {
showToast('请选择开始和结束时间')
return
}
const start = new Date(historyStart.value).getTime()
const stop = new Date(historyEnd.value).getTime()
if (Number.isNaN(start) || Number.isNaN(stop)) {
showToast('时间格式不正确')
return
}
if (start >= stop) {
showToast('结束时间必须晚于开始时间')
return
}
showHistoryPicker.value = false
console.log('查询历史轨迹:', start, stop)
mqtt.pushmsg(
dubaoId as string,
'gpssport',
JSON.stringify({ page: 1, rows: 100000, start, stop }),
)
}
// --- geofence helpers ---
function clearFenceOverlay() {
try {
if (fenceMarker) {
map.value.remove(fenceMarker)
fenceMarker = null
}
if (fenceCircle) {
map.value.remove(fenceCircle)
fenceCircle = null
}
} catch (e) {
console.error('clearFenceOverlay err', e)
}
}
function drawFence(center: [number, number], radius: number) {
if (!map.value || !AMap) return
clearFenceOverlay()
fenceMarker = new AMap.Marker({ position: center, title: '围栏中心' })
fenceCircle = new AMap.Circle({
center: center,
radius: radius,
strokeColor: '#FF5722',
strokeOpacity: 0.6,
strokeWeight: 2,
fillColor: 'rgba(255,87,34,0.15)',
})
map.value.add(fenceCircle)
map.value.add(fenceMarker)
map.value.setFitView([fenceCircle])
}
function saveFence() {
if (!fenceCenter.value) {
showToast('请先在地图上选择围栏中心点')
return
}
const [lng, lat] = fenceCenter.value
const data = { lng, lat, radius: fenceRadius.value }
console.log('保存围栏数据:', data)
mqtt.pushmsg(dubaoId as string, 'setfence', JSON.stringify(data))
showFencePicker.value = false
showToast('围栏已保存')
}
mqtt.$subscribe(async (mutate: any, state) => {
if (typeof mutate.events.newValue === 'boolean') {
return
}
let data = JSON.parse(state.datamsg)
let msg = JSON.parse(data.msg)
switch (msg.code) {
case 'gps':
const gpsData = JSON.parse(msg.data)
maker(gpsData[0])
break
case 'gpssport':
console.log('gpssport', msg.data)
if (!msg.data || msg.data === '[]') {
showToast('没有运动轨迹数据')
console.log('没有运动轨迹数据')
return
}
const gpssportData = JSON.parse(msg.data)
polylinesport(gpssportData)
break
default:
break
}
})
;(window as any)._AMapSecurityConfig = {
securityJsCode: c.gaodesavekey,
}
let AMap: any
const map = shallowRef<any>(null) // 方式1:使用 any
const router = useRouter()
const route = useRoute()
const dubaoId = route.params.dubaoId
const goBack = () => {
router.back()
}
const goMore = () => {
showMenu.value = true
}
watchEffect(() => {
console.log('fenceCenter changed:', fenceCenter.value)
drawFence(fenceCenter.value as [number, number], fenceRadius.value)
})
onMounted(() => {
AMapLoader.load({
key: c.gaodekey,
version: '2.0',
plugins: ['AMap.ToolBar', 'AMap.Scale'],
})
.then((AMap2) => {
AMap = AMap2
map.value = new AMap.Map('container', {
viewMode: '2D',
zoom: 14,
center: [116.397428, 39.90923],
})
map?.value.addControl(new AMap.ToolBar())
map?.value.addControl(new AMap.Scale())
// map click: set fence center when fence picker open
map.value.on('click', (e: any) => {
console.log('Fence center set to:', e)
// if (!showFencePicker.value) return
const lnglat = e.lnglat || e.latlng
const center: [number, number] = [lnglat.lng, lnglat.lat]
fenceCenter.value = center
drawFence(center, fenceRadius.value)
console.log('Fence center set to:', center)
})
let stop = Date.now()
let start = stop - 1000 * 60 * 60 * 24 * 7
mqtt.pushmsg(
dubaoId as string,
'gps',
JSON.stringify({ page: 1, rows: 1, start: start, stop: stop }),
)
// maker({ lng: 117.397428, lat: 39.90923, name: '嘟宝' })
})
.catch((e) => {
console.error('地图加载失败:', e)
})
})
onUnmounted(() => {
map.value?.destroy()
})
function maker(gps: any) {
if (!map.value) return
let name = dubao.getDuBao(dubaoId as string)?.dubaoName || '嘟宝'
const marker = new AMap.Marker({
position: new AMap.LngLat(gps.lng, gps.lat),
title: name,
animation: 'AMAP_ANIMATION_DROP', // 落点动画
})
marker.on('click', (e: any) => {
const infoWindow = new AMap.InfoWindow({
content: `<div style="padding: 5px;">${name}</div>`,
offset: new AMap.Pixel(0, -30),
})
infoWindow.open(map.value, marker.getPosition())
console.log('Marker clicked:', name, e)
})
// 添加到地图
map.value.add(marker)
map.value.setCenter(marker.getPosition())
}
function polyline(gpsData: any) {
gpsData = [
[116.397428, 39.90923], // 起点:天安门
[116.401234, 39.912345],
[116.403322, 39.915119], // 故宫
[116.407654, 39.91789],
[116.410691, 39.902737], // 王府井
[116.413456, 39.905678],
[116.416789, 39.908901], // 终点
]
const polyline = new AMap.Polyline({
path: gpsData,
strokeColor: '#3366FF',
strokeWidth: 6,
strokeOpacity: 0.8,
})
map.value.add(polyline)
map.value.setFitView([polyline])
const startMarker = new AMap.Marker({
position: gpsData[0],
title: '起点',
label: {
content:
'<div style="background:#4CAF50;color:#fff;padding:2px 10px;border-radius:12px;">起点</div>',
direction: 'top',
},
})
map.value.add(startMarker)
const endMarker = new AMap.Marker({
position: gpsData[gpsData.length - 1],
title: '终点',
label: {
content:
'<div style="background:#F44336;color:#fff;padding:2px 10px;border-radius:12px;">终点</div>',
direction: 'top',
},
})
map.value.add(endMarker)
}
function polylinesport(gpsData: any) {
console.log('polylinesport', gpsData)
let gpsDataArray: any[] = []
for (let i = 0; i < gpsData.length; i++) {
gpsDataArray[i] = [gpsData[i].lng, gpsData[i].lat]
}
const polyline = new AMap.Polyline({
path: gpsDataArray,
strokeColor: '#3366FF',
strokeWidth: 6,
strokeOpacity: 0.8,
})
map.value.add(polyline)
map.value.setFitView([polyline])
const startMarker = new AMap.Marker({
position: gpsDataArray[gpsDataArray.length - 1],
title: '起点',
label: {
content:
'<div style="background:#4CAF50;color:#fff;padding:2px 10px;border-radius:12px;">起点</div>',
direction: 'top',
},
})
map.value.add(startMarker)
const endMarker = new AMap.Marker({
position: gpsDataArray[0],
title: '终点',
label: {
content:
'<div style="background:#F44336;color:#fff;padding:2px 10px;border-radius:12px;">终点</div>',
direction: 'top',
},
})
map.value.add(endMarker)
}
</script>
<style scoped>
.map-page {
position: relative;
width: 100%;
height: 100%;
}
#container {
width: 100%;
height: 100%;
}
.back-btn {
position: absolute;
top: 12px;
left: 12px;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
color: #333;
}
.more-btn {
position: absolute;
top: 12px;
right: 12px;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
background: rgba(255, 255, 255, 0.95);
/* background: transparent; */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
color: #333;
}
.history-picker {
padding: 16px 16px 20px;
}
.history-picker-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
}
.history-picker-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.history-picker-label {
font-size: 14px;
color: #333;
}
.history-picker-input {
width: 60%;
border: 1px solid #e5e5e5;
border-radius: 8px;
padding: 10px 12px;
font-size: 14px;
}
.history-picker-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 16px;
}
</style>
引言:AI辅助开发的演进之路
- 从代码补全到智能助手:Copilot如何改变编程习惯
- 从助手到伙伴:Agent带来的范式转变
- 本文主旨:分享个人开发工作流被AI重塑的亲身经历
第一部分:Copilot时代——效率的第一次飞跃
1.1 从IntelliSense到GitHub Copilot
- 传统IDE智能提示的局限性
- Copilot带来的上下文感知代码生成
- 实际案例:如何用Copilot加速日常开发
1.2 Copilot工作流整合
- VS Code中的无缝集成体验
- 代码审查与重构的辅助
- 学习曲线与适应过程
1.3 Copilot的局限与瓶颈
- 上下文窗口的限制
- 复杂业务逻辑的挑战
- 对开发者思维模式的依赖
第二部分:Agent登场——从工具到合作伙伴
2.1 什么是AI Agent?
- Agent与Copilot的本质区别
- 自主决策与任务分解能力
- 记忆与学习机制
2.2 主流开发Agent体验
- Cursor Agent模式深度体验
- Windsurf的智能工作流
- 其他开发Agent工具对比
2.3 Agent如何重构开发流程
- 需求分析到代码实现的闭环
- 自动化测试与调试
- 文档生成与知识管理
第三部分:我的工作流颠覆实录
3.1 需求阶段:从模糊描述到清晰规划
- 自然语言需求直接转化为技术方案
- Agent协助的技术选型与架构设计
- 风险评估与备选方案生成
3.2 开发阶段:从写代码到指导代码
- 角色转变:从执行者到架构师
- Agent驱动的迭代开发模式
- 实时代码审查与优化建议
3.3 测试与部署:自动化程度的质变
- 智能测试用例生成
- 性能分析与优化建议
- 部署配置与监控设置
第四部分:技术深度解析
4.1 底层技术对比
- Copilot:基于大型代码库的补全模型
- Agent:多模态理解与规划能力
- RAG与工具调用机制
4.2 实际效能数据
- 开发速度提升对比
- 代码质量指标变化
- Bug率与维护成本统计
4.3 集成开发环境演进
- IDE插件的智能化升级
- 命令行工具的AI增强
- 团队协作平台的变化
第五部分:挑战与应对策略
5.1 技术挑战
- 上下文管理的复杂性
- 幻觉问题与准确性保障
- 私有代码的安全考量
5.2 工作习惯调整
- 思维模式的转变过程
- 与Agent的有效协作技巧
- 保持技术深度的平衡
5.3 团队协作变革
- 代码审查流程的重构
- 知识共享方式的进化
- 项目管理工具的适配
第六部分:未来展望
6.1 技术发展趋势
- 多Agent协作系统
- 领域专用Agent的兴起
- 硬件加速与边缘部署
6.2 开发者角色演变
- 从编码到“导演”
- 架构设计能力的强化
- 业务理解深度的要求
6.3 给开发者的建议
- 如何开始Agent之旅
- 技能栈的调整方向
- 保持竞争力的学习路径
结语:拥抱变革,重塑价值
- AI不是替代,而是增强
- 开发工作流的持续进化
- 在技术浪潮中定位自己的独特价值