let visitMap = null; let visitMarkers = []; let currentFilter = "all"; let amapKey = null; let amapSecurityJsCode = ""; let amapScriptLoaded = false; let selectedCustomer = null; let modalResolver = null; let appSidebarOpen = false; function setAuthView(loggedIn) { const authWrap = document.getElementById("authWrap"); const appWrap = document.getElementById("appWrap"); if (authWrap) authWrap.classList.toggle("hidden", loggedIn); if (appWrap) appWrap.classList.toggle("hidden", !loggedIn); if (loggedIn && !visitMap) { setTimeout(() => { initVisitMap(); }, 300); } } function setAppSidebarOpen(open) { appSidebarOpen = !!open; const sidebar = document.querySelector("#appWrap .sidebar"); const overlay = document.getElementById("appSidebarOverlay"); if (sidebar) sidebar.classList.toggle("open", appSidebarOpen); if (overlay) overlay.classList.toggle("show", appSidebarOpen); } function bindAppSidebarDrawer() { const menuBtn = document.getElementById("appMenuBtn"); const overlay = document.getElementById("appSidebarOverlay"); if (menuBtn) menuBtn.addEventListener("click", () => setAppSidebarOpen(!appSidebarOpen)); if (overlay) overlay.addEventListener("click", () => setAppSidebarOpen(false)); } function showCenterModal({ title = "提示", message = "", confirmText = "确认", cancelText = "取消", showCancel = true }) { const overlay = document.getElementById("appModal"); const titleEl = document.getElementById("appModalTitle"); const bodyEl = document.getElementById("appModalBody"); const actions = document.getElementById("appModalActions"); const cancelBtn = document.getElementById("appModalCancelBtn"); const confirmBtn = document.getElementById("appModalConfirmBtn"); titleEl.textContent = title; bodyEl.textContent = message; cancelBtn.textContent = cancelText; confirmBtn.textContent = confirmText; cancelBtn.style.display = showCancel ? "block" : "none"; actions.classList.toggle("single", !showCancel); overlay.classList.add("show"); return new Promise((resolve) => { modalResolver = resolve; }); } function closeCenterModal(result) { const overlay = document.getElementById("appModal"); overlay.classList.remove("show"); if (modalResolver) { modalResolver(result); modalResolver = null; } } async function loadConfig() { try { const config = await api("/config"); amapKey = config.amapKey; amapSecurityJsCode = config.amapSecurityJsCode || ""; window.AMAP_KEY = amapKey; console.log("高德地图 Key 加载成功"); return true; } catch (e) { console.error("获取高德 Key 失败,使用默认值", e); amapKey = "3e6daab967b17e5ccaba579de91c7845"; amapSecurityJsCode = ""; window.AMAP_KEY = amapKey; return false; } } function ensureAMapLoaded() { return new Promise((resolve, reject) => { if (window.AMap) { amapScriptLoaded = true; resolve(); return; } if (amapScriptLoaded) { resolve(); return; } if (amapSecurityJsCode) { window._AMapSecurityConfig = { securityJsCode: amapSecurityJsCode }; } const script = document.createElement("script"); script.src = `https://webapi.amap.com/maps?v=2.0&key=${encodeURIComponent(amapKey || "3e6daab967b17e5ccaba579de91c7845")}`; script.async = true; script.onload = () => { amapScriptLoaded = true; resolve(); }; script.onerror = () => reject(new Error("高德地图 SDK 加载失败")); document.head.appendChild(script); }); } async function initVisitMap() { if (visitMap) return; try { await ensureAMapLoaded(); } catch (e) { console.error(e); return; } visitMap = new AMap.Map("visitMap", { center: [121.4737, 31.2304], zoom: 11, mapStyle: "amap://styles/normal", zoomControl: true, scaleControl: true, resizeEnable: true }); console.log("高德地图初始化成功"); if (window.lastVisitData && window.lastVisitData.length > 0) { renderVisitMap(window.lastVisitData); } } function colorToAMap(statusColor) { if (statusColor === "green") return "#188038"; if (statusColor === "yellow") return "#fbbc04"; if (statusColor === "half-yellow-half-green") return "#188038"; return "#d93025"; } function markerFill(statusColor) { if (statusColor === "half-yellow-half-green") { return "linear-gradient(90deg, #fbbc04 50%, #188038 50%)"; } return colorToAMap(statusColor); } function renderVisitMap(items) { if (!visitMap) { console.warn("地图尚未初始化"); return; } if (visitMarkers.length > 0) { visitMap.remove(visitMarkers); visitMarkers = []; } const filtered = items.filter(item => { if (currentFilter === "all") return true; if (currentFilter === "red" && item.status_color === "red") return true; if (currentFilter === "green" && item.status_color === "green") return true; if (currentFilter === "yellow" && item.status_color === "yellow") return true; if (currentFilter === "half" && item.status_color === "half-yellow-half-green") return true; return false; }); console.log(`渲染 ${filtered.length} 个点 (筛选: ${currentFilter})`); const newMarkers = []; filtered.forEach((item) => { if (!item.latitude || !item.longitude) return; const marker = new AMap.Marker({ position: [parseFloat(item.longitude), parseFloat(item.latitude)], title: item.customer_name || "未知客户", content: `
` }); if (item.marker_photo_url) { marker.setLabel({ content: ``, direction: "top", offset: new AMap.Pixel(0, -42) }); } marker.on('click', function() { selectedCustomer = item; resetVisitPhotoInputs(); updateSelectedCustomerInfo(); const infoWindow = new AMap.InfoWindow({ content: ` ${item.customer_name || '未知客户'}
许可证: ${item.license_no}
状态: ${item.status_color}
坐标: ${parseFloat(item.latitude).toFixed(5)}, ${parseFloat(item.longitude).toFixed(5)} `, offset: new AMap.Pixel(0, -30) }); infoWindow.open(visitMap, marker.getPosition()); }); visitMap.add(marker); newMarkers.push(marker); }); visitMarkers = newMarkers; if (newMarkers.length > 0) { visitMap.setFitView(newMarkers, false, [40, 40, 40, 40]); } else { visitMap.setZoomAndCenter(11, [121.4737, 31.2304]); if (items.length === 0) { new AMap.Marker({ position: [121.4737, 31.2304], content: '
暂无客户数据
请先在「客户采集」中添加客户
' }).setMap(visitMap); } } } function resetVisitPhotoInputs() { const startInput = document.getElementById("startPhotoInput"); const endInput = document.getElementById("endPhotoInput"); const startPreview = document.getElementById("startPhotoPreview"); const endPreview = document.getElementById("endPhotoPreview"); if (startInput) startInput.value = ""; if (endInput) endInput.value = ""; if (startPreview) startPreview.style.display = "none"; if (endPreview) endPreview.style.display = "none"; } function updateVisitActionControls() { const startInput = document.getElementById("startPhotoInput"); const endInput = document.getElementById("endPhotoInput"); const startBtn = document.getElementById("startVisitBtn"); const endBtn = document.getElementById("endVisitBtn"); const hasSelected = !!selectedCustomer; const hasOpen = hasSelected && !!selectedCustomer.has_open_visit; const hasStartFile = !!(startInput && startInput.files && startInput.files[0]); const hasEndFile = !!(endInput && endInput.files && endInput.files[0]); if (startInput) startInput.disabled = !hasSelected || hasOpen; if (startBtn) startBtn.disabled = !hasSelected || hasOpen || !hasStartFile; // 未上传第一张照片前,第二张上传入口不可用;进行中走访才允许上传第二张 if (endInput) endInput.disabled = !hasSelected || !hasOpen; // 未上传第二张照片前,结束签到不可用 if (endBtn) endBtn.disabled = !hasSelected || !hasOpen || !hasEndFile; } function bindFilterButtons() { const container = document.getElementById("statusFilter"); if (!container) return; container.querySelectorAll("button").forEach(btn => { btn.addEventListener("click", () => { container.querySelectorAll("button").forEach(b => b.classList.remove("active")); btn.classList.add("active"); currentFilter = btn.getAttribute("data-status"); if (window.lastVisitData) { renderVisitMap(window.lastVisitData); } }); }); } function updateSelectedCustomerInfo() { const el = document.getElementById("selectedCustomerInfo"); if (!el) return; if (!selectedCustomer) { el.textContent = "请点击地图上的客户标点后进行走访签到。"; updateVisitActionControls(); return; } const hasOpen = !!selectedCustomer.has_open_visit; el.textContent = hasOpen ? `已选择客户:${selectedCustomer.customer_name}(${selectedCustomer.license_no}),存在进行中走访,请上传第二张照片后结束签到。` : `已选择客户:${selectedCustomer.customer_name}(${selectedCustomer.license_no})`; updateVisitActionControls(); } function fileToDataUrl(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(file); }); } function loadImageFromDataUrl(dataUrl) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => reject(new Error("图片读取失败")); img.src = dataUrl; }); } function canvasToDataUrl(canvas, quality) { return canvas.toDataURL("image/jpeg", quality); } async function compressImageDataUrl(dataUrl, options = {}) { const { maxWidth = 1600, maxHeight = 1600, targetBytes = 900 * 1024, minQuality = 0.5, startQuality = 0.82 } = options; const img = await loadImageFromDataUrl(dataUrl); const scale = Math.min(1, maxWidth / img.width, maxHeight / img.height); const width = Math.max(1, Math.round(img.width * scale)); const height = Math.max(1, Math.round(img.height * scale)); const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d"); if (!ctx) return dataUrl; ctx.drawImage(img, 0, 0, width, height); let quality = startQuality; let out = canvasToDataUrl(canvas, quality); while (out.length * 0.75 > targetBytes && quality > minQuality) { quality = Math.max(minQuality, quality - 0.08); out = canvasToDataUrl(canvas, quality); } return out; } async function fileToCompressedDataUrl(file) { const raw = await fileToDataUrl(file); return compressImageDataUrl(raw); } function bindPhotoPreview(inputId, previewId) { const input = document.getElementById(inputId); const preview = document.getElementById(previewId); if (!input || !preview) return; input.addEventListener("change", () => { const file = input.files && input.files[0]; if (!file) { preview.style.display = "none"; updateVisitActionControls(); return; } const url = URL.createObjectURL(file); preview.src = url; preview.style.display = "block"; updateVisitActionControls(); }); } async function startVisit() { try { if (!selectedCustomer) throw new Error("请先点击地图上的客户"); if (selectedCustomer.has_open_visit) throw new Error("该客户已有未完成走访,不能重复开始"); const file = document.getElementById("startPhotoInput").files?.[0]; if (!file) throw new Error("开始签到必须拍第一张照片"); const startPhotoUrl = await fileToCompressedDataUrl(file); await api("/visits/start", { method: "POST", body: JSON.stringify({ customer_id: selectedCustomer.customer_id, start_photo_url: startPhotoUrl, latitude: selectedCustomer.latitude, longitude: selectedCustomer.longitude }) }); document.getElementById("visitActionMsg").innerText = "开始走访成功,请完成后拍第二张照片结束签到。"; await loadVisitManagement(); } catch (e) { document.getElementById("visitActionMsg").innerText = e.message || "开始走访失败"; } } async function endVisit() { try { if (!selectedCustomer) throw new Error("请先点击地图上的客户"); if (!selectedCustomer.has_open_visit) throw new Error("该客户没有进行中的走访"); const file = document.getElementById("endPhotoInput").files?.[0]; if (!file) throw new Error("结束签到必须拍第二张照片"); const endPhotoUrl = await fileToCompressedDataUrl(file); const result = await api("/visits/end", { method: "POST", body: JSON.stringify({ customer_id: selectedCustomer.customer_id, end_photo_url: endPhotoUrl }) }); document.getElementById("visitActionMsg").innerText = `结束走访成功,停留时长:${result.duration_text}`; document.getElementById("startPhotoInput").value = ""; document.getElementById("endPhotoInput").value = ""; document.getElementById("startPhotoPreview").style.display = "none"; document.getElementById("endPhotoPreview").style.display = "none"; await loadVisitManagement(); updateVisitActionControls(); } catch (e) { document.getElementById("visitActionMsg").innerText = e.message || "结束走访失败"; } } function bindTabs() { document.querySelectorAll(".menu-btn[data-tab]").forEach((btn) => { btn.addEventListener("click", () => { document.querySelectorAll(".menu-btn[data-tab]").forEach((b) => b.classList.remove("active")); btn.classList.add("active"); const tab = btn.getAttribute("data-tab"); document.getElementById("tabCollect").classList.toggle("hidden", tab !== "collect"); document.getElementById("tabVisit").classList.toggle("hidden", tab !== "visit"); setAppSidebarOpen(false); if (tab === "visit") { setTimeout(async () => { if (!visitMap) { await initVisitMap(); } if (visitMap) { visitMap.resize(); } if (window.lastVisitData && window.lastVisitData.length > 0) { renderVisitMap(window.lastVisitData); } else { await loadVisitManagement(); } }, 120); } }); }); } function switchAuthPane(mode) { const isLogin = mode === "login"; document.getElementById("loginPane").classList.toggle("hidden", !isLogin); document.getElementById("registerPane").classList.toggle("hidden", isLogin); document.getElementById("showLoginBtn").classList.toggle("active", isLogin); document.getElementById("showRegisterBtn").classList.toggle("active", !isLogin); } async function loginByPhone() { try { const phone = document.getElementById("loginPhone").value.trim(); const password = document.getElementById("loginPassword").value.trim(); if (!phone || !password) { document.getElementById("loginMsg").innerText = "请输入手机号和密码"; return; } const data = await api("/auth/login", { method: "POST", body: JSON.stringify({ phone, password }) }); setToken(data.access_token); const me = await getMe(true); if (me.role !== "inspector") { clearAuth(); throw new Error("请使用手机端巡检账号登录"); } document.getElementById("loginPhone").value = ""; document.getElementById("loginPassword").value = ""; setAuthView(true); await initAfterLogin(); document.getElementById("loginMsg").innerText = "登录成功"; } catch (e) { document.getElementById("loginMsg").innerText = e.message; } } async function registerByPhone() { try { const phone = document.getElementById("registerPhone").value.trim(); const password = document.getElementById("registerPassword").value.trim(); await api("/auth/register", { method: "POST", body: JSON.stringify({ phone, password }) }); document.getElementById("registerMsg").innerText = "注册成功,请使用新账号登录"; document.getElementById("registerPhone").value = ""; document.getElementById("registerPassword").value = ""; document.getElementById("loginPhone").value = phone; document.getElementById("loginPassword").value = ""; switchAuthPane("login"); document.getElementById("loginMsg").innerText = "请登录"; } catch (e) { document.getElementById("registerMsg").innerText = e.message; } } async function collectCustomer() { try { const license_prefix = document.getElementById("licensePrefix").value; const license_suffix = document.getElementById("licenseSuffix").value.trim(); const is_key_customer = !!document.getElementById("isKeyCustomer").checked; if (!license_suffix) throw new Error("请先输入许可证号后缀"); const goCollect = await showCenterModal({ title: "确认采集", message: "确认开始采集当前位置并保存该客户吗?", confirmText: "确认保存", cancelText: "取消", showCancel: true }); if (!goCollect) { document.getElementById("collectMsg").innerText = "已取消采集"; return; } const { latitude, longitude } = await getAccurateLocation(); let row; try { row = await api("/customers/collect", { method: "POST", body: JSON.stringify({ license_prefix, license_suffix, latitude, longitude, is_key_customer }) }); } catch (e) { if (e.status === 409) { const yes = await showCenterModal({ title: "重复采集确认", message: "该许可证号已采集过,是否重新采集并覆盖当前位置?", confirmText: "覆盖保存", cancelText: "取消", showCancel: true }); if (!yes) { document.getElementById("collectMsg").innerText = "已取消重新采集"; return; } row = await api("/customers/collect", { method: "POST", body: JSON.stringify({ license_prefix, license_suffix, latitude, longitude, force: true, is_key_customer }) }); } else { throw e; } } document.getElementById("collectMsg").innerText = row.recollected ? `重新采集成功:${row.license_no},新坐标(${row.latitude}, ${row.longitude})` : `采集成功:${row.license_no},坐标(${row.latitude}, ${row.longitude})`; await showCenterModal({ title: "保存成功", message: "客户位置已保存。", confirmText: "知道了", showCancel: false }); document.getElementById("isKeyCustomer").checked = false; await loadVisitManagement(); } catch (e) { document.getElementById("collectMsg").innerText = e.message || "采集失败,请检查定位权限"; } } async function getAccurateLocation() { await ensureAMapLoaded(); return new Promise((resolve, reject) => { AMap.plugin("AMap.Geolocation", () => { const geolocation = new AMap.Geolocation({ enableHighAccuracy: true, timeout: 20000, maximumAge: 0, convert: true, useNative: true, GeoLocationFirst: true, noIpLocate: 1, noGeoLocation: 0 }); geolocation.getCurrentPosition((status, result) => { if (status === "complete" && result && result.position) { const locationType = String(result.location_type || "").toLowerCase(); const accuracy = Number(result.accuracy); // 拒绝IP粗定位,避免偏到城市中心 if (locationType.includes("ip")) { reject(new Error("当前仅获取到IP粗定位,坐标不准确。请开启GPS并重试")); return; } // 精度阈值:超过150米视为不可靠 if (Number.isFinite(accuracy) && accuracy > 150) { reject(new Error(`定位精度不足(约${Math.round(accuracy)}米),请到开阔区域重试`)); return; } const lat = typeof result.position.getLat === "function" ? result.position.getLat() : result.position.lat; const lng = typeof result.position.getLng === "function" ? result.position.getLng() : result.position.lng; if (Number.isFinite(lat) && Number.isFinite(lng)) { console.log("定位成功", { lat, lng, accuracy: result.accuracy, locationType: result.location_type }); resolve({ latitude: lat, longitude: lng }); return; } } reject(new Error("未获取到精准定位,请开启GPS并授予定位权限")); }); }); }); } async function loadVisitManagement() { try { const data = await api("/visit-management"); window.lastVisitData = data.items || []; if (selectedCustomer) { const refreshed = window.lastVisitData.find((x) => x.customer_id === selectedCustomer.customer_id); selectedCustomer = refreshed || null; updateSelectedCustomerInfo(); } console.log("Loaded visit data:", window.lastVisitData.length, "customers"); renderVisitMap(window.lastVisitData); } catch (e) { console.error("Failed to load visit management:", e); } } async function initAfterLogin() { try { await loadConfig(); await ensureAMapLoaded(); await loadVisitManagement(); } catch (e) { console.error("Init after login failed:", e); if (e.message.includes("登录") || e.message.includes("token")) { clearAuth(); setAuthView(false); } } } document.getElementById("loginBtn").addEventListener("click", loginByPhone); document.getElementById("registerBtn").addEventListener("click", registerByPhone); document.getElementById("showLoginBtn").addEventListener("click", () => switchAuthPane("login")); document.getElementById("showRegisterBtn").addEventListener("click", () => switchAuthPane("register")); document.getElementById("collectBtn").addEventListener("click", collectCustomer); document.getElementById("refreshVisitBtn").addEventListener("click", loadVisitManagement); document.getElementById("startVisitBtn").addEventListener("click", startVisit); document.getElementById("endVisitBtn").addEventListener("click", endVisit); document.getElementById("logoutBtn").addEventListener("click", () => { clearAuth(); setAuthView(false); setAppSidebarOpen(false); }); document.getElementById("appModalConfirmBtn").addEventListener("click", () => closeCenterModal(true)); document.getElementById("appModalCancelBtn").addEventListener("click", () => closeCenterModal(false)); document.getElementById("appModal").addEventListener("click", (e) => { if (e.target === document.getElementById("appModal")) { closeCenterModal(false); } }); bindTabs(); bindAppSidebarDrawer(); bindFilterButtons(); bindPhotoPreview("startPhotoInput", "startPhotoPreview"); bindPhotoPreview("endPhotoInput", "endPhotoPreview"); switchAuthPane("login"); updateSelectedCustomerInfo(); updateVisitActionControls(); if (getToken()) { getMe(true) .then(async (me) => { if (!me || me.role !== "inspector") { clearAuth(); setAuthView(false); document.getElementById("loginMsg").innerText = "请使用手机端巡检账号登录"; return; } await initAfterLogin(); if (getToken()) setAuthView(true); else setAuthView(false); }) .catch(() => { clearAuth(); setAuthView(false); }); }