wuyi 1 mesiac pred
rodič
commit
cd6ccfa7c0
4 zmenil súbory, kde vykonal 887 pridanie a 113 odobranie
  1. 1 0
      package.json
  2. 653 0
      src/components/LocationPicker.vue
  3. 228 113
      src/views/ScanView.vue
  4. 5 0
      yarn.lock

+ 1 - 0
package.json

@@ -19,6 +19,7 @@
     "axios": "^1.8.4",
     "axios": "^1.8.4",
     "chart.js": "^4.4.8",
     "chart.js": "^4.4.8",
     "decimal.js": "^10.5.0",
     "decimal.js": "^10.5.0",
+    "leaflet": "^1.9.4",
     "less": "^4.2.2",
     "less": "^4.2.2",
     "pinia": "^3.0.1",
     "pinia": "^3.0.1",
     "primeflex": "^4.0.0",
     "primeflex": "^4.0.0",

+ 653 - 0
src/components/LocationPicker.vue

@@ -0,0 +1,653 @@
+<script setup>
+import { ref, watch, nextTick, onBeforeUnmount, computed } from 'vue'
+import Dialog from 'primevue/dialog'
+
+// Leaflet 将使用懒加载
+let L = null
+let searchAbortController = null
+
+const props = defineProps({
+  modelValue: {
+    type: Boolean,
+    default: false
+  },
+  initialLocation: {
+    type: String,
+    default: ''
+  },
+  closable: {
+    type: Boolean,
+    default: true
+  },
+  readonly: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const emit = defineEmits(['update:modelValue', 'save'])
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (value) => emit('update:modelValue', value)
+})
+
+const mapContainer = ref(null)
+const mapInstance = ref(null)
+const mapMarker = ref(null)
+const selectedLocation = ref({ lat: null, lng: null, address: '' })
+const loadingMap = ref(false)
+const mapLoaded = ref(false)
+const searchQuery = ref('')
+const searchResults = ref([])
+const searchLoading = ref(false)
+const searchError = ref('')
+
+// 懒加载 Leaflet
+const loadLeaflet = async () => {
+  if (mapLoaded.value && L) return L
+
+  loadingMap.value = true
+  try {
+    // 动态导入 Leaflet 和 CSS
+    const [leafletModule] = await Promise.all([
+      import('leaflet'),
+      import('leaflet/dist/leaflet.css')
+    ])
+
+    L = leafletModule.default
+
+    // 修复 Leaflet 默认图标路径问题
+    delete L.Icon.Default.prototype._getIconUrl
+    L.Icon.Default.mergeOptions({
+      iconRetinaUrl: new URL('leaflet/dist/images/marker-icon-2x.png', import.meta.url).href,
+      iconUrl: new URL('leaflet/dist/images/marker-icon.png', import.meta.url).href,
+      shadowUrl: new URL('leaflet/dist/images/marker-shadow.png', import.meta.url).href
+    })
+
+    mapLoaded.value = true
+    return L
+  } catch (error) {
+    loadingMap.value = false
+    throw error
+  } finally {
+    loadingMap.value = false
+  }
+}
+
+const initMap = async () => {
+  if (!mapContainer.value) return
+
+  // 懒加载 Leaflet
+  try {
+    await loadLeaflet()
+  } catch (error) {
+    return
+  }
+
+  // 如果地图实例已存在,先移除
+  if (mapInstance.value) {
+    try {
+      mapInstance.value.remove()
+    } catch (e) {
+      // 忽略移除错误
+    }
+    mapInstance.value = null
+    mapMarker.value = null
+  }
+
+  await nextTick()
+
+  // 确保容器可见
+  if (!mapContainer.value || mapContainer.value.offsetParent === null) {
+    return
+  }
+
+  // 默认位置:纽约市
+  const defaultLat = 40.7128
+  const defaultLng = -74.0060
+  let initialLat = defaultLat
+  let initialLng = defaultLng
+
+  // 如果已有选中的位置,使用它
+  if (selectedLocation.value.lat && selectedLocation.value.lng) {
+    initialLat = selectedLocation.value.lat
+    initialLng = selectedLocation.value.lng
+  } else if (!props.readonly) {
+    // 只在非只读模式下尝试获取用户位置
+    const isSecureContext = window.isSecureContext || location.protocol === 'https:' || location.hostname === 'localhost' || location.hostname === '127.0.0.1'
+
+    if (isSecureContext && navigator.geolocation) {
+      try {
+        const position = await new Promise((resolve, reject) => {
+          navigator.geolocation.getCurrentPosition(
+            resolve,
+            (error) => {
+              reject(error)
+            },
+            {
+              enableHighAccuracy: true,
+              timeout: 5000,
+              maximumAge: 0
+            }
+          )
+        })
+        initialLat = position.coords.latitude
+        initialLng = position.coords.longitude
+      } catch (error) {
+        // 静默失败,使用默认位置
+      }
+    }
+  }
+
+  // 初始化地图
+  mapInstance.value = L.map(mapContainer.value, {
+    center: [initialLat, initialLng],
+    zoom: 13,
+    zoomControl: true
+  })
+
+  // 添加地图图层
+  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+    attribution: '© OpenStreetMap contributors',
+    maxZoom: 19
+  }).addTo(mapInstance.value)
+
+  // 添加初始标记
+  mapMarker.value = L.marker([initialLat, initialLng], {
+    draggable: !props.readonly
+  }).addTo(mapInstance.value)
+
+  selectedLocation.value = { lat: initialLat, lng: initialLng, address: '' }
+
+  // 确保地图正确渲染
+  setTimeout(() => {
+    if (mapInstance.value) {
+      mapInstance.value.invalidateSize()
+      mapInstance.value._onResize()
+    }
+  }, 100)
+
+
+  // 地图点击事件(只读模式下禁用)
+  if (!props.readonly) {
+    mapInstance.value.on('click', (e) => {
+      const { lat, lng } = e.latlng
+      updateMarker(lat, lng)
+    })
+  }
+
+  // 标记拖拽事件
+  mapMarker.value.on('dragend', (e) => {
+    const { lat, lng } = e.target.getLatLng()
+    updateMarker(lat, lng)
+  })
+
+  // 初始位置的反向地理编码
+  reverseGeocode(initialLat, initialLng)
+}
+
+const updateMarker = (lat, lng) => {
+  if (mapMarker.value) {
+    mapMarker.value.setLatLng([lat, lng])
+  }
+  selectedLocation.value = { lat, lng, address: '' }
+  reverseGeocode(lat, lng)
+}
+
+// 精简地址,只保留关键信息
+const simplifyAddress = (data) => {
+  if (!data || !data.address) {
+    return null
+  }
+
+  const addr = data.address
+  const parts = []
+
+  // 街道地址(建筑物名称 + 街道号码 + 街道名称)
+  if (addr.building || addr.house_number || addr.road) {
+    const streetParts = []
+    if (addr.building) streetParts.push(addr.building)
+    if (addr.house_number) streetParts.push(addr.house_number)
+    if (addr.road) streetParts.push(addr.road)
+    if (streetParts.length > 0) {
+      parts.push(streetParts.join(' '))
+    }
+  }
+
+  // 城市/镇
+  if (addr.city) {
+    parts.push(addr.city)
+  } else if (addr.town) {
+    parts.push(addr.town)
+  } else if (addr.village) {
+    parts.push(addr.village)
+  } else if (addr.municipality) {
+    parts.push(addr.municipality)
+  }
+
+  // 州/省
+  if (addr.state) {
+    parts.push(addr.state)
+  }
+
+  // 国家
+  if (addr.country) {
+    parts.push(addr.country)
+  }
+
+  return parts.length > 0 ? parts.join(', ') : null
+}
+
+// 正向地理编码:从地址字符串获取坐标
+const geocode = async (address) => {
+  try {
+    const response = await fetch(
+      `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}&limit=1&accept-language=en`
+    )
+    const data = await response.json()
+    if (data && data.length > 0) {
+      return {
+        lat: parseFloat(data[0].lat),
+        lng: parseFloat(data[0].lon),
+        address: address
+      }
+    }
+    return null
+  } catch (error) {
+    console.error('Geocoding error:', error)
+    return null
+  }
+}
+
+const reverseGeocode = async (lat, lng) => {
+  try {
+    const response = await fetch(
+      `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18&addressdetails=1&accept-language=en`
+    )
+    const data = await response.json()
+    if (data) {
+      // 优先使用精简地址
+      const simplified = simplifyAddress(data)
+      if (simplified) {
+        selectedLocation.value.address = simplified
+      } else if (data.display_name) {
+        selectedLocation.value.address = data.display_name
+      } else {
+        selectedLocation.value.address = `${lat.toFixed(6)}, ${lng.toFixed(6)}`
+      }
+    } else {
+      selectedLocation.value.address = `${lat.toFixed(6)}, ${lng.toFixed(6)}`
+    }
+  } catch (error) {
+    selectedLocation.value.address = `${lat.toFixed(6)}, ${lng.toFixed(6)}`
+  }
+}
+
+const ensureMapReady = async () => {
+  if (!mapInstance.value) {
+    await initMap()
+  }
+}
+
+const resetSearchState = (clearQuery = false) => {
+  if (clearQuery) {
+    searchQuery.value = ''
+  }
+  if (searchAbortController) {
+    searchAbortController.abort()
+    searchAbortController = null
+  }
+  searchResults.value = []
+  searchError.value = ''
+  searchLoading.value = false
+}
+
+const destroyMapInstance = () => {
+  if (mapMarker.value) {
+    try {
+      mapMarker.value.off('dragend')
+    } catch (error) {
+      // 忽略标记事件清理错误
+    } finally {
+      mapMarker.value = null
+    }
+  }
+
+  if (mapInstance.value) {
+    try {
+      mapInstance.value.off()
+      mapInstance.value.remove()
+    } catch (error) {
+    } finally {
+      mapInstance.value = null
+    }
+  }
+
+  resetSearchState(true)
+}
+
+const handleSave = () => {
+  if (selectedLocation.value.lat && selectedLocation.value.lng) {
+    const location = selectedLocation.value.address || `${selectedLocation.value.lat}, ${selectedLocation.value.lng}`
+    emit('save', location)
+    // 如果允许关闭,则关闭弹窗;否则保持打开
+    if (props.closable) {
+      visible.value = false
+    }
+  }
+}
+
+const handleClose = () => {
+  // 如果允许关闭,则关闭弹窗
+  if (props.closable) {
+    visible.value = false
+  }
+}
+
+watch(searchQuery, (value) => {
+  if (!value.trim()) {
+    resetSearchState()
+  }
+})
+
+const searchLocations = async () => {
+  const query = searchQuery.value.trim()
+  if (!query) {
+    resetSearchState()
+    searchLoading.value = false
+    return
+  }
+
+  if (searchAbortController) {
+    searchAbortController.abort()
+  }
+
+  searchLoading.value = true
+  searchError.value = ''
+  searchAbortController = new AbortController()
+
+  try {
+    const response = await fetch(
+      `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&addressdetails=1&limit=5&accept-language=en`,
+      {
+        signal: searchAbortController.signal
+      }
+    )
+
+    if (!response.ok) {
+      throw new Error('Search failed')
+    }
+
+    const data = await response.json()
+    searchResults.value = data.map((item) => {
+      // 尝试精简地址
+      const simplified = simplifyAddress(item)
+      return {
+        id: item.osm_id ?? `${item.lat}-${item.lon}`,
+        displayName: simplified || item.display_name,
+        lat: parseFloat(item.lat),
+        lng: parseFloat(item.lon)
+      }
+    })
+
+    if (searchResults.value.length === 0) {
+      searchError.value = 'No matching address found.'
+    } else {
+      searchError.value = ''
+    }
+  } catch (error) {
+    if (error.name === 'AbortError') {
+      return
+    }
+    searchError.value = 'Search failed, please try again later.'
+  } finally {
+    searchLoading.value = false
+    searchAbortController = null
+  }
+}
+
+const handleSearchSelect = async (result) => {
+  searchQuery.value = result.displayName
+  selectedLocation.value = {
+    lat: result.lat,
+    lng: result.lng,
+    address: result.displayName
+  }
+
+  searchResults.value = []
+  searchError.value = ''
+
+  await ensureMapReady()
+
+  if (mapInstance.value) {
+    mapInstance.value.setView([result.lat, result.lng], 15, { animate: true })
+  }
+
+  if (!mapMarker.value) {
+    mapMarker.value = L.marker([result.lat, result.lng], {
+      draggable: !props.readonly
+    }).addTo(mapInstance.value)
+    mapMarker.value.on('dragend', (e) => {
+      const { lat, lng } = e.target.getLatLng()
+      updateMarker(lat, lng)
+    })
+  }
+
+  updateMarker(result.lat, result.lng)
+}
+
+// 监听弹窗显示状态
+watch(
+  () => props.modelValue,
+  async (visible) => {
+    if (visible) {
+      // 如果已有地址,尝试解析坐标(格式:lat, lng 或地址字符串)
+      if (props.initialLocation) {
+        const coordsMatch = props.initialLocation.match(/(-?\d+\.?\d*),\s*(-?\d+\.?\d*)/)
+        if (coordsMatch) {
+          // 是坐标格式
+          selectedLocation.value = {
+            lat: parseFloat(coordsMatch[1]),
+            lng: parseFloat(coordsMatch[2]),
+            address: props.initialLocation
+          }
+        } else {
+          // 是地址字符串,需要进行地理编码获取坐标
+          selectedLocation.value.address = props.initialLocation
+          const geocodeResult = await geocode(props.initialLocation)
+          if (geocodeResult) {
+            selectedLocation.value.lat = geocodeResult.lat
+            selectedLocation.value.lng = geocodeResult.lng
+          }
+        }
+      }
+      // 等待 DOM 更新和 Dialog 动画完成
+      await nextTick()
+      // 使用 setTimeout 确保 Dialog 完全显示后再初始化地图
+      setTimeout(async () => {
+        if (mapContainer.value) {
+          if (!mapInstance.value) {
+            // 首次打开,初始化地图
+            initMap()
+          } else {
+            // 再次打开,检查地图实例是否有效
+            try {
+              await loadLeaflet()
+
+              // 检查地图实例是否真的有效
+              if (mapInstance.value) {
+                const container = mapInstance.value.getContainer()
+                if (!container) {
+                  mapInstance.value = null
+                  mapMarker.value = null
+                  initMap()
+                  return
+                }
+              } else {
+                mapInstance.value = null
+                mapMarker.value = null
+                initMap()
+                return
+              }
+
+              // 确保容器可见后再更新
+              if (mapContainer.value && mapContainer.value.offsetParent !== null) {
+
+                // 检查地图图层是否存在
+                const hasTileLayer = mapInstance.value.eachLayer && Array.from(mapInstance.value._layers || {}).some(layer => layer instanceof L.TileLayer)
+
+                // 如果图层丢失,重新添加
+                if (!hasTileLayer) {
+                  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+                    attribution: '© OpenStreetMap contributors',
+                    maxZoom: 19
+                  }).addTo(mapInstance.value)
+                }
+
+                // 更新地图大小(重要:在容器可见后调用)
+                setTimeout(() => {
+                  if (mapInstance.value) {
+                    mapInstance.value.invalidateSize()
+
+                    // 强制刷新地图
+                    setTimeout(() => {
+                      mapInstance.value.invalidateSize()
+                      mapInstance.value._onResize()
+                    }, 100)
+
+                    // 如果有已保存的位置,更新地图视图
+                    if (selectedLocation.value.lat && selectedLocation.value.lng) {
+                      mapInstance.value.setView([selectedLocation.value.lat, selectedLocation.value.lng], 13, { animate: false })
+                      if (!mapMarker.value) {
+                        // 如果标记不存在,重新创建
+                        mapMarker.value = L.marker([selectedLocation.value.lat, selectedLocation.value.lng], {
+                          draggable: !props.readonly
+                        }).addTo(mapInstance.value)
+                        mapMarker.value.on('dragend', (e) => {
+                          const { lat, lng } = e.target.getLatLng()
+                          updateMarker(lat, lng)
+                        })
+                      } else {
+                        updateMarker(selectedLocation.value.lat, selectedLocation.value.lng)
+                      }
+                    } else {
+                      // 如果没有保存的位置,重新获取用户位置或使用默认位置
+                      const defaultLat = 40.7128
+                      const defaultLng = -74.0060
+                      mapInstance.value.setView([defaultLat, defaultLng], 13, { animate: false })
+                      if (!mapMarker.value) {
+                        mapMarker.value = L.marker([defaultLat, defaultLng], {
+                          draggable: !props.readonly
+                        }).addTo(mapInstance.value)
+                        mapMarker.value.on('dragend', (e) => {
+                          const { lat, lng } = e.target.getLatLng()
+                          updateMarker(lat, lng)
+                        })
+                      } else {
+                        updateMarker(defaultLat, defaultLng)
+                      }
+                    }
+                  }
+                }, 50)
+              } else {
+                // 容器不可见,重新初始化
+                mapInstance.value = null
+                mapMarker.value = null
+                initMap()
+              }
+            } catch (error) {
+              // 如果地图实例无效,重新初始化
+              mapInstance.value = null
+              mapMarker.value = null
+              initMap()
+            }
+          }
+        }
+      }, 150) // 等待 Dialog 动画完成(稍微增加延迟确保完全显示)
+    } else {
+      destroyMapInstance()
+    }
+  }
+)
+
+onBeforeUnmount(() => {
+  destroyMapInstance()
+})
+</script>
+
+<template>
+  <Dialog v-model:visible="visible" modal :closable="closable" :closeOnEscape="closable" :dismissableMask="closable"
+    :style="{ width: '90vw', maxWidth: '800px' }" :draggable="false" @hide="handleClose">
+    <template #header>
+      <div class="flex items-center gap-3">
+        <div class="flex h-10 w-10 items-center justify-center rounded-full bg-cyan-100">
+          <i class="pi pi-map-marker text-lg text-cyan-600" />
+        </div>
+        <div>
+          <h3 class="text-lg font-semibold text-slate-900">{{ readonly ? 'View address' : 'Select address' }}</h3>
+          <p class="text-sm text-slate-500">{{ readonly ? 'Preview the stored location.' : 'Click the map or drag the marker to choose a location.' }}</p>
+        </div>
+      </div>
+    </template>
+
+    <div class="space-y-4 py-4">
+      <div v-if="!readonly" class="space-y-2">
+        <label class="text-sm font-medium text-slate-700">Search places</label>
+        <div class="flex flex-col gap-2 sm:flex-row">
+          <input v-model="searchQuery" type="text"
+            class="flex-1 rounded-xl border border-slate-300 px-4 py-2.5 text-sm text-slate-700 outline-none focus:border-cyan-500 focus:ring-2 focus:ring-cyan-200"
+            placeholder="Type a city, street, or landmark. Press Enter or click Search." @keyup.enter="searchLocations" />
+          <button type="button"
+            class="rounded-xl bg-cyan-600 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-cyan-700 disabled:cursor-not-allowed disabled:opacity-50"
+            :disabled="searchLoading" @click="searchLocations">
+            {{ searchLoading ? 'Searching...' : 'Search' }}
+          </button>
+        </div>
+        <p v-if="searchError" class="text-xs text-rose-500">{{ searchError }}</p>
+        <div v-if="searchResults.length"
+          class="max-h-48 overflow-y-auto rounded-xl border border-slate-200 bg-white shadow-sm">
+          <button v-for="result in searchResults" :key="result.id" type="button"
+            class="w-full border-b border-slate-100 px-4 py-3 text-left last:border-b-0 hover:bg-slate-50"
+            @click="handleSearchSelect(result)">
+            <p class="text-sm font-medium text-slate-700">{{ result.displayName }}</p>
+            <p class="text-xs text-slate-500">
+              Coordinates: {{ result.lat.toFixed(6) }}, {{ result.lng.toFixed(6) }}
+            </p>
+          </button>
+        </div>
+      </div>
+
+      <div ref="mapContainer" class="w-full h-96 rounded-xl overflow-hidden border border-slate-200 relative">
+        <div v-if="loadingMap" class="absolute inset-0 flex items-center justify-center bg-slate-100 z-10">
+          <div class="text-center">
+            <i class="pi pi-spin pi-spinner text-4xl text-slate-400 mb-2"></i>
+            <p class="text-sm text-slate-500">Loading map...</p>
+          </div>
+        </div>
+      </div>
+      <div v-if="selectedLocation.address" class="rounded-xl border border-slate-200 bg-slate-50 p-3">
+        <p class="text-sm font-medium text-slate-700">{{ readonly ? 'Location details:' : 'Selected address:' }}</p>
+        <p class="mt-1 text-sm text-slate-600">{{ selectedLocation.address }}</p>
+        <p class="mt-1 text-xs text-slate-500">
+          Coordinates: {{ selectedLocation.lat?.toFixed(6) }}, {{ selectedLocation.lng?.toFixed(6) }}
+        </p>
+      </div>
+    </div>
+
+    <template #footer>
+      <div class="flex justify-end gap-3">
+        <button v-if="closable" type="button"
+          class="rounded-xl border border-slate-300 px-5 py-2.5 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
+          @click="handleClose">
+          {{ readonly ? 'Close' : 'Cancel' }}
+        </button>
+        <button v-if="!readonly" type="button"
+          class="rounded-xl bg-cyan-600 px-6 py-2.5 text-sm font-semibold text-white transition hover:bg-cyan-700 disabled:opacity-50"
+          :disabled="!selectedLocation.lat || !selectedLocation.lng" @click="handleSave">
+          {{ closable ? 'Save address' : 'Confirm selection' }}
+        </button>
+      </div>
+    </template>
+  </Dialog>
+</template>

