vue3 引入高德地图实例

vue3高德地图进阶版

地图引入进阶
文章末尾有完整代码
关于vue项目如何引入高德地图以及一些基础用法见上一篇文章
📙:vue3引入高德地图

`



前言

深入了解下项目中需要用到的高德地图的一些功能用法。


1.海量标注

  高德地图的海量标注主要是指在高德地图数据集中对各种地理信息进行标注的过程。这些标注可以用于训练和优化高德地图的各种功能和服务,例如导航、位置推荐、POI搜索等。

高德地图的海量标注主要包括以下几个方面:

  1. 道路标注:对道路、桥梁、隧道等交通设施进行标注,以便用户在导航时能够准确地找到目的地。

  2. 建筑物标注:对各种建筑物、商场、酒店等进行标注,以便用户在搜索地点或查询周边服务时能够得到更准确的结果。

  3. 地形标注:对地形、山川、河流等自然景观进行标注,以便用户在查看地图时能够更好地了解周围环境。

  4. POI标注:对各种兴趣点(Point of Interest,简称POI)进行标注,例如公园、景点、餐厅、商店等,以便用户在搜索地点或查询周边服务时能够得到更准确的结果。

实际应用

创建地图-设置图标与文字-创建LabelMarker实例-创建图层-点标记添加到图层-图层添加到地图-标记添加事件:

// 1.创建地图
const map = new AMap.Map('container',{
    zoom: 10,  //设置地图显示的缩放级别
    center: [116.397428, 39.90923],  //设置地图中心点坐标
    mapStyle: 'amap://styles/whitesmoke',  //设置地图的显示样式
    viewMode: '2D'  //设置地图模式
});
// 2.设置图标对象
const icon = {
   type: 'image', // 图标类型,现阶段只支持 image 类型
   image: 'https://a.amap.com/jsapi_demos/static/demo-center/marker/express2.png', // 图片 url
   size: [64, 30],// 图片尺寸
   anchor: 'center', // 图片相对 position 的锚点,默认为 bottom-center 
};
// 3.设置文字对象
const text = {
    content: '中邮速递易', // 要展示的文字内容
    direction: 'right', // 文字方向,有 icon 时为围绕文字的方向,没有 icon 时,则为相对 position 的位置
    offset: [-20, -5], // 在 direction 基础上的偏移量
    style: { // 文字样式       
        fontSize: 12 ,// 字体大小        
        fillColor: '#22886f', // 字体颜色  
        strokeColor: '#fff', // 描边颜色
        strokeWidth: 2,  // 描边宽度
    }
}
// 4.创建 LabelMarker 实例
const labelMarker = new AMap.LabelMarker({
    name: '标注2', // 此属性非绘制文字内容,仅最为标识使用
    position: [116.466994, 39.984904],
    zIndex: 16,
    icon: icon, // 将第一步创建的 icon 对象传给 icon 属性
    text: text,// 将第二步创建的 text 对象传给 text 属性
});
/** 5.创建 LabelsLayer 图层 (LabelsLayer 图层是承载 LabelMarker 的图层,
所有创建的 LabelMarker 需要添加到 LabelsLayer 图层上才能最终展示在地图上。) */
const labelsLayer = new AMap.LabelsLayer({
    zooms: [3, 20],
    zIndex: 1000,
    collision: true,  // 该层内标注是否避让
    allowCollision: true, // 设置 allowCollision:true,可以让标注避让用户的标注
});
// 6.将 LabelMarker 添加到 LabelsLayer 上
// 添加一个 labelMarker
labelsLayer.add(labelsMarker);
 // 批量添加 labelMarker
 labelsLayer.add([labelsMarker1, labelsMarker2])
 // 移除 LabelMarker
 labelsLayer.remove(labelsMarker)
 // 批量移除 labelMarker
 labelsLayer.remove([labelsMarker1, labelsMarker2])
 // 7.将 LabelsLayer 添加到地图实例
 map.add(labelsLayer);
// 8.为LabelMarker 添加事件
labelMarker.on('click', function(e){
    labelMarker.setOpacity(0.5);
});

2.自定义图层

自定义图层允许用户根据自己的需求创建和管理自己的地图图层。

实际应用:

自定义图层使用 AMap.CustomLayer 类来进行构造,构造函数接受两个参数,第一个参数是作为图层的 dom
画布,第二个参数是图层的相关属性设定,与通用图层属性相同:
具体步骤为
1.创建自定义图层
2.自定义渲染方法

// 1.创建一个自定义图层
// 创建 canvas
var canvas = document.createElement('canvas');

// 将 canvas 宽高设置为地图实例的宽高
canvas.width = map.getSize().width;
canvas.height = map.getSize().height;

// 创建一个自定义图层
var customLayer = new AMap.CustomLayer(canvas, {
  zIndex: 12,
  zooms: [3, 18] // 设置可见级别,[最小级别,最大级别]
});

// 将自定义图层添加到地图 
map.add(customLayer);
// 2. 自定义渲染方法
/**可使用 render 方法自定义图层渲染,开发者应该更新绘制时使用的容器内像素位置来重新绘制图层内容,像素位置是由经纬度坐标转换而来,通常使用 map. lnglatToContainer 方法进行转换
提示
render 方法在自定义图层初次绘制、地图移动与缩放结束时调用。
*/
// 使用 canvas 在地图中心点绘制一个圆形
customLayer.render = () => {
  // 获取地图中心点位置
  var center = map.getCenter()
  var pos = map.lngLatToContainer(center);
  var r = 20;
  
  // 使用 canvas 绘制圆形
  var ctx = canvas.getContext("2d");
  ctx.fillStyle = '#08f';
  ctx.strokeStyle = '#fff';
  ctx.beginPath();
  ctx.moveTo(pos.x + r, pos.y)
  ctx.arc(pos.x, pos.y, r, 0, 2 * Math.PI);
  ctx.lineWidth = 3
  ctx.closePath();
  ctx.stroke();
  ctx.fill();
}

3. 其他坐标转高德坐标

  高德地图使用的坐标系为WGS84(World Geodetic System 1984),该坐标系的坐标轴与地球的经纬度坐标系相同。

使用不同坐标系前,我们需要使用 AMap.convertFrom() 方法将这些非高德坐标系进行转换。

实际应用:

var gps = [116.3, 39.9]; // 需要转换的gps类型的坐标

// 参数说明:需要转换的坐标,需要转换的坐标类型,转换成功后的回调函数
AMap.convertFrom(gps, 'gps', function (status, result) {
  if (result.info === 'ok') {
    var lnglats = result.locations; // 转换后的高德坐标 Array.<LngLat> 
  }
});

4.输入提示和POI搜索插件结合使用

  输入提示插件和POI搜索插件是两种不同的插件,它们都可以用于提高地图应用的用户体验。
  输入提示插件是一种辅助工具,可以帮助用户快速输入地址、地点名称等信息。当用户在地图上点击位置图标时,输入提示插件会弹出一个输入框,让用户输入地址或地点名称。如果插件已经缓存了该位置的信息,它还可以自动填充地址信息,减少用户的输入量。
  POI搜索插件则是一种搜索工具,可以帮助用户快速找到附近的商店、餐馆、酒店等地点。当用户在地图上点击搜索图标时,POI搜索插件会弹出一个搜索框,让用户输入关键词进行搜索。插件会根据用户的位置信息和搜索关键词,在地图上显示出符合条件的POI(Point of Interest)信息,包括名称、地址、电话等。用户可以根据需要选择其中一个POI进行导航或拨打电话。

通常AutoComplete与PlaceSearch结合使用,使用时只需在select事件的响应函数中调用PlaceSearch的相关查询方法,这里我们使用PlaceSearch包装好的的map属性来实现POI搜索结果的显示:

实际应用:

AMap.plugin(['AMap.AutoComplete','AMap.PlaceSearch'],function(){
  var autoOptions = {
    // 城市,默认全国 
    city: "北京",
    // 使用联想输入的input的id
    input: "input"
  }
  var autocomplete= new AMap.AutoComplete(autoOptions)

  var placeSearch = new AMap.PlaceSearch({
    city:'北京',
    map:map
  })
  autocomplete.on('select', function(e){
    //TODO 针对选中的poi实现自己的功能
    placeSearch.search(e.poi.name)
  })
 })

5. 地理编码与逆地理编码

  地理编码和逆地理编码是两种不同的技术,它们都与地图应用中的地址搜索有关。
  地理编码是将一个地址转换为经纬度坐标的过程。通常情况下,地理编码器会从互联网上获取该地址对应的经纬度信息,并将其返回给应用程序。在地图应用中,可以将地理编码结果显示在地图上,以便用户更方便地找到目的地。

  逆地理编码则是将一个经纬度坐标转换为地址的过程。逆地理编码器会使用特定的算法和数据库,将输入的经纬度坐标映射到对应的地址。在地图应用中,可以将逆地理编码结果显示在搜索框中,以便用户快速输入地址进行搜索。

  这两种技术都是基于地址信息的,因此需要相应的数据源来支持它们的实现。例如,地理编码器需要访问互联网上的地址数据库或API接口来获取地址信息;而逆地理编码器则需要访问特定的地址数据库或API接口来进行地址转换。同时,由于不同地区的地址格式和命名规范可能不同,因此还需要针对不同的地区进行相应的调整和适配。

高德JS API提供AMap.Geocoder服务插件来完成这两种编码,创建地理编码对象的代码如下:

实际应用:

AMap.plugin('AMap.Geocoder', function() {
  var geocoder = new AMap.Geocoder({
    city: '全国' // city 指定进行编码查询的城市,支持传入城市名、adcode 和 citycode
  })
})
  // 使用geocoder做地理编码
  AMap.plugin('AMap.Geocoder', function() {
  var geocoder = new AMap.Geocoder({
    city: '010' // city 指定进行编码查询的城市,支持传入城市名、adcode 和 citycode
  })
  
  var address = '北京市海淀区苏州街';

  geocoder.getLocation(address, function(status, result) {
    if (status === 'complete' && result.info === 'OK') {
      // result中对应详细地理坐标信息
    }
  })
})
// 批量地理编码 getLocation()方法参数传入为地址数组,
var addresses = ['朝阳区阜荣街10号', '朝阳区广顺南大街13号', '朝阳区阜通西大街17号'];
geocoder.getLocation(addresses, function(status, result) {
  if (status === 'complete' && result.info === 'OK') {
    // result中对应详细地理坐标信息
    // result.geocodes为批量地址地理编码数据
  }
})
  // 使用geocoder做逆地理编码
  AMap.plugin('AMap.Geocoder', function() {
  var geocoder = new AMap.Geocoder({
    city: '010' // city 指定进行编码查询的城市,支持传入城市名、adcode 和 citycode
  })
 
  var lnglat = [116.396574, 39.992706]

  geocoder.getAddress(lnglat, function(status, result) {
    if (status === 'complete' && result.info === 'OK') {
        // result为对应的地理位置详细信息
    }
  })
})
  // 批量逆地理编码
  var lnglats = [
  new AMap.LngLat(116.451662,39.944899), 
  new AMap.LngLat(116.444569,39.93927), 
  new AMap.LngLat(116.442932,39.933147)
];
// 也可简写成 var lnglats = [[116.451662,39.944899], [116.444569,39.93927], [116.442932,39.933147]]

geocoder.getAddress(lnglats, function(status, result) {
  if (status === 'complete' && result.info === 'OK') {
    // result为对应的地理位置详细信息
    // result.regeocodes为批量逆地址地理编码数据
  }
})

6. 完整代码实例:

结合以上说明的各个方法的demo

<template>
    <div class="home_div">
      <div id="search">
          <a-input
            v-model:value="searchValue"
            @keyup.enter="seachAddress"
            id="tipinput"
            placeholder="请输入要搜索的位置"
            style="width: 200px; margin: -1px 2px 0 12px"
          />
          <a-button type="primary" @click="seachAddress">查询</a-button>
        </div>
        <div id="container" style="height: 50vh; width: 100%"></div>
    </div>
</template>
<script setup>
// 地图map
import AMapLoader from "@amap/amap-jsapi-loader"
import { reactive, ref, onMounted, nextTick, defineProps } from 'vue';
import { shallowRef } from '@vue/reactivity'

/**实际生产中不要用 此处仅测试 
   具体实际使用方法看官网*/
window._AMapSecurityConfig = {
  securityJsCode: '你的应用生成的秘钥', // 应用生成的秘钥
}

const searchValue = ref('')
const lnglat = ref('')
const state = reactive({
    
    map: null,
    placeSearch: null,
    autoComplete: null,
    marker: null,
    form: {
        address: '',
        lng: '',
        lat: '',
    },
})


// 地图初始化
function initMap(arr) {  // 参数为中心点经纬度坐标 
  AMapLoader.load({
        key: "应用生成的key",
        version: "2.0",
        plugins: ["AMap.ToolBar", "AMap.ControlBar", 'AMap.AutoComplete', 'AMap.PlaceSearch', 'AMap.Geocoder', 'AMap.Marker'], // 地图插件 根据需求从高德开放平台添加
    }).then((AMap) => {
        state.map = new AMap.Map('container', {
            viewMode: "3D",  //  是否为3D地图模式
            zoom: 10, //  地图显示的缩放级别
            zooms:[2,22], // 地图缩放范围
            center: arr, //  地图中心点坐标
            resizeEnable: true  //  是否监控地图容器尺寸变化
        });
        // 地图放大缩小插件
        let toolBar = new AMap.ToolBar({
            position: {
                top: '120px',
                right: '51px'
            }
        })
        // 3D地图插件
        let controlBar = new AMap.ControlBar({
            position: {
                top: '20px',
                right: '20px',
            },
        });

        state.geoCoder = new AMap.Geocoder({
            city: '全国', // 默认:“全国”
            radius: 1000 // 范围,默认:500
        })

        state.autoComplete = new AMap.AutoComplete({ city: '全国' });

        state.placeSearch = new AMap.PlaceSearch({
          map: state.map     
        })
        state.map.on('click', clickMap)
        state.map.addControl(toolBar);   // 添加右上角的放大缩小
        state.map.addControl(controlBar);   // 添加右上角的放大缩小
    }).catch((e) => {
        console.error(e);  //加载错误提示
    }).finally(() => {
        initCoord([state.form.lng, state.form.lat])
    })   
}

// 地图点击事件
function clickMap(e) { // 点击地图事件
    if (!e && !e.lnglat) {
        return
    }
    state.form.lng = e.lnglat.lng
    state.form.lat = e.lnglat.lat
    regeocoder()
    removeMarker() // 先删除地图上标记点
    setMapMarker() // 在添加新的标记点
}

// 关键字搜索
const seachAddress = () => {
  if (searchValue.value != '') {
    //清除地图上的覆盖物
    state.map.clearMap()
    state.map.plugin('AMap.PlaceSearch', () => {
      let placeSearch = new AMap.PlaceSearch({
        // city 指定搜索所在城市,支持传入格式有:城市名、citycode和adcode
        city: '010',
        map: state.map
      })
      let that = state.form
      placeSearch.search(searchValue.value, function (status, result) {
        // 查询成功时,result即对应匹配的POI信息 
        var pois = result.poiList.pois
        for (var i = 0; i < pois.length; i++) {
          var poi = pois[i]
          var marker = []
          marker[i] = new AMap.Marker({
            position: poi.location, // 经纬度对象,也可以是经纬度构成的一维数组[116.39, 39.9]
            title: poi.name
          })
          // 将创建的点标记添加到已有的地图实例:
          state.map.add(marker[i])
        }
        state.map.setFitView()
        AMap.Event.addListener(placeSearch, 'markerClick', function (data) {
          let result = data
          //经纬度
          state.form.lng = result.event.lnglat.lng
          state.form.lat = result.event.lnglat.lat
          toGetAddress()
        })        
      })
    })
  } else {
    alert('请输入地址')
  }
}


// 筛选查询
function searchCoord(data) {
  if(data) {
    //清除地图上的覆盖物
    state.map.clearMap()
    state.map.setCenter([data.records[0].longitude, data.records[0].latitude])
    state.map.setZoom(11)
    var marker = []
    for (var index = 0; index < data.records.length; index++) {
      var poi = data.records[index]
      // 其他坐标转J02坐标
      var gps = [poi.longitude, poi.latitude]
      AMap.convertFrom(gps, 'gps', function (status, result) {
        if (result.info === 'ok') {    
          poi.longitude = result.locations[0].lng
          poi.latitude = result.locations[0].lat              
        }
      });
      marker[index] = new AMap.Marker({
        position: [poi.longitude, poi.latitude],
        title:poi.locationName
      })
      // 将创建的点标记添加到已有的地图实例:
      state.map.add(marker[index])
      var content = []
      if(poi.personInCharge) {
        content.push("负责人: "+ poi.personInCharge + "")
      }
      marker[index].content = content
      marker[index].title = poi.locationName
      marker[index].on('click', showInfoWindow)
      state.map.off('click', clickMap)
    }
  }
}

// 自定义窗体
function showInfoWindow(e) {
  //实例化信息窗体
  var title = e.target.title,
      content = e.target.content;
  var infoWindow =  new AMap.InfoWindow({
      isCustom: true,  //使用自定义窗体
      content: createInfoWindow(title, content.join("<br/>")),
      closeWhenClickMap: true,
      offset: new AMap.Pixel(16, -45)
  });
  infoWindow.open(state.map, e.target.getPosition());
}

//构建自定义信息窗体
function createInfoWindow(title, content) {
    var info = document.createElement("div");
    info.className = "custom-info input-card content-window-card";

    //可以通过下面的方式修改自定义窗体的宽高
    info.style.width = "400px";
    // 定义顶部标题
    var top = document.createElement("div");
    var titleD = document.createElement("div");
    top.className = "info-top";
    titleD.innerHTML = title;
    top.appendChild(titleD);
    info.appendChild(top);

    // 定义中部内容
    var middle = document.createElement("div");
    middle.className = "info-middle";
    middle.style.backgroundColor = 'white';
    middle.innerHTML = content;
    info.appendChild(middle);

    // 定义底部内容
    var bottom = document.createElement("div");
    bottom.className = "info-bottom";
    bottom.style.position = 'relative';
    bottom.style.top = '0px';
    bottom.style.margin = '0 auto';
    // var sharp = document.createElement("img");
    // sharp.src = "https://webapi.amap.com/images/sharp.png";
    // bottom.appendChild(sharp);
    info.appendChild(bottom);
    return info;
}


// 下拉选中查询 
function select(e) {
    state.placeSearch.setCity(e.poi.adcode)
    state.placeSearch.search(e.poi.name) //关键字查询查询
} 

// 坐标转换
function initCoord(gps) {
  // 其他坐标转J02坐标
  AMap.convertFrom(gps, 'gps', function (status, result) {
    if (result.info === 'ok') {    
      state.form.lng = result.locations[0].lng
      state.form.lat = result.locations[0].lat
      nextTick(function () {
          removeMarker()
          setMapMarker()
      })
    }
  });
}

// 设置标记
function setMapMarker() {
    if (state.form.lng == '' && state.form.lat == '') {
        return
    }
    state.map.setFitView()
    state.marker = new AMap.Marker({
        map: state.map,
        position: [state.form.lng, state.form.lat],
    })
    toGetAddress()
    state.map.add(state.marker)
    state.map.setFitView() 
}

// 清除标记
function removeMarker() {
    if (state.marker) {
        state.map.remove(state.marker)
    }
}

// 坐标位置转换
function regeocoder() {
  let lnglat = [state.form.lng, state.form.lat]
  state.geoCoder.getAddress(lnglat, (status, result) => {
      if (status === 'complete' && result.regeocode) {
          state.form.address =result.regeocode.formattedAddress // 返回位置信息
      }
  })
}

// 获取地址
function toGetAddress() {
    let lnglat = [state.form.lng, state.form.lat]
    state.geoCoder.getAddress(lnglat, (status, result) => {
        if (status === 'complete' && result.regeocode) {
             // 此处写处理逻辑
        }
    })
}

function toGetCoordinate() {
    let address = state.form.address
    state.geocoder.getLocation(address, function (status, result) {
      if (status === 'complete' && result.info === 'OK') {
          initMap([result.geocodes[0].location.lng, result.geocodes[0].location.lat])
          state.form.lng = result.geocodes[0].location.lng
          state.form.lat = result.geocodes[0].location.lat
          state.form.address = result.geocodes[0].formattedAddress
      }
    })
    nextTick(function () {
        removeMarker()
        setMapMarker()
    }) 
}

onMounted(() => {
      //组件挂载
      initMap([经度, 纬度])
});




/** 样式*/
</script>
<style scope>
    .home_div{
        height: 100%;
        width: 100%;
        padding: 0px;
        margin: 0px;
        position: relative;
    }
    #container{
        padding: 0px;
        margin: 0px;
    }

    html, body, #container {
            height: 100%;
            width: 100%;
        }

        .content-window-card {
            position: relative;
            box-shadow: none;
            bottom: 0;
            left: 0;
            width: auto;
            padding: 0;
        }

        .content-window-card p {
            height: 2rem;
        }

        .custom-info {
            border: solid 1px silver;
        }

        div.info-top {
            position: relative;
            background: none repeat scroll 0 0 #F9F9F9;
            border-bottom: 1px solid #CCC;
            border-radius: 5px 5px 0 0;
        }

        div.info-top div {
            display: inline-block;
            color: #333333;
            font-size: 14px;
            font-weight: bold;
            line-height: 31px;
            padding: 0 10px;
        }

        div.info-top img {
            position: absolute;
            top: 10px;
            right: 10px;
            transition-duration: 0.25s;
        }

        div.info-top img:hover {
            box-shadow: 0px 0px 5px #000;
        }

        div.info-middle {
            font-size: 12px;
            padding: 10px 6px;
            line-height: 20px;
        }

        div.info-bottom {
            height: 0px;
            width: 100%;
            clear: both;
            text-align: center;
        }

        div.info-bottom img {
            position: relative;
            z-index: 104;
        }

        span {
            margin-left: 5px;
            font-size: 11px;
        }

        .info-middle img {
            float: left;
            margin-right: 6px;
        }



</style>

总结

项目用的基本就这么多了 总体来说官方文档写的还是还详尽的,用法也都很简单,有些没写到的直接看官方api就好了。
  如果喜欢的话,欢迎 🤞关注 👍点赞 💬评论 🤝收藏  🙌一起讨论
  你的评价就是我✍️创作的动力!					  💞💞💞
  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
你可以使用MapboxGL和Vue3来加载高德地图实例。首先,确保你已经安装了MapboxGL和Vue3的依赖。 接下来,你可以创建一个Vue组件来加载地图。在这个组件中,你需要先引入MapboxGL和高德地图JavaScript库: ```javascript import mapboxgl from 'mapbox-gl'; import AMapLoader from '@amap/amap-jsapi-loader'; export default { mounted() { // 加载高德地图 AMapLoader.load({ key: 'your-amap-api-key', version: '2.0', plugins: ['AMap.Geocoder'] }).then(() => { // 初始化MapboxGL mapboxgl.accessToken = 'your-mapbox-access-token'; // 创建地图实例 const map = new mapboxgl.Map({ container: 'map', style: 'mapbox://styles/mapbox/streets-v11', center: [lng, lat], zoom: 12 }); // 在地图上添加高德地图瓦片图层 const amapLayer = new AMap.TileLayer(); map.addLayer(amapLayer); // 在地图上添加其他图层、标记等 // ... }); } } ``` 在上面的代码中,你需要替换`your-amap-api-key`为你的高德地图API密钥,以及`your-mapbox-access-token`为你的Mapbox访问令牌。你还可以根据需要设置地图的中心点和缩放级别。 最后,将地图容器添加到你的Vue模板中: ```html <template> <div id="map"></div> </template> <script> import mapComponent from '@/components/MapComponent.vue'; export default { components: { mapComponent } } </script> ``` 通过这种方式,你可以在Vue组件中使用MapboxGL和高德地图来加载地图实例。记得替换你的API密钥和访问令牌,以及根据需要自定义地图样式和添加其他图层。希望对你有帮助!
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

家有娇妻张兔兔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值