+ 228 - 113
src/views/ScanView.vue

@@ -3,6 +3,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { useRoute, useRouter } from 'vue-router'
 import { useToast } from 'primevue/usetoast'
 import { useToast } from 'primevue/usetoast'
 import Dialog from 'primevue/dialog'
 import Dialog from 'primevue/dialog'
+import LocationPicker from '@/components/LocationPicker.vue'
 import {
 import {
   fetchQrInfoApi,
   fetchQrInfoApi,
   updatePersonProfileApi,
   updatePersonProfileApi,
@@ -16,7 +17,7 @@ const route = useRoute()
 const router = useRouter()
 const router = useRouter()
 const toast = useToast()
 const toast = useToast()
 
 
-// 从路径参数获取二维码编号
+// Pull QR code from route params
 const qrCode = ref(route.params.qrCode?.toString().trim() || '')
 const qrCode = ref(route.params.qrCode?.toString().trim() || '')
 const queryInput = ref(qrCode.value)
 const queryInput = ref(qrCode.value)
 
 
@@ -39,6 +40,8 @@ const maintenancePassed = ref(false)
 const showMaintenancePanel = ref(false)
 const showMaintenancePanel = ref(false)
 const showMaintenanceDialog = ref(false)
 const showMaintenanceDialog = ref(false)
 const isEditing = ref(false)
 const isEditing = ref(false)
+const showLocationDialog = ref(false)
+const showLocationViewDialog = ref(false)
 
 
 const personKeys = [
 const personKeys = [
   'photoUrl',
   'photoUrl',
@@ -50,8 +53,8 @@ const personKeys = [
   'emergencyContactPhone',
   'emergencyContactPhone',
   'emergencyContactEmail'
   'emergencyContactEmail'
 ]
 ]
-const petKeys = ['photoUrl', 'name', 'contactName', 'contactPhone', 'contactEmail', 'remark']
-const goodsKeys = ['photoUrl', 'name', 'contactName', 'contactPhone', 'contactEmail', 'remark']
+const petKeys = ['photoUrl', 'name', 'contactName', 'contactPhone', 'contactEmail', 'location', 'remark']
+const goodsKeys = ['photoUrl', 'name', 'contactName', 'contactPhone', 'contactEmail', 'location', 'remark']
 
 
 const formData = reactive({
 const formData = reactive({
   photoUrl: '',
   photoUrl: '',
@@ -65,7 +68,8 @@ const formData = reactive({
   remark: '',
   remark: '',
   contactName: '',
   contactName: '',
   contactPhone: '',
   contactPhone: '',
-  contactEmail: ''
+  contactEmail: '',
+  location: ''
 })
 })
 
 
 const qrType = computed(() => qrDetail.value?.qrType || 'person')
 const qrType = computed(() => qrDetail.value?.qrType || 'person')
@@ -75,22 +79,22 @@ const isGoods = computed(() => qrType.value === 'goods')
 const hasProfile = computed(() => Boolean(profile.value))
 const hasProfile = computed(() => Boolean(profile.value))
 const isFirstFill = computed(() => Boolean(qrDetail.value) && !hasProfile.value)
 const isFirstFill = computed(() => Boolean(qrDetail.value) && !hasProfile.value)
 const heroTitle = computed(() => {
 const heroTitle = computed(() => {
-  if (!qrDetail.value) return '联系卡'
-  if (isPerson.value) return '人员卡'
-  if (isPet.value) return '宠物卡'
-  if (isGoods.value) return '物品卡'
-  return '联系卡'
+  if (!qrDetail.value) return 'Contact Card'
+  if (isPerson.value) return 'Person Card'
+  if (isPet.value) return 'Pet Card'
+  if (isGoods.value) return 'Item Card'
+  return 'Contact Card'
 })
 })
 
 
 const parseError = (error) => {
 const parseError = (error) => {
-  if (!error) return '请求失败'
+  if (!error) return 'Request failed'
   if (typeof error === 'string') return error
   if (typeof error === 'string') return error
   if (error.message) return error.message
   if (error.message) return error.message
   if (error.detail) return error.detail
   if (error.detail) return error.detail
   if (error.response?.data?.message) return error.response.data.message
   if (error.response?.data?.message) return error.response.data.message
   if (error.data?.message) return error.data.message
   if (error.data?.message) return error.data.message
   if (error.message === undefined && error?.error) return error.error
   if (error.message === undefined && error?.error) return error.error
-  return '请求失败'
+  return 'Request failed'
 }
 }
 
 
 const resetForm = (source) => {
 const resetForm = (source) => {
@@ -114,11 +118,11 @@ const resetForm = (source) => {
 
 
 const setDocumentTitle = () => {
 const setDocumentTitle = () => {
   if (typeof document === 'undefined') return
   if (typeof document === 'undefined') return
-  let defaultName = '人员'
-  if (isPet.value) defaultName = '宠物'
-  if (isGoods.value) defaultName = '物品'
+  let defaultName = 'Person'
+  if (isPet.value) defaultName = 'Pet'
+  if (isGoods.value) defaultName = 'Item'
   const name = profile.value?.name || defaultName
   const name = profile.value?.name || defaultName
-  document.title = `${name} | 应急二维码`
+  document.title = `${name} | Emergency QR`
 }
 }
 
 
 const fetchQrDetails = async () => {
 const fetchQrDetails = async () => {
@@ -143,7 +147,7 @@ const fetchQrDetails = async () => {
 
 
 const handleQrSubmit = () => {
 const handleQrSubmit = () => {
   if (!queryInput.value.trim()) {
   if (!queryInput.value.trim()) {
-    toast.add({ severity: 'warn', summary: '提示', detail: '请先输入二维码编号', life: 2600 })
+    toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please enter a QR code first.', life: 2600 })
     return
     return
   }
   }
   router.push({ name: 'scan', params: { qrCode: queryInput.value.trim() } })
   router.push({ name: 'scan', params: { qrCode: queryInput.value.trim() } })
@@ -151,7 +155,7 @@ const handleQrSubmit = () => {
 
 
 const handleVerifyMaintenance = async () => {
 const handleVerifyMaintenance = async () => {
   if (!qrCode.value || !maintenanceCode.value) {
   if (!qrCode.value || !maintenanceCode.value) {
-    toast.add({ severity: 'warn', summary: '提示', detail: '请输入维护码', life: 2400 })
+    toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please enter the maintenance code.', life: 2400 })
     return
     return
   }
   }
   loading.verifying = true
   loading.verifying = true
@@ -164,14 +168,14 @@ const handleVerifyMaintenance = async () => {
     showMaintenancePanel.value = false
     showMaintenancePanel.value = false
     showMaintenanceDialog.value = false
     showMaintenanceDialog.value = false
     isEditing.value = true
     isEditing.value = true
-    toast.add({ severity: 'success', summary: '验证成功', detail: '已解锁信息维护', life: 2600 })
-    // 首次填写时自动滚动到表单
+    toast.add({ severity: 'success', summary: 'Verified', detail: 'Maintenance editing unlocked.', life: 2600 })
+    // Scroll to the form the first time we collect data
     if (isFirstFill.value) {
     if (isFirstFill.value) {
       setTimeout(scrollToForm, 300)
       setTimeout(scrollToForm, 300)
     }
     }
   } catch (error) {
   } catch (error) {
     maintenancePassed.value = false
     maintenancePassed.value = false
-    toast.add({ severity: 'error', summary: '验证失败', detail: parseError(error), life: 3200 })
+    toast.add({ severity: 'error', summary: 'Verification failed', detail: parseError(error), life: 3200 })
   } finally {
   } finally {
     loading.verifying = false
     loading.verifying = false
   }
   }
@@ -202,14 +206,14 @@ const handleSaveProfile = async () => {
     if (isPet.value) updater = updatePetProfileApi
     if (isPet.value) updater = updatePetProfileApi
     if (isGoods.value) updater = updateGoodsInfoApi
     if (isGoods.value) updater = updateGoodsInfoApi
     await updater(payload)
     await updater(payload)
-    toast.add({ severity: 'success', summary: '保存成功', detail: '资料已更新', life: 3000 })
+    toast.add({ severity: 'success', summary: 'Saved', detail: 'Profile has been updated.', life: 3000 })
     await fetchQrDetails()
     await fetchQrDetails()
-    // 保存成功后退出编辑模式
+    // Exit edit mode after a successful save
     isEditing.value = false
     isEditing.value = false
     maintenancePassed.value = false
     maintenancePassed.value = false
     maintenanceCode.value = ''
     maintenanceCode.value = ''
   } catch (error) {
   } catch (error) {
-    toast.add({ severity: 'error', summary: '保存失败', detail: parseError(error), life: 3200 })
+    toast.add({ severity: 'error', summary: 'Save failed', detail: parseError(error), life: 3200 })
   } finally {
   } finally {
     loading.saving = false
     loading.saving = false
   }
   }
@@ -221,8 +225,8 @@ const triggerPhotoPicker = () => {
 }
 }
 
 
 const handleImageError = (event) => {
 const handleImageError = (event) => {
-  // 图片加载失败时的处理
-  console.warn('图片加载失败:', formData.photoUrl)
+  // Handle broken image preview
+  console.warn('Image load failed:', formData.photoUrl)
   event.target.style.display = 'none'
   event.target.style.display = 'none'
 }
 }
 
 
@@ -230,35 +234,35 @@ const handlePhotoChange = async (event) => {
   const file = event.target.files?.[0]
   const file = event.target.files?.[0]
   if (!file) return
   if (!file) return
 
 
-  // 验证文件类型
+  // Validate mime type
   if (!file.type.startsWith('image/')) {
   if (!file.type.startsWith('image/')) {
-    toast.add({ severity: 'warn', summary: '提示', detail: '请选择图片文件', life: 2400 })
+    toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please choose an image file.', life: 2400 })
     return
     return
   }
   }
 
 
-  // 验证文件大小(限制为 15MB)
+  // Validate file size (limit 15MB)
   const maxSize = 15 * 1024 * 1024
   const maxSize = 15 * 1024 * 1024
   if (file.size > maxSize) {
   if (file.size > maxSize) {
-    toast.add({ severity: 'warn', summary: '提示', detail: '图片大小不能超过 15MB', life: 2400 })
+    toast.add({ severity: 'warn', summary: 'Notice', detail: 'Image size cannot exceed 15MB.', life: 2400 })
     return
     return
   }
   }
 
 
   loading.photo = true
   loading.photo = true
   try {
   try {
     const response = await uploadFile(file)
     const response = await uploadFile(file)
-    // 根据返回的数据结构获取 URL
+    // Normalize upload response shape
     const url = response?.data?.url || response?.url || ''
     const url = response?.data?.url || response?.url || ''
     if (url) {
     if (url) {
       formData.photoUrl = url
       formData.photoUrl = url
-      toast.add({ severity: 'success', summary: '上传成功', detail: '图片已更新', life: 2400 })
+      toast.add({ severity: 'success', summary: 'Uploaded', detail: 'Photo has been updated.', life: 2400 })
     } else {
     } else {
-      throw new Error('未获取到图片地址')
+      throw new Error('Image URL not found')
     }
     }
   } catch (error) {
   } catch (error) {
-    toast.add({ severity: 'error', summary: '上传失败', detail: parseError(error), life: 3200 })
+    toast.add({ severity: 'error', summary: 'Upload failed', detail: parseError(error), life: 3200 })
   } finally {
   } finally {
     loading.photo = false
     loading.photo = false
-    // 清空 input,允许重复上传同一文件
+    // Reset input so the same file can trigger change
     if (event.target) {
     if (event.target) {
       event.target.value = ''
       event.target.value = ''
     }
     }
@@ -282,6 +286,33 @@ const sendEmail = (email) => {
   window.location.href = `mailto:${email}`
   window.location.href = `mailto:${email}`
 }
 }
 
 
+const copyInfo = async (value, label = 'info') => {
+  const content = value?.toString().trim()
+  if (!content) {
+    toast.add({ severity: 'info', summary: 'Notice', detail: `Nothing to copy for ${label}.`, life: 2200 })
+    return
+  }
+  try {
+    if (navigator?.clipboard?.writeText) {
+      await navigator.clipboard.writeText(content)
+    } else {
+      const textarea = document.createElement('textarea')
+      textarea.value = content
+      textarea.style.position = 'fixed'
+      textarea.style.opacity = '0'
+      document.body.appendChild(textarea)
+      textarea.focus()
+      textarea.select()
+      document.execCommand('copy')
+      document.body.removeChild(textarea)
+    }
+    toast.add({ severity: 'success', summary: 'Copied', detail: `${label} copied to clipboard.`, life: 2000 })
+  } catch (error) {
+    console.error('Copy failed', error)
+    toast.add({ severity: 'error', summary: 'Copy failed', detail: 'Please copy manually.', life: 2200 })
+  }
+}
+
 const resetPageState = () => {
 const resetPageState = () => {
   qrDetail.value = null
   qrDetail.value = null
   profile.value = null
   profile.value = null
@@ -317,13 +348,43 @@ watch([qrType, () => profile.value?.name], () => {
   setDocumentTitle()
   setDocumentTitle()
 })
 })
 
 
-// 首次填写时自动显示验证弹窗
+// Auto open verification dialog on first fill
 watch(isFirstFill, (value) => {
 watch(isFirstFill, (value) => {
   if (value && !maintenancePassed.value) {
   if (value && !maintenancePassed.value) {
     showMaintenanceDialog.value = true
     showMaintenanceDialog.value = true
   }
   }
 })
 })
 
 
+const handleOpenLocationDialog = () => {
+  showLocationDialog.value = true
+}
+
+const handleSaveLocation = (location) => {
+  formData.location = location
+  toast.add({ severity: 'success', summary: 'Saved', detail: 'Address selected.', life: 2400 })
+}
+
+const handleOpenLocationView = () => {
+  showLocationViewDialog.value = true
+}
+
+const openGoogleMaps = () => {
+  const location = isPerson.value ? null : profile.value?.location
+  if (!location) return
+
+  // Try to parse coordinate format (lat, lng)
+  const coordsMatch = location.match(/(-?\d+\.?\d*),\s*(-?\d+\.?\d*)/)
+  if (coordsMatch) {
+    const lat = coordsMatch[1]
+    const lng = coordsMatch[2]
+    // Open Google Maps with coordinates
+    window.open(`https://www.google.com/maps?q=${lat},${lng}`, '_blank')
+  } else {
+    // Otherwise search by address string
+    window.open(`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(location)}`, '_blank')
+  }
+}
+
 onMounted(() => {
 onMounted(() => {
   if (!qrCode.value) {
   if (!qrCode.value) {
     setDocumentTitle()
     setDocumentTitle()
@@ -353,15 +414,15 @@ onMounted(() => {
               <i class="pi pi-qrcode text-2xl text-cyan-300" />
               <i class="pi pi-qrcode text-2xl text-cyan-300" />
             </div>
             </div>
             <div class="flex-1">
             <div class="flex-1">
-              <p class="text-lg font-semibold text-white">欢迎使用应急二维码系统</p>
+              <p class="text-lg font-semibold text-white">Welcome to the Emergency QR system</p>
               <p class="mt-2 text-sm text-slate-300">
               <p class="mt-2 text-sm text-slate-300">
-                请扫描二维码或在下方输入二维码编号进行查询
+                Scan the QR code or enter its value below to continue.
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
           <div class="mt-6 flex flex-col gap-4 sm:flex-row sm:items-start">
           <div class="mt-6 flex flex-col gap-4 sm:flex-row sm:items-start">
             <div class="flex-1 space-y-2">
             <div class="flex-1 space-y-2">
-              <input v-model="queryInput" type="text" placeholder="输入二维码编号"
+              <input v-model="queryInput" type="text" placeholder="Enter QR code"
                 class="w-full rounded-2xl border border-white/20 bg-white/5 px-5 py-3.5 text-base text-white placeholder:text-slate-400 backdrop-blur-sm transition-all duration-200 focus:border-cyan-400 focus:bg-white/10 focus:outline-none focus:ring-2 focus:ring-cyan-400/30"
                 class="w-full rounded-2xl border border-white/20 bg-white/5 px-5 py-3.5 text-base text-white placeholder:text-slate-400 backdrop-blur-sm transition-all duration-200 focus:border-cyan-400 focus:bg-white/10 focus:outline-none focus:ring-2 focus:ring-cyan-400/30"
                 @keyup.enter="handleQrSubmit" />
                 @keyup.enter="handleQrSubmit" />
               <div class="flex items-center gap-2 px-1">
               <div class="flex items-center gap-2 px-1">
@@ -370,14 +431,14 @@ onMounted(() => {
                   <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                   <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                     d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
                     d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
                 </svg>
                 </svg>
-                <span class="text-sm text-slate-400/90">例如:<span
+                <span class="text-sm text-slate-400/90">Example: <span
                     class="font-mono text-slate-300/80">QRMIH76J350CE2CF6150A51F4E</span></span>
                     class="font-mono text-slate-300/80">QRMIH76J350CE2CF6150A51F4E</span></span>
               </div>
               </div>
             </div>
             </div>
             <button type="button"
             <button type="button"
               class="rounded-2xl bg-gradient-to-r from-cyan-400 to-blue-400 px-8 py-3.5 text-base font-semibold text-slate-900 shadow-lg shadow-cyan-500/30 transition-all duration-200 hover:scale-105 hover:shadow-xl hover:shadow-cyan-500/40 sm:whitespace-nowrap"
               class="rounded-2xl bg-gradient-to-r from-cyan-400 to-blue-400 px-8 py-3.5 text-base font-semibold text-slate-900 shadow-lg shadow-cyan-500/30 transition-all duration-200 hover:scale-105 hover:shadow-xl hover:shadow-cyan-500/40 sm:whitespace-nowrap"
               @click="handleQrSubmit">
               @click="handleQrSubmit">
-              查看信息
+              View info
             </button>
             </button>
           </div>
           </div>
         </section>
         </section>
@@ -385,14 +446,14 @@ onMounted(() => {
         <section v-else class="space-y-6">
         <section v-else class="space-y-6">
           <div v-if="infoStatus.state === 'error'" class="rounded-3xl border border-red-500/40 bg-red-500/10 p-5">
           <div v-if="infoStatus.state === 'error'" class="rounded-3xl border border-red-500/40 bg-red-500/10 p-5">
             <p class="text-sm text-red-100">{{ infoStatus.message }}</p>
             <p class="text-sm text-red-100">{{ infoStatus.message }}</p>
-            <p class="mt-2 text-xs text-red-200">请检查二维码编号是否正确,或联系管理员获取帮助。</p>
+            <p class="mt-2 text-xs text-red-200">Please double-check the QR code or reach out for assistance.</p>
           </div>
           </div>
 
 
           <div v-if="loading.info" class="rounded-3xl border border-white/10 bg-white/5 p-6 text-base text-white">
           <div v-if="loading.info" class="rounded-3xl border border-white/10 bg-white/5 p-6 text-base text-white">
-            正在加载二维码信息...
+            Loading QR information...
           </div>
           </div>
 
 
-          <!-- 只有验证通过后才显示内容 -->
+          <!-- Only render details after maintenance verification -->
           <div v-if="infoStatus.state === 'ready' && qrDetail && (maintenancePassed || hasProfile)" class="space-y-6">
           <div v-if="infoStatus.state === 'ready' && qrDetail && (maintenancePassed || hasProfile)" class="space-y-6">
             <div v-if="hasProfile && !isEditing"
             <div v-if="hasProfile && !isEditing"
               class="rounded-3xl border border-white/10 bg-white text-slate-900 shadow-2xl shadow-cyan-500/10 relative">
               class="rounded-3xl border border-white/10 bg-white text-slate-900 shadow-2xl shadow-cyan-500/10 relative">
@@ -406,12 +467,12 @@ onMounted(() => {
                         @error="(e) => e.target.style.display = 'none'" />
                         @error="(e) => e.target.style.display = 'none'" />
                       <div v-else class="flex h-full w-full flex-col items-center justify-center gap-2 text-slate-400">
                       <div v-else class="flex h-full w-full flex-col items-center justify-center gap-2 text-slate-400">
                         <i class="pi pi-image text-4xl" />
                         <i class="pi pi-image text-4xl" />
-                        <span class="text-sm">无照片</span>
+                        <span class="text-sm">No photo</span>
                       </div>
                       </div>
-                      <!-- 编辑按钮 -->
+                      <!-- Edit button -->
                       <button v-if="!isEditing" type="button"
                       <button v-if="!isEditing" type="button"
                         class="absolute top-3 right-3 flex h-9 w-9 items-center justify-center rounded-full bg-white/90 backdrop-blur-sm text-slate-600 hover:bg-white hover:text-slate-900 transition-all shadow-lg hover:shadow-xl"
                         class="absolute top-3 right-3 flex h-9 w-9 items-center justify-center rounded-full bg-white/90 backdrop-blur-sm text-slate-600 hover:bg-white hover:text-slate-900 transition-all shadow-lg hover:shadow-xl"
-                        @click="showMaintenanceDialog = true" title="编辑资料">
+                        @click="showMaintenanceDialog = true" title="Edit profile">
                         <i class="pi pi-pencil text-xs" />
                         <i class="pi pi-pencil text-xs" />
                       </button>
                       </button>
                     </div>
                     </div>
@@ -422,49 +483,81 @@ onMounted(() => {
                   <div class="space-y-4">
                   <div class="space-y-4">
                     <div>
                     <div>
                       <p class="mt-1 text-sm text-slate-500">
                       <p class="mt-1 text-sm text-slate-500">
-                        {{ isPerson ? '紧急联系人卡' : isGoods ? '物品信息' : '宠物信息' }}
+                        {{ isPerson ? 'Emergency contact card' : isGoods ? 'Item information' : 'Pet information' }}
                       </p>
                       </p>
                       <p class="text-2xl font-semibold text-slate-900">
                       <p class="text-2xl font-semibold text-slate-900">
-                        {{ profile?.name || '未命名' }}
+                        {{ profile?.name || 'Unnamed' }}
                       </p>
                       </p>
                     </div>
                     </div>
                     <div
                     <div
                       class="rounded-2xl border border-slate-200 bg-gradient-to-br from-slate-50 to-white p-5 transition hover:shadow-md">
                       class="rounded-2xl border border-slate-200 bg-gradient-to-br from-slate-50 to-white p-5 transition hover:shadow-md">
-                      <div class="flex items-center gap-2">
-                        <div class="flex h-8 w-8 items-center justify-center rounded-full bg-emerald-100">
-                          <i class="pi pi-user text-sm text-emerald-600" />
+                      <div class="flex items-center gap-3">
+                        <div
+                          class="flex h-12 w-12 items-center justify-center rounded-full bg-emerald-100 shadow-inner">
+                          <i class="pi pi-user text-lg text-emerald-600" />
                         </div>
                         </div>
-                        <p class="text-xs font-medium uppercase tracking-wider text-slate-500">{{ isPerson ? '紧急联系人' :
-                          '联系人' }}</p>
+                        <p class="text-[11px] font-semibold uppercase tracking-[0.35em] text-slate-500">{{ isPerson ?
+                          'EMERGENCY CONTACT' : 'CONTACT' }}</p>
                       </div>
                       </div>
-                      <p class="mt-3 text-xl font-semibold text-slate-900">
+                      <p class="mt-3 cursor-pointer text-3xl font-semibold leading-tight text-slate-800" title="Click to copy name"
+                        @click="copyInfo(isPerson ? profile?.emergencyContactName : profile?.contactName, 'Name')">
                         {{ isPerson ? profile?.emergencyContactName || '-' : profile?.contactName || '-' }}
                         {{ isPerson ? profile?.emergencyContactName || '-' : profile?.contactName || '-' }}
                       </p>
                       </p>
-                      <div class="mt-3 space-y-1.5 text-sm text-slate-600">
-                        <div class="flex items-center gap-2">
-                          <i class="pi pi-phone text-xs text-slate-400" />
-                          <span>{{ isPerson ? profile?.emergencyContactPhone || '-' : profile?.contactPhone || '-'
-                            }}</span>
+                      <div class="mt-4 space-y-3 text-sm text-slate-600">
+                        <div class="flex items-center gap-3">
+                          <i class="pi pi-phone text-lg text-slate-500" />
+                          <div class="leading-tight cursor-pointer select-text text-slate-600" title="Click to copy phone"
+                            @click="copyInfo(isPerson ? profile?.emergencyContactPhone : profile?.contactPhone, 'Phone')">
+                            <p class="text-[10px] uppercase tracking-[0.4em] text-slate-400">PHONE</p>
+                            <p class="text-base font-semibold text-slate-700">
+                              {{ isPerson ? profile?.emergencyContactPhone || '-' : profile?.contactPhone || '-' }}
+                            </p>
+                          </div>
                         </div>
                         </div>
                         <div v-if="(isPerson ? profile?.emergencyContactEmail : profile?.contactEmail)"
                         <div v-if="(isPerson ? profile?.emergencyContactEmail : profile?.contactEmail)"
-                          class="flex items-center gap-2">
-                          <i class="pi pi-envelope text-xs text-slate-400" />
-                          <span class="truncate">{{ isPerson ? profile?.emergencyContactEmail : profile?.contactEmail
-                            }}</span>
+                          class="flex items-center gap-3">
+                          <i class="pi pi-envelope text-lg text-slate-500" />
+                          <div class="leading-tight min-w-0 cursor-pointer select-text text-slate-600" title="Click to copy email"
+                            @click="copyInfo(isPerson ? profile?.emergencyContactEmail : profile?.contactEmail, 'Email')">
+                            <p class="text-[10px] uppercase tracking-[0.4em] text-slate-400">EMAIL</p>
+                            <p class="truncate text-base font-semibold text-slate-700">
+                              {{ isPerson ? profile?.emergencyContactEmail : profile?.contactEmail }}
+                            </p>
+                          </div>
+                        </div>
+                        <div v-if="!isPerson && profile?.location" class="flex items-start gap-3">
+                          <i class="pi pi-map-marker text-lg text-slate-500 mt-0.5 flex-shrink-0" />
+                          <div class="leading-tight cursor-pointer select-text text-slate-600" title="Click to copy address"
+                            @click="copyInfo(profile?.location, 'Address')">
+                            <p class="text-[10px] uppercase tracking-[0.4em] text-slate-400">LOCATION</p>
+                            <p class="flex-1 break-words whitespace-normal text-base font-medium text-slate-700">
+                              {{ profile.location }}
+                            </p>
+                          </div>
                         </div>
                         </div>
                       </div>
                       </div>
-                      <div class="mt-4 flex flex-wrap gap-2">
+                      <div class="mt-4 grid grid-cols-2 gap-2">
                         <button v-if="profile?.emergencyContactPhone || profile?.contactPhone" type="button"
                         <button v-if="profile?.emergencyContactPhone || profile?.contactPhone" type="button"
-                          class="inline-flex items-center gap-1.5 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1.5 text-xs font-medium text-emerald-700 transition hover:bg-emerald-100"
+                          class="inline-flex w-full items-center justify-center gap-1.5 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1.5 text-xs font-medium text-emerald-700 transition hover:bg-emerald-100"
                           @click="callNumber(isPerson ? profile?.emergencyContactPhone : profile?.contactPhone)">
                           @click="callNumber(isPerson ? profile?.emergencyContactPhone : profile?.contactPhone)">
-                          <i class="pi pi-phone" /> 拨打
+                          <i class="pi pi-phone" /> Call
                         </button>
                         </button>
                         <button
                         <button
                           v-if="(isPerson && profile?.emergencyContactEmail) || (!isPerson && profile?.contactEmail)"
                           v-if="(isPerson && profile?.emergencyContactEmail) || (!isPerson && profile?.contactEmail)"
                           type="button"
                           type="button"
-                          class="inline-flex items-center gap-1.5 rounded-full border border-blue-200 bg-blue-50 px-3 py-1.5 text-xs font-medium text-blue-700 transition hover:bg-blue-100"
+                          class="inline-flex w-full items-center justify-center gap-1.5 rounded-full border border-blue-200 bg-blue-50 px-3 py-1.5 text-xs font-medium text-blue-700 transition hover:bg-blue-100"
                           @click="sendEmail(isPerson ? profile?.emergencyContactEmail : profile?.contactEmail)">
                           @click="sendEmail(isPerson ? profile?.emergencyContactEmail : profile?.contactEmail)">
-                          <i class="pi pi-envelope" /> 邮件
+                          <i class="pi pi-envelope" /> Email
+                        </button>
+                        <button v-if="!isPerson && profile?.location" type="button"
+                          class="inline-flex w-full items-center justify-center gap-1.5 rounded-full border border-purple-200 bg-purple-50 px-3 py-1.5 text-xs font-medium text-purple-700 transition hover:bg-purple-100"
+                          @click="handleOpenLocationView">
+                          <i class="pi pi-map" /> Map
+                        </button>
+                        <button v-if="!isPerson && profile?.location" type="button"
+                          class="inline-flex w-full items-center justify-center gap-1.5 rounded-full border border-red-200 bg-red-50 px-3 py-1.5 text-xs font-medium text-red-700 transition hover:bg-red-100"
+                          @click="openGoogleMaps">
+                          <i class="pi pi-external-link" /> Google Maps
                         </button>
                         </button>
                       </div>
                       </div>
                     </div>
                     </div>
@@ -473,11 +566,11 @@ onMounted(() => {
                         <div class="flex h-8 w-8 items-center justify-center rounded-full bg-amber-100">
                         <div class="flex h-8 w-8 items-center justify-center rounded-full bg-amber-100">
                           <i class="pi pi-info-circle text-sm text-amber-600" />
                           <i class="pi pi-info-circle text-sm text-amber-600" />
                         </div>
                         </div>
-                        <p class="text-xs font-medium uppercase tracking-wider text-slate-500">{{ isPerson ? '特别说明' :
-                          isGoods ? '备注说明' : '补充描述' }}</p>
+                        <p class="text-xs font-medium uppercase tracking-wider text-slate-500">{{ isPerson ? 'Additional notes' :
+                          isGoods ? 'Item notes' : 'Extra description' }}</p>
                       </div>
                       </div>
                       <p class="mt-3 whitespace-pre-wrap text-sm leading-relaxed text-slate-700">
                       <p class="mt-3 whitespace-pre-wrap text-sm leading-relaxed text-slate-700">
-                        {{ isPerson ? (profile?.specialNote || '暂无说明') : (profile?.remark || '暂无说明') }}
+                        {{ isPerson ? (profile?.specialNote || 'No extra details') : (profile?.remark || 'No extra details') }}
                       </p>
                       </p>
                     </div>
                     </div>
                   </div>
                   </div>
@@ -487,18 +580,17 @@ onMounted(() => {
           </div>
           </div>
         </section>
         </section>
 
 
-        <!-- 只有验证通过后才显示表单 -->
         <section v-if="qrCode && maintenancePassed" id="scan-form-section"
         <section v-if="qrCode && maintenancePassed" id="scan-form-section"
           class="rounded-3xl border border-white/10 bg-white p-6 text-slate-900 shadow-2xl shadow-cyan-500/10">
           class="rounded-3xl border border-white/10 bg-white p-6 text-slate-900 shadow-2xl shadow-cyan-500/10">
           <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
           <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
             <div>
             <div>
               <p class="text-xs uppercase tracking-[0.3em] text-slate-400">Profile setup</p>
               <p class="text-xs uppercase tracking-[0.3em] text-slate-400">Profile setup</p>
               <h2 class="mt-1 text-2xl font-semibold text-slate-900">
               <h2 class="mt-1 text-2xl font-semibold text-slate-900">
-                {{ hasProfile ? '更新资料' : '首次填写资料' }}
+                {{ hasProfile ? 'Update profile' : 'First-time setup' }}
               </h2>
               </h2>
             </div>
             </div>
             <p class="text-sm text-slate-500">
             <p class="text-sm text-slate-500">
-              二维码:<span class="font-mono text-slate-700">{{ qrCode }}</span>
+              QR Code: <span class="font-mono text-slate-700">{{ qrCode }}</span>
             </p>
             </p>
           </div>
           </div>
 
 
@@ -506,9 +598,10 @@ onMounted(() => {
             <div class="flex items-start gap-3">
             <div class="flex items-start gap-3">
               <i class="pi pi-info-circle text-emerald-600" />
               <i class="pi pi-info-circle text-emerald-600" />
               <div class="flex-1 text-sm text-emerald-700">
               <div class="flex-1 text-sm text-emerald-700">
-                <p class="font-semibold">首次填写提示</p>
+                <p class="font-semibold">First-time tip</p>
                 <p class="mt-1">
                 <p class="mt-1">
-                  这是首次为此二维码录入信息,提交后系统会自动激活。请妥善保管维护码,后续修改资料需要验证。
+                  This is the first record for this QR code. Once submitted, it becomes active. Keep the maintenance code
+                  safe for future edits.
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
@@ -517,9 +610,9 @@ onMounted(() => {
             <div class="flex items-start gap-3">
             <div class="flex items-start gap-3">
               <i class="pi pi-check-circle text-cyan-600" />
               <i class="pi pi-check-circle text-cyan-600" />
               <div class="flex-1 text-sm text-cyan-700">
               <div class="flex-1 text-sm text-cyan-700">
-                <p class="font-semibold">编辑模式</p>
+                <p class="font-semibold">Edit mode</p>
                 <p class="mt-1">
                 <p class="mt-1">
-                  您已通过维护码验证,现在可以修改资料了。修改后请记得点击保存按钮。
+                  Maintenance code verified. You can now edit the profile—remember to save your changes.
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
@@ -528,8 +621,8 @@ onMounted(() => {
           <form class="mt-6 space-y-6" @submit.prevent="handleSaveProfile">
           <form class="mt-6 space-y-6" @submit.prevent="handleSaveProfile">
             <div class="space-y-4">
             <div class="space-y-4">
               <div>
               <div>
-                <p class="text-sm font-medium text-slate-700">展示头像 / 实物图片</p>
-                <p class="mt-1 text-xs text-slate-500">建议尺寸 640×640 以上,支持 JPG、PNG,最大 15MB</p>
+                <p class="text-sm font-medium text-slate-700">Display photo / item image</p>
+                <p class="mt-1 text-xs text-slate-500">Recommended 640×640+, supports JPG/PNG, up to 15MB.</p>
               </div>
               </div>
               <div
               <div
                 class="relative w-full max-w-md mx-auto overflow-hidden rounded-2xl bg-slate-100 ring-2 ring-slate-200"
                 class="relative w-full max-w-md mx-auto overflow-hidden rounded-2xl bg-slate-100 ring-2 ring-slate-200"
@@ -542,7 +635,7 @@ onMounted(() => {
                 <div v-else-if="!formData.photoUrl"
                 <div v-else-if="!formData.photoUrl"
                   class="flex h-full w-full flex-col items-center justify-center gap-2 text-slate-400">
                   class="flex h-full w-full flex-col items-center justify-center gap-2 text-slate-400">
                   <i class="pi pi-image text-5xl" />
                   <i class="pi pi-image text-5xl" />
-                  <span class="text-sm">无图片</span>
+                  <span class="text-sm">No image</span>
                 </div>
                 </div>
               </div>
               </div>
               <input ref="photoInputRef" type="file" accept="image/jpeg,image/jpg,image/png,image/webp" class="hidden"
               <input ref="photoInputRef" type="file" accept="image/jpeg,image/jpg,image/png,image/webp" class="hidden"
@@ -552,31 +645,31 @@ onMounted(() => {
                   class="rounded-full border border-slate-300 bg-white px-6 py-2.5 text-sm font-medium text-slate-700 transition hover:bg-slate-50 disabled:opacity-50"
                   class="rounded-full border border-slate-300 bg-white px-6 py-2.5 text-sm font-medium text-slate-700 transition hover:bg-slate-50 disabled:opacity-50"
                   :disabled="loading.photo" @click="triggerPhotoPicker">
                   :disabled="loading.photo" @click="triggerPhotoPicker">
                   <i class="pi mr-2" :class="loading.photo ? 'pi-spin pi-spinner' : 'pi-upload'" />
                   <i class="pi mr-2" :class="loading.photo ? 'pi-spin pi-spinner' : 'pi-upload'" />
-                  {{ loading.photo ? '上传中...' : '上传图片' }}
+                  {{ loading.photo ? 'Uploading...' : 'Upload image' }}
                 </button>
                 </button>
                 <button v-if="formData.photoUrl" type="button"
                 <button v-if="formData.photoUrl" type="button"
                   class="rounded-full border border-red-200 bg-red-50 px-4 py-2.5 text-sm font-medium text-red-600 transition hover:bg-red-100"
                   class="rounded-full border border-red-200 bg-red-50 px-4 py-2.5 text-sm font-medium text-red-600 transition hover:bg-red-100"
                   @click="formData.photoUrl = ''">
                   @click="formData.photoUrl = ''">
                   <i class="pi pi-times mr-1" />
                   <i class="pi pi-times mr-1" />
-                  清除图片
+                  Clear image
                 </button>
                 </button>
               </div>
               </div>
             </div>
             </div>
 
 
             <div class="grid gap-4 md:grid-cols-2">
             <div class="grid gap-4 md:grid-cols-2">
               <label class="space-y-2 text-sm">
               <label class="space-y-2 text-sm">
-                <span class="text-slate-500">{{ isPerson ? '姓名' : isGoods ? '物品名称' : '宠物名称' }}</span>
+                <span class="text-slate-500">{{ isPerson ? 'Name' : isGoods ? 'Item name' : 'Pet name' }}</span>
                 <input v-model="formData.name" type="text" class="w-full rounded-2xl border border-slate-200 px-4 py-3"
                 <input v-model="formData.name" type="text" class="w-full rounded-2xl border border-slate-200 px-4 py-3"
-                  :placeholder="isPerson ? '例如:张三' : isGoods ? '例如:苹果笔记本电脑' : '例如:小白'" required />
+                  :placeholder="isPerson ? 'e.g. John Doe' : isGoods ? 'e.g. MacBook Pro' : 'e.g. Snowy'" required />
               </label>
               </label>
 
 
               <label v-if="isPerson" class="space-y-2 text-sm">
               <label v-if="isPerson" class="space-y-2 text-sm">
-                <span class="text-slate-500">性别</span>
+                <span class="text-slate-500">Gender</span>
                 <div class="flex gap-2">
                 <div class="flex gap-2">
                   <button v-for="option in [
                   <button v-for="option in [
-                    { label: '', value: 'male' },
-                    { label: '', value: 'female' },
-                    { label: '保密', value: 'unknown' }
+                    { label: 'Male', value: 'male' },
+                    { label: 'Female', value: 'female' },
+                    { label: 'Prefer not to say', value: 'unknown' }
                   ]" :key="option.value" type="button" class="flex-1 rounded-2xl border px-4 py-3 text-sm"
                   ]" :key="option.value" type="button" class="flex-1 rounded-2xl border px-4 py-3 text-sm"
                     :class="formData.gender === option.value ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-200 text-slate-500'"
                     :class="formData.gender === option.value ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-200 text-slate-500'"
                     @click="formData.gender = option.value">
                     @click="formData.gender = option.value">
@@ -588,67 +681,82 @@ onMounted(() => {
 
 
             <div v-if="isPerson" class="grid gap-4 md:grid-cols-2">
             <div v-if="isPerson" class="grid gap-4 md:grid-cols-2">
               <label class="space-y-2 text-sm">
               <label class="space-y-2 text-sm">
-                <span class="text-slate-500">本人电话</span>
+                <span class="text-slate-500">Owner phone</span>
                 <input v-model="formData.phone" type="tel" class="w-full rounded-2xl border border-slate-200 px-4 py-3"
                 <input v-model="formData.phone" type="tel" class="w-full rounded-2xl border border-slate-200 px-4 py-3"
                   required />
                   required />
               </label>
               </label>
 
 
               <label class="space-y-2 text-sm">
               <label class="space-y-2 text-sm">
-                <span class="text-slate-500">紧急联系人姓名</span>
+                <span class="text-slate-500">Emergency contact name</span>
                 <input v-model="formData.emergencyContactName" type="text"
                 <input v-model="formData.emergencyContactName" type="text"
                   class="w-full rounded-2xl border border-slate-200 px-4 py-3" required />
                   class="w-full rounded-2xl border border-slate-200 px-4 py-3" required />
               </label>
               </label>
 
 
               <label class="space-y-2 text-sm">
               <label class="space-y-2 text-sm">
-                <span class="text-slate-500">紧急联系人电话</span>
+                <span class="text-slate-500">Emergency contact phone</span>
                 <input v-model="formData.emergencyContactPhone" type="tel"
                 <input v-model="formData.emergencyContactPhone" type="tel"
                   class="w-full rounded-2xl border border-slate-200 px-4 py-3" required />
                   class="w-full rounded-2xl border border-slate-200 px-4 py-3" required />
               </label>
               </label>
 
 
               <label class="space-y-2 text-sm md:col-span-2">
               <label class="space-y-2 text-sm md:col-span-2">
-                <span class="text-slate-500">紧急联系人邮箱</span>
+                <span class="text-slate-500">Emergency contact email</span>
                 <input v-model="formData.emergencyContactEmail" type="email"
                 <input v-model="formData.emergencyContactEmail" type="email"
-                  class="w-full rounded-2xl border border-slate-200 px-4 py-3" placeholder="可选" />
+                  class="w-full rounded-2xl border border-slate-200 px-4 py-3" placeholder="Optional" />
               </label>
               </label>
             </div>
             </div>
 
 
             <div v-else class="grid gap-4 md:grid-cols-2">
             <div v-else class="grid gap-4 md:grid-cols-2">
               <label class="space-y-2 text-sm">
               <label class="space-y-2 text-sm">
-                <span class="text-slate-500">联系人姓名</span>
+                <span class="text-slate-500">Contact name</span>
                 <input v-model="formData.contactName" type="text"
                 <input v-model="formData.contactName" type="text"
                   class="w-full rounded-2xl border border-slate-200 px-4 py-3" required />
                   class="w-full rounded-2xl border border-slate-200 px-4 py-3" required />
               </label>
               </label>
 
 
               <label class="space-y-2 text-sm">
               <label class="space-y-2 text-sm">
-                <span class="text-slate-500">联系人电话</span>
+                <span class="text-slate-500">Contact phone</span>
                 <input v-model="formData.contactPhone" type="tel"
                 <input v-model="formData.contactPhone" type="tel"
                   class="w-full rounded-2xl border border-slate-200 px-4 py-3" required />
                   class="w-full rounded-2xl border border-slate-200 px-4 py-3" required />
               </label>
               </label>
 
 
               <label class="space-y-2 text-sm md:col-span-2">
               <label class="space-y-2 text-sm md:col-span-2">
-                <span class="text-slate-500">联系人邮箱</span>
+                <span class="text-slate-500">Contact email</span>
                 <input v-model="formData.contactEmail" type="email"
                 <input v-model="formData.contactEmail" type="email"
-                  class="w-full rounded-2xl border border-slate-200 px-4 py-3" placeholder="可选" />
+                  class="w-full rounded-2xl border border-slate-200 px-4 py-3" placeholder="Optional" />
+              </label>
+
+              <label class="space-y-2 text-sm md:col-span-2">
+                <span class="text-slate-500">Contact address</span>
+                <div class="flex gap-2">
+                  <input v-model="formData.location" type="text" readonly
+                    class="flex-1 rounded-2xl border border-slate-200 px-4 py-3 bg-slate-50 cursor-pointer"
+                    placeholder="Click to select address" @click="handleOpenLocationDialog" />
+                  <button type="button"
+                    class="rounded-2xl border border-slate-300 bg-white px-6 py-3 text-sm font-medium text-slate-700 transition hover:bg-slate-50 whitespace-nowrap"
+                    @click="handleOpenLocationDialog">
+                    <i class="pi pi-map-marker mr-2" />
+                    Select address
+                  </button>
+                </div>
               </label>
               </label>
             </div>
             </div>
 
 
             <label class="block space-y-2 text-sm">
             <label class="block space-y-2 text-sm">
-              <span class="text-slate-500">{{ isPerson ? '特别说明 / 健康提示' : isGoods ? '备注说明' : '补充描述 / 备注' }}</span>
+              <span class="text-slate-500">{{ isPerson ? 'Additional notes / health tips' : isGoods ? 'Item notes' : 'Extra remarks' }}</span>
               <textarea v-if="isPerson" v-model="formData.specialNote" rows="4"
               <textarea v-if="isPerson" v-model="formData.specialNote" rows="4"
-                class="w-full rounded-2xl border border-slate-200 px-4 py-3" placeholder="例如过敏药物、携带物品等" />
+                class="w-full rounded-2xl border border-slate-200 px-4 py-3" placeholder="e.g. allergies, medical needs, carried items" />
               <textarea v-else v-model="formData.remark" rows="4"
               <textarea v-else v-model="formData.remark" rows="4"
                 class="w-full rounded-2xl border border-slate-200 px-4 py-3"
                 class="w-full rounded-2xl border border-slate-200 px-4 py-3"
-                :placeholder="isGoods ? '例如物品特征、使用说明、联系注意事项等' : '例如宠物习性、健康状况等'" />
+                :placeholder="isGoods ? 'e.g. item features, usage notes, handling tips' : 'e.g. pet habits, health info, behavior notes'" />
             </label>
             </label>
 
 
             <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
             <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
               <p class="text-sm text-slate-500">
               <p class="text-sm text-slate-500">
-                提交后将同步到扫码页面。
+                Submitted data will sync to the public scan page.
               </p>
               </p>
               <button type="submit"
               <button type="submit"
                 class="rounded-full bg-slate-900 px-8 py-3 text-sm font-semibold text-white hover:bg-slate-800"
                 class="rounded-full bg-slate-900 px-8 py-3 text-sm font-semibold text-white hover:bg-slate-800"
                 :disabled="loading.saving">
                 :disabled="loading.saving">
-                {{ loading.saving ? '保存中...' : hasProfile ? '保存更新' : '提交资料' }}
+                {{ loading.saving ? 'Saving...' : hasProfile ? 'Save changes' : 'Submit profile' }}
               </button>
               </button>
             </div>
             </div>
           </form>
           </form>
@@ -656,16 +764,23 @@ onMounted(() => {
 
 
         <footer class="mt-8 border-t border-white/10 pt-6 text-center">
         <footer class="mt-8 border-t border-white/10 pt-6 text-center">
           <p class="text-xs text-slate-400">
           <p class="text-xs text-slate-400">
-            数据仅用于紧急联系场景,上传即视为同意存储并用于扫码展示
+            Data is used solely for emergency contact scenarios. Uploading means you consent to display it when scanned.
           </p>
           </p>
           <p class="mt-2 text-xs text-slate-500">
           <p class="mt-2 text-xs text-slate-500">
-            <i class="pi pi-shield text-cyan-400" /> 您的隐私受到保护
+            <i class="pi pi-shield text-cyan-400" /> Your privacy stays protected
           </p>
           </p>
         </footer>
         </footer>
       </div>
       </div>
     </div>
     </div>
 
 
-    <!-- 维护码验证弹窗 -->
+    <!-- Location picker dialog -->
+    <LocationPicker v-model="showLocationDialog" :initial-location="formData.location" @save="handleSaveLocation" />
+
+    <!-- Location preview dialog (read-only) -->
+    <LocationPicker v-model="showLocationViewDialog" :initial-location="isPerson ? null : profile?.location"
+      :closable="true" :readonly="true" />
+
+    <!-- Maintenance code dialog -->
     <Dialog v-model:visible="showMaintenanceDialog" modal :closable="!isFirstFill" :closeOnEscape="!isFirstFill"
     <Dialog v-model:visible="showMaintenanceDialog" modal :closable="!isFirstFill" :closeOnEscape="!isFirstFill"
       :dismissableMask="!isFirstFill" :style="{ width: '90vw', maxWidth: '450px' }" :draggable="false">
       :dismissableMask="!isFirstFill" :style="{ width: '90vw', maxWidth: '450px' }" :draggable="false">
       <template #header>
       <template #header>
@@ -675,10 +790,10 @@ onMounted(() => {
           </div>
           </div>
           <div>
           <div>
             <h3 class="text-lg font-semibold text-slate-900">
             <h3 class="text-lg font-semibold text-slate-900">
-              {{ isFirstFill ? '首次使用需要验证' : '编辑资料需要验证' }}
+              {{ isFirstFill ? 'Verification required for first use' : 'Verification required to edit' }}
             </h3>
             </h3>
             <p class="text-sm text-slate-500">
             <p class="text-sm text-slate-500">
-              {{ isFirstFill ? '请输入二维码附带的维护码' : '请输入维护码以解锁编辑权限' }}
+              {{ isFirstFill ? 'Enter the maintenance code that came with the QR tag.' : 'Enter the maintenance code to unlock editing.' }}
             </p>
             </p>
           </div>
           </div>
         </div>
         </div>
@@ -686,16 +801,16 @@ onMounted(() => {
 
 
       <div class="space-y-4 py-4">
       <div class="space-y-4 py-4">
         <div>
         <div>
-          <label class="mb-2 block text-sm font-medium text-slate-700">维护码</label>
+          <label class="mb-2 block text-sm font-medium text-slate-700">Maintenance code</label>
           <input v-model="maintenanceCode" type="text" maxlength="8"
           <input v-model="maintenanceCode" type="text" maxlength="8"
             class="w-full rounded-xl border border-slate-300 px-4 py-3 text-slate-900 placeholder:text-slate-400 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20"
             class="w-full rounded-xl border border-slate-300 px-4 py-3 text-slate-900 placeholder:text-slate-400 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20"
-            placeholder="输入 8 位维护码" @keyup.enter="handleVerifyMaintenance" autofocus />
+            placeholder="Enter the maintenance code" @keyup.enter="handleVerifyMaintenance" autofocus />
         </div>
         </div>
         <div v-if="isFirstFill" class="rounded-xl border border-emerald-200 bg-emerald-50 p-3">
         <div v-if="isFirstFill" class="rounded-xl border border-emerald-200 bg-emerald-50 p-3">
           <div class="flex items-start gap-2">
           <div class="flex items-start gap-2">
             <i class="pi pi-info-circle text-sm text-emerald-600" />
             <i class="pi pi-info-circle text-sm text-emerald-600" />
             <p class="text-xs text-emerald-700">
             <p class="text-xs text-emerald-700">
-              这是首次为此二维码录入信息,验证后即可开始填写资料。
+              This is the first record for this QR code. Verify to begin entering details.
             </p>
             </p>
           </div>
           </div>
         </div>
         </div>
@@ -706,12 +821,12 @@ onMounted(() => {
           <button v-if="!isFirstFill" type="button"
           <button v-if="!isFirstFill" type="button"
             class="rounded-xl border border-slate-300 px-5 py-2.5 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
             class="rounded-xl border border-slate-300 px-5 py-2.5 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
             @click="showMaintenanceDialog = false">
             @click="showMaintenanceDialog = false">
-            取消
+            Cancel
           </button>
           </button>
           <button type="button"
           <button type="button"
             class="rounded-xl bg-cyan-600 px-6 py-2.5 text-sm font-semibold text-white transition hover:bg-cyan-700 disabled:opacity-50"
             class="rounded-xl bg-cyan-600 px-6 py-2.5 text-sm font-semibold text-white transition hover:bg-cyan-700 disabled:opacity-50"
             :disabled="loading.verifying || !maintenanceCode" @click="handleVerifyMaintenance">
             :disabled="loading.verifying || !maintenanceCode" @click="handleVerifyMaintenance">
-            {{ loading.verifying ? '验证中...' : '验证' }}
+            {{ loading.verifying ? 'Verifying...' : 'Verify' }}
           </button>
           </button>
         </div>
         </div>
       </template>
       </template>

+ 5 - 0
yarn.lock

@@ -1932,6 +1932,11 @@ kolorist@^1.8.0:
   resolved "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz"
   resolved "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz"
   integrity sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==
   integrity sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==
 
 
+leaflet@^1.9.4:
+  version "1.9.4"
+  resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.9.4.tgz#23fae724e282fa25745aff82ca4d394748db7d8d"
+  integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==
+
 less@^4.2.2:
 less@^4.2.2:
   version "4.2.2"
   version "4.2.2"
   resolved "https://registry.npmmirror.com/less/-/less-4.2.2.tgz"
   resolved "https://registry.npmmirror.com/less/-/less-4.2.2.tgz"