感谢原作者大佬Github
https://github.com/zhuoershang/movecar
先展示



核心功能
多用户隔离系统:通过URL参数
?u=用户名区分不同车主,每个用户独立配置30分钟断点续传:用户刷新页面后自动恢复会话,无需重新发送通知
域名优先级二维码:支持
EXTERNAL_URL自定义域名生成二维码仅限国内访问:非中国IP返回Access Denied
车牌验证(T9虚拟键盘)
后4位验证:通过环境变量
CAR_TITLE设置完整车牌,自动提取后4位作为验证码T9虚拟键盘:
短按数字键(0-9):直接输入数字
长按2-9键:弹出字母选择浮层,手指滑动选择字母,松手确认
删除键逐位删除
极速响应,直接操作DOM元素
验证状态持久化:通过
sessionStorage缓存验证结果,刷新页面无需重新验证
定位功能
自动获取定位:页面加载后自动请求GPS定位
30秒倒计时:定位失败后显示倒计时,30秒后才能发送通知
重新获取按钮:定位失败后可手动重新获取
WGS84转GCJ02:自动转换坐标系,适配高德和苹果地图
通知发送
一键通知车主:留言+位置信息发送给车主
快捷留言标签:预设常用留言模板
通知发送频率限制:同一用户1分钟内只能发送1次
多渠道通知
PushPlus:微信公众号推送
WxPusher:微信推送(支持HTML格式+地图链接)
Bark:iOS推送
Email:通过Resend API发送邮件
FlareMsg:自建消息推送
车主回复
车主确认页面:点击通知链接进入,可查看扫码者位置
一键回复:预设回复模板(马上到、暂时无法离开等)
自定义回复:支持输入文字回复
回复带定位:车主可选择分享自己的位置
实时轮询:扫码者页面每5秒轮询检查车主回复状态
展示功能(车主回复后显示)
BLOG按钮:
文字通过
BLOG_LABEL变量配置链接通过
BLOG_URL变量配置可通过
SHOW_BLOG=always始终显示
图片展示:
图片链接通过
IMAGE_URL变量配置可通过
SHOW_IMAGE=always始终显示支持自定义样式(宽高、圆角、阴影等)
加载失败自动隐藏
其他功能
拨打车主电话:配置
PHONE_NUMBER后显示拨号按钮二维码打印页:访问
/qr?u=用户名生成打印版二维码防误触空按钮:键盘左下角空位不可点击
环境变量配置汇总
基础配置
通知渠道配置
以上变量均支持按用户配置,如 PUSHPLUS_TOKEN_DEFAULT
BLOG按钮配置
图片展示配置
BLOG按钮和图片的显示逻辑
页面路由汇总
用户页面布局顺序
扫码者页面(成功通知后):
📧 通知已送达卡片
👨✈️ 车主回复卡片(含地图链接)
🔄 刷新状态按钮
📞 拨打车主电话按钮(可选)
🌐 访问车主BLOG按钮(可选,默认车主回复后显示)
🖼️ 图片展示区域(可选,默认车主回复后显示)
最终代码,仅在原作者大佬代码内做了美化修改,功能基本一致
/**
* MoveCar 多用户智能挪车系统 - v3.8
* 新增:BLOG按钮下方预留图片展示区域,链接通过变量配置
* 修复:验证车牌虚拟键盘支持桌面端鼠标输入字母
*/
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
const CONFIG = {
KV_TTL: 3600,
SESSION_TTL: 1800,
RATE_LIMIT_TTL: 60
}
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
async function handleRequest(request) {
const country = request.cf?.country;
if (country && country !== 'CN' && country !== 'XX') {
return new Response('Access Denied', { status: 208 });
}
const url = new URL(request.url)
const path = url.pathname
const userParam = url.searchParams.get('u') || 'default';
const userKey = userParam.toLowerCase();
if (path === '/qr') return renderQRPage(url.origin, userKey);
if (path === '/api/notify' && request.method === 'POST') return handleNotify(request, url, userKey);
if (path === '/api/get-location') return handleGetLocation(userKey);
if (path === '/api/owner-confirm' && request.method === 'POST') return handleOwnerConfirmAction(request, userKey);
if (path === '/api/check-status') {
const s = url.searchParams.get('s');
return handleCheckStatus(userKey, s);
}
if (path === '/owner-confirm') return renderOwnerPage(userKey);
return renderMainPage(url.origin, userKey);
}
function getUserConfig(userKey, envPrefix) {
const specificKey = envPrefix + "_" + userKey.toUpperCase();
if (typeof globalThis[specificKey] !== 'undefined') return globalThis[specificKey];
if (typeof globalThis[envPrefix] !== 'undefined') return globalThis[envPrefix];
return null;
}
function wgs84ToGcj02(lat, lng) {
const a = 6378245.0; const ee = 0.00669342162296594323;
if (lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271) return { lat, lng };
let dLat = transformLat(lng - 105.0, lat - 35.0);
let dLng = transformLng(lng - 105.0, lat - 35.0);
const radLat = lat / 180.0 * Math.PI;
let magic = Math.sin(radLat); magic = 1 - ee * magic * magic;
const sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * Math.PI);
dLng = (dLng * 180.0) / (a / sqrtMagic * Math.cos(radLat) * Math.PI);
return { lat: lat + dLat, lng: lng + dLng };
}
function transformLat(x, y) {
let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(y * Math.PI) + 40.0 * Math.sin(y / 3.0 * Math.PI)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(y / 12.0 * Math.PI) + 320 * Math.sin(y * Math.PI / 30.0)) * 2.0 / 3.0;
return ret;
}
function transformLng(x, y) {
let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(x * Math.PI) + 40.0 * Math.sin(x / 3.0 * Math.PI)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(x / 12.0 * Math.PI) + 300.0 * Math.sin(x / 30.0 * Math.PI)) * 2.0 / 3.0;
return ret;
}
function generateMapUrls(lat, lng) {
const gcj = wgs84ToGcj02(lat, lng);
return {
amapUrl: "https://uri.amap.com/marker?position=" + gcj.lng + "," + gcj.lat + "&name=扫码者位置",
appleUrl: "https://maps.apple.com/?ll=" + gcj.lat + "," + gcj.lng + "&q=扫码者位置"
};
}
async function handleNotify(request, url, userKey) {
try {
if (typeof MOVE_CAR_STATUS === 'undefined') throw new Error('KV 未绑定');
const lockKey = "lock_" + userKey;
const isLocked = await MOVE_CAR_STATUS.get(lockKey);
if (isLocked) throw new Error('发送频率过快,请一分钟后再试');
const body = await request.json();
const sessionId = body.sessionId;
const clientTimestamp = body.clientTimestamp || Date.now();
const ppToken = getUserConfig(userKey, 'PUSHPLUS_TOKEN');
const barkUrl = getUserConfig(userKey, 'BARK_URL');
const email = getUserConfig(userKey, 'EMAIL');
const resendApiKey = getUserConfig("", 'RESEND_API_KEY');
const resendFrom = getUserConfig("", 'RESEND_FROM') || 'noreply_huang@xian5.de5.net';
const wxPusherAppToken = getUserConfig(userKey, 'WXPUSHER_APP_TOKEN');
const wxPusherUid = getUserConfig(userKey, 'WXPUSHER_UID');
const FlareMsgToken = getUserConfig(userKey, 'FlareMsgToken') || getUserConfig("", 'FlareMsgToken');
const FlareMsgTempID = getUserConfig("", 'FlareMsgTempID') || '';
const carTitle = getUserConfig(userKey, 'CAR_TITLE') || '车主';
const baseDomain = (typeof globalThis.EXTERNAL_URL !== 'undefined' && globalThis.EXTERNAL_URL) ? globalThis.EXTERNAL_URL.replace(/\/$/, "") : url.origin;
const confirmUrl = baseDomain + "/owner-confirm?u=" + userKey;
let notifyText = "🚗 挪车请求【" + carTitle + "】\n💬 留言: " + (body.message || '车旁有人等待');
const statusData = { status: 'waiting', sessionId: sessionId, sentAt: clientTimestamp };
let maps = null;
if (body.location && body.location.lat) {
maps = generateMapUrls(body.location.lat, body.location.lng);
await MOVE_CAR_STATUS.put("loc_" + userKey, JSON.stringify({
...body.location,
...maps,
message: body.message || '车旁有人等待'
}), { expirationTtl: CONFIG.KV_TTL });
} else {
await MOVE_CAR_STATUS.put("loc_" + userKey, JSON.stringify({
message: body.message || '车旁有人等待'
}), { expirationTtl: CONFIG.KV_TTL });
}
await MOVE_CAR_STATUS.put("status_" + userKey, JSON.stringify(statusData), { expirationTtl: CONFIG.SESSION_TTL });
await MOVE_CAR_STATUS.put(lockKey, '1', { expirationTtl: CONFIG.RATE_LIMIT_TTL });
const tasks = [];
if (ppToken) {
tasks.push(
fetch('http://www.pushplus.plus/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: ppToken,
title: "🚗 挪车请求:" + carTitle,
content: notifyText.replace(/\\n/g, '<br>') + '<br><br><a href="' + confirmUrl + '" style="font-size:18px;color:#0093E9">【点击处理】</a>',
template: 'html'
})
}).catch(e => console.error('PushPlus error:', e))
);
}
if (wxPusherAppToken && wxPusherUid) {
let wxPusherContent = "🚗 <b>挪车请求【" + carTitle + "】</b><br><br>";
wxPusherContent += "💬 留言:" + (body.message || '车旁有人等待') + "<br>";
wxPusherContent += "⏰ 时间:" + new Date(clientTimestamp).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) + "<br>";
if (maps) {
wxPusherContent += "<br>📍 点击查看对方位置:<br>";
wxPusherContent += '<a href="' + maps.amapUrl + '">高德地图</a> | <a href="' + maps.appleUrl + '">苹果地图</a><br>';
}
wxPusherContent += '<br><a href="' + confirmUrl + '">📱 点击处理挪车</a>';
tasks.push(
fetch('https://wxpusher.zjiecode.com/api/send/message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
appToken: wxPusherAppToken,
content: wxPusherContent,
contentType: 3,
uids: [wxPusherUid],
summary: "🚗 挪车请求:" + carTitle,
verifyPay: false
})
}).catch(e => console.error('WxPusher error:', e))
);
}
if (FlareMsgToken) tasks.push(fetch('https://flaremsg.xian5.de5.net/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: FlareMsgToken,
temp: FlareMsgTempID,
from: carTitle,
desc: (body.message || '车旁有人等待'),
remark: new Date(statusData.sentAt).toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).replace(/\//g, '-'),
url: confirmUrl
})
}));
if (barkUrl) tasks.push(fetch(barkUrl + "/" + encodeURIComponent('挪车请求') + "/" + encodeURIComponent(notifyText) + "?url=" + encodeURIComponent(confirmUrl)));
if (email && resendApiKey) {
const escapedMessage = escapeHtml(body.message || '车旁有人等待');
let locationHtml = '';
if (maps) {
locationHtml = '<p><strong>扫码者位置:</strong><br><a href="' + maps.amapUrl + '">高德地图</a> | <a href="' + maps.appleUrl + '">苹果地图</a></p>';
}
const mailHtml = '<h2>🚗 挪车请求【' + carTitle + '】</h2><p><strong>留言:</strong>' + escapedMessage + '</p>' + locationHtml + '<p><a href="' + confirmUrl + '" style="display:inline-block;padding:10px 20px;background:#0093E9;color:#fff;text-decoration:none;border-radius:5px;">点击处理挪车</a></p>';
tasks.push(
fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + resendApiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
from: resendFrom,
to: [email],
subject: '挪车请求:' + carTitle,
html: mailHtml
})
}).catch(e => console.error('Resend error:', e))
);
}
await Promise.all(tasks);
return new Response(JSON.stringify({ success: true }));
} catch (e) {
return new Response(JSON.stringify({ success: false, error: e.message }), { status: 500 });
}
}
async function handleCheckStatus(userKey, clientSessionId) {
const data = await MOVE_CAR_STATUS.get("status_" + userKey);
if (!data) return new Response(JSON.stringify({ status: 'none' }));
const statusObj = JSON.parse(data);
if (statusObj.sessionId !== clientSessionId) {
return new Response(JSON.stringify({ status: 'none' }));
}
const ownerLoc = await MOVE_CAR_STATUS.get("owner_loc_" + userKey);
return new Response(JSON.stringify({
status: statusObj.status,
ownerLocation: ownerLoc ? JSON.parse(ownerLoc) : null,
sentAt: statusObj.sentAt || null
}));
}
async function handleGetLocation(userKey) {
const data = await MOVE_CAR_STATUS.get("loc_" + userKey);
return new Response(data || '{}');
}
async function handleOwnerConfirmAction(request, userKey) {
const body = await request.json();
const data = await MOVE_CAR_STATUS.get("status_" + userKey);
if (data) {
const statusObj = JSON.parse(data);
statusObj.status = 'confirmed';
let ownerInfo = {};
if (body.location) {
const urls = generateMapUrls(body.location.lat, body.location.lng);
ownerInfo = { ...body.location, ...urls };
}
if (body.replyMessage) {
ownerInfo.replyMessage = body.replyMessage;
}
ownerInfo.replyAt = Date.now();
await MOVE_CAR_STATUS.put("owner_loc_" + userKey, JSON.stringify(ownerInfo), { expirationTtl: 600 });
await MOVE_CAR_STATUS.put("status_" + userKey, JSON.stringify(statusObj), { expirationTtl: 600 });
}
return new Response(JSON.stringify({ success: true }));
}
function renderQRPage(origin, userKey) {
const carTitle = getUserConfig(userKey, 'CAR_TITLE') || '车主';
let baseDomain = (typeof globalThis.EXTERNAL_URL !== 'undefined' && globalThis.EXTERNAL_URL) ? globalThis.EXTERNAL_URL.replace(/\/$/, "") : origin;
const targetUrl = baseDomain + "/?u=" + userKey;
return new Response('<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>制作挪车码</title><style>body{font-family:sans-serif;background:#f8fafc;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}.qr-card{background:white;padding:40px 20px;border-radius:30px;box-shadow:0 10px 40px rgba(0,0,0,0.05);text-align:center;width:90%;max-width:380px}.qr-img{width:250px;height:250px;margin:25px auto;border:1px solid #f1f5f9;padding:8px;border-radius:12px}.btn{display:block;background:#0093E9;color:white;text-decoration:none;padding:16px;border-radius:16px;font-weight:bold;margin-top:20px}.url-info{font-size:11px;color:#cbd5e1;margin-top:15px;word-break:break-all}</style></head><body><div class="qr-card"><h2 style="color:#1e293b">' + carTitle + ' 的专属挪车码</h2><p style="color:#64748b;font-size:14px;margin-top:8px">扫码通知,保护隐私</p><img class="qr-img" src="https://api.qrserver.com/v1/create-qr-code/?size=450x450&data=' + encodeURIComponent(targetUrl) + '"><a href="javascript:window.print()" class="btn">🖨️ 立即打印挪车牌</a><div class="url-info">' + targetUrl + '</div></div></body></html>', { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });
}
/** 界面渲染:扫码者页(BLOG按钮 + 图片展示) **/
function renderMainPage(origin, userKey) {
const phone = getUserConfig(userKey, 'PHONE_NUMBER') || '';
const carTitle = getUserConfig(userKey, 'CAR_TITLE') || '车主A888';
const phoneHtml = phone ? '<a href="tel:' + phone + '" class="btn-phone">📞 拨打车主电话</a>' : '';
const lastFour = carTitle.length >= 4 ? carTitle.slice(-4) : carTitle;
const needVerify = (carTitle !== '车主' && carTitle.length >= 4);
// BLOG按钮配置
const blogLabel = getUserConfig(userKey, 'BLOG_LABEL') || '🌐 访问车主主页';
const blogUrl = getUserConfig(userKey, 'BLOG_URL') || 'https://www.15176688.xyz';
const showBlog = getUserConfig(userKey, 'SHOW_BLOG'); // "always" 始终显示
// 图片配置
const showImage = getUserConfig(userKey, 'SHOW_IMAGE'); // 任意值即启用图片
const imageUrl = getUserConfig(userKey, 'IMAGE_URL') || '';
const imageAlt = getUserConfig(userKey, 'IMAGE_ALT') || '车主展示图片';
const imageWidth = getUserConfig(userKey, 'IMAGE_WIDTH') || '100%';
const imageHeight = getUserConfig(userKey, 'IMAGE_HEIGHT') || 'auto';
const imageStyle = getUserConfig(userKey, 'IMAGE_STYLE') || 'max-width:100%;border-radius:16px;margin-top:10px;box-shadow:0 5px 15px rgba(0,0,0,0.1)';
// 图片HTML - 只有配置了IMAGE_URL且SHOW_IMAGE有值时才生成
let imageHtml = '';
if (showImage && imageUrl) {
imageHtml = '<div id="displayImage" class="image-container hidden"><img id="displayImageImg" src="' + imageUrl + '" alt="' + imageAlt + '" style="' + imageStyle + ';width:' + imageWidth + ';height:' + imageHeight + '" onload="this.parentElement.classList.remove(\'hidden\')" onerror="this.parentElement.classList.add(\'hidden\')"></div>';
}
return new Response('<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, viewport-fit=cover, user-scalable=no"><title>挪车通知</title><style>*{box-sizing:border-box;-webkit-tap-highlight-color:transparent;margin:0;padding:0}body{font-family:-apple-system,sans-serif;background:linear-gradient(160deg,#0093E9 0%,#80D0C7 100%);min-height:100vh;padding:20px;display:flex;justify-content:center}.container{width:100%;max-width:500px;display:flex;flex-direction:column;gap:15px}.card{background:white;border-radius:24px;padding:20px;box-shadow:0 10px 30px rgba(0,0,0,0.1)}.header{text-align:center}.icon-wrap{width:64px;height:64px;background:#0093E9;border-radius:20px;display:flex;align-items:center;justify-content:center;margin:0 auto 10px;font-size:32px;color:white}textarea{width:100%;min-height:90px;border:1px solid #eee;border-radius:14px;padding:15px;font-size:16px;outline:none;margin-top:10px;background:#fcfcfc;resize:none}.tag{display:inline-block;background:#f1f5f9;padding:10px 16px;border-radius:20px;font-size:14px;margin:5px 3px;cursor:pointer;color:#475569}.btn-main{background:#0093E9;color:white;border:none;padding:18px;border-radius:18px;font-size:18px;font-weight:bold;cursor:pointer;width:100%}.btn-main:disabled{background:#94a3b8;cursor:not-allowed}.btn-phone{background:#ef4444;color:white;border:none;padding:15px;border-radius:15px;text-decoration:none;text-align:center;font-weight:bold;display:block;margin-top:10px}.btn-blog{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white;border:none;padding:16px;border-radius:18px;font-size:17px;font-weight:bold;cursor:pointer;width:100%;text-decoration:none;text-align:center;display:none;margin-top:10px;box-shadow:0 5px 15px rgba(102,126,234,0.4);transition:transform 0.15s,box-shadow 0.15s}.btn-blog:active{transform:scale(0.96);box-shadow:0 3px 10px rgba(102,126,234,0.3)}.image-container{text-align:center;margin-top:10px}.image-container img{display:block;max-width:100%;height:auto;border-radius:16px;box-shadow:0 5px 15px rgba(0,0,0,0.1)}.btn-retry{background:#f59e0b;color:white;border:none;padding:8px 16px;border-radius:20px;font-size:13px;cursor:pointer;margin-left:10px}.hidden{display:none!important}.map-links{display:flex;gap:10px;margin-top:15px}.map-btn{flex:1;padding:14px;border-radius:14px;text-align:center;text-decoration:none;color:white;font-weight:bold}.amap{background:#1890ff}.apple{background:#000}.code-inputs{display:flex;justify-content:center;gap:10px;margin:15px 0}.code-inputs input{width:55px;height:65px;text-align:center;font-size:28px;font-weight:bold;border:2px solid #e2e8f0;border-radius:12px;outline:none;transition:all 0.15s;background:#f8fafc;text-transform:uppercase;pointer-events:none;caret-color:transparent}.code-inputs input.active{border-color:#0093E9;box-shadow:0 0 0 3px rgba(0,147,233,0.3);background:#eff6ff}.code-inputs input.filled{border-color:#10b981;background:#f0fdf4}.error-msg{color:#ef4444;font-size:14px;min-height:20px;text-align:center;margin-top:5px}.verify-btn{background:#10b981;color:white;border:none;padding:16px;border-radius:18px;font-size:18px;font-weight:bold;cursor:pointer;width:100%;margin-top:15px;transition:background 0.2s}.verify-btn:active{background:#059669}.loc-row{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap}.loc-text{font-size:13px;color:#94a3b8}.countdown-msg{font-size:14px;color:#f97316;text-align:center;margin:8px 0;min-height:24px;background-color:#fff3e0;padding:6px 12px;border-radius:20px;font-weight:500;display:none}.numpad{background:#f1f5f9;border-radius:20px;padding:12px;margin-top:15px;user-select:none;-webkit-user-select:none;touch-action:manipulation}.numpad-row{display:flex;gap:8px;margin-bottom:8px}.numpad-row:last-child{margin-bottom:0}.numpad-key{flex:1;height:58px;border:none;border-radius:12px;font-size:24px;font-weight:700;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#1e293b;background:white;box-shadow:0 2px 6px rgba(0,0,0,0.06);-webkit-tap-highlight-color:transparent;touch-action:manipulation;transition:transform 0.05s,background 0.05s;will-change:transform}.numpad-key:active{transform:scale(0.93);background:#e2e8f0;box-shadow:0 1px 3px rgba(0,0,0,0.08)}.numpad-key-num{background:#0093E9;color:white;font-size:26px}.numpad-key-num:active{background:#0077cc}.numpad-key-delete{background:#ef4444;color:white;font-size:18px}.numpad-key-delete:active{background:#dc2626}.numpad-key-empty{background:transparent;box-shadow:none;cursor:default;pointer-events:none}.key-sub{font-size:10px;color:#94a3b8;display:block;line-height:1;margin-top:-2px}.numpad-key-num .key-sub{color:rgba(255,255,255,0.7)}.letter-popup{position:fixed;background:rgba(0,0,0,0.92);border-radius:20px;padding:8px 10px;display:flex;gap:6px;z-index:9999;box-shadow:0 10px 30px rgba(0,0,0,0.5);flex-wrap:wrap;justify-content:center;pointer-events:auto}.popup-letter{color:white;font-size:24px;font-weight:700;padding:10px 14px;border-radius:14px;background:rgba(255,255,255,0.15);min-width:44px;text-align:center;transition:background 0.08s,transform 0.08s}.popup-letter.highlight{background:#0093E9;transform:scale(1.12)}@media(max-width:400px){.numpad-key{height:50px;font-size:22px;border-radius:10px}.numpad-key-num{font-size:24px}.numpad{padding:10px}.numpad-row{gap:6px;margin-bottom:6px}.code-inputs input{width:48px;height:58px;font-size:26px}}</style></head><body><div class="container" id="verifyView"' + (needVerify ? '' : ' style="display:none"') + '><div class="card"><div class="icon-wrap">🔐</div><h2 style="color:#1e293b">验证车牌</h2><p style="color:#64748b;margin-top:5px">请输入车牌号后四位,长按键盘可滑动切换英文</p><div class="code-inputs" id="codeInputs"><input type="text" maxlength="1" class="code-digit" id="digit0" readonly><input type="text" maxlength="1" class="code-digit" id="digit1" readonly><input type="text" maxlength="1" class="code-digit" id="digit2" readonly><input type="text" maxlength="1" class="code-digit" id="digit3" readonly></div><div class="error-msg" id="verifyError"></div><div class="numpad" id="numpad"><div class="numpad-row"><button class="numpad-key numpad-key-num" data-key="1">1</button><button class="numpad-key numpad-key-num" data-key="2">2<span class="key-sub">ABC</span></button><button class="numpad-key numpad-key-num" data-key="3">3<span class="key-sub">DEF</span></button></div><div class="numpad-row"><button class="numpad-key numpad-key-num" data-key="4">4<span class="key-sub">GHI</span></button><button class="numpad-key numpad-key-num" data-key="5">5<span class="key-sub">JKL</span></button><button class="numpad-key numpad-key-num" data-key="6">6<span class="key-sub">MNO</span></button></div><div class="numpad-row"><button class="numpad-key numpad-key-num" data-key="7">7<span class="key-sub">PQRS</span></button><button class="numpad-key numpad-key-num" data-key="8">8<span class="key-sub">TUV</span></button><button class="numpad-key numpad-key-num" data-key="9">9<span class="key-sub">WXYZ</span></button></div><div class="numpad-row"><button class="numpad-key numpad-key-empty"></button><button class="numpad-key numpad-key-num" data-key="0">0</button><button class="numpad-key numpad-key-delete" data-key="delete">⌫</button></div></div><button class="verify-btn" id="verifyBtn">验证</button></div></div><div class="container ' + (needVerify ? 'hidden' : '') + '" id="mainView"><div class="card header"><div class="icon-wrap">🚗</div><h2 style="color:#1e293b">呼叫 ' + carTitle + '</h2><p style="color:#64748b;font-size:14px;margin-top:5px">提示:车主将收到即时提醒</p></div><div class="card"><textarea id="msgInput" placeholder="请输入留言...\n(获取定位后通知,车主回复更快哦!)"></textarea><div style="margin-top:5px"><div class="tag" onclick="setTag(\'麻烦挪下车,谢谢\')">🚧 挡路了</div><div class="tag" onclick="setTag(\'有急事外出,速来\')">🏃 急事</div><div class="tag" onclick="setTag(\'有叔叔贴条,速度来挪车!\')">⏱️ 温馨提醒</div><div class="tag" onclick="setTag(\'请挪车,我在你车旁,请查看位置,尽快前来!\')">🏃 发送我的位置</div><div class="tag" onclick="setTag(\'这是我的车位,我要用了,谢谢\')">🚧 占我车位</div></div></div><div class="card" id="locStatusCard"><div class="loc-row"><span class="loc-text" id="locStatus">定位请求中...</span><button id="retryLocationBtn" class="btn-retry" style="display:none" onclick="retryLocation()">重新获取</button></div></div><div class="countdown-msg" id="countdownMsg"></div><button id="notifyBtn" class="btn-main" onclick="sendNotify()">🔔 一键通知车主</button></div><div class="container hidden" id="successView"><div class="card" style="text-align:center"><div style="font-size:64px;margin-bottom:15px">📧</div><h2 style="color:#1e293b">通知已送达</h2><p style="color:#64748b">车主已收到挪车请求,请在车旁稍候</p><p id="sentTimeDisplay" style="font-size:12px;color:#888;margin-top:5px"></p></div><div id="ownerFeedback" class="card hidden" style="text-align:center;border:2.5px solid #10b981"><div style="font-size:40px">👨✈️</div><h3 id="ownerReplyMsg" style="color:#059669">车主回复:马上到</h3><div class="map-links"><a id="ownerAmap" href="#" class="map-btn amap">高德地图</a><a id="ownerApple" href="#" class="map-btn apple">苹果地图</a></div></div><div><button class="btn-main" style="background:#f59e0b;margin-top:10px" onclick="location.reload()">🔄 刷新状态</button>' + phoneHtml + '<a id="blogBtn" class="btn-blog" href="' + blogUrl + '" target="_blank" rel="noopener">' + blogLabel + '</a>' + imageHtml + '</div></div><div id="letterPopup" class="letter-popup hidden"></div><script>!function(){var userLoc=null;var userKey="' + userKey + '";var correctLastFour="' + lastFour + '";var needVerify=' + (needVerify ? 'true' : 'false') + ';var showBlogAlways=' + (showBlog === 'always' ? 'true' : 'false') + ';var showImageAlways=' + (showImage ? 'true' : 'false') + ';var locationReady=false;var countdown=30;var countdownInterval=null;var sessionId=localStorage.getItem("movecar_session_"+userKey);if(!sessionId){sessionId=Date.now()+"_"+Math.random().toString(36).substr(2,9);localStorage.setItem("movecar_session_"+userKey,sessionId)}var verifiedFlag="verified_"+userKey;var isVerified=sessionStorage.getItem(verifiedFlag)==="true";if(!needVerify){initMain()}else{if(isVerified){document.getElementById("verifyView").style.display="none";document.getElementById("mainView").classList.remove("hidden");initMain()}else{initVerify()}}function initVerify(){var inputs=[document.getElementById("digit0"),document.getElementById("digit1"),document.getElementById("digit2"),document.getElementById("digit3")];var verifyBtn=document.getElementById("verifyBtn");var errorDiv=document.getElementById("verifyError");var popup=document.getElementById("letterPopup");var keyLetterMap={"2":["A","B","C"],"3":["D","E","F"],"4":["G","H","I"],"5":["J","K","L"],"6":["M","N","O"],"7":["P","Q","R","S"],"8":["T","U","V"],"9":["W","X","Y","Z"]};var longPressTimer=null;var longPressKey=null;var popupVisible=false;var popupLetters=[];var highlightedIndex=-1;function getFilledCount(){var n=0;for(var i=0;i<4;i++){if(inputs[i].value!="")n++}return n}function inputChar(ch){if(getFilledCount()>=4)return;errorDiv.textContent="";var c=ch.toUpperCase();for(var i=0;i<4;i++){if(inputs[i].value===""){inputs[i].value=c;inputs[i].classList.add("filled");break}}updateCursor();if(getFilledCount()>=4){for(var j=0;j<4;j++)inputs[j].classList.remove("active")}}function deleteChar(){errorDiv.textContent="";for(var i=3;i>=0;i--){if(inputs[i].value!==""){inputs[i].value="";inputs[i].classList.remove("filled");break}}updateCursor()}function updateCursor(){for(var i=0;i<4;i++){inputs[i].classList.remove("active")}for(var k=0;k<4;k++){if(inputs[k].value===""){inputs[k].classList.add("active");return}}for(var j=0;j<4;j++)inputs[j].classList.remove("active")}function showPopup(keyChar,x,y){var letters=keyLetterMap[keyChar];if(!letters)return;popupLetters=letters;highlightedIndex=0;popup.innerHTML="";for(var i=0;i<letters.length;i++){var s=document.createElement("span");s.className="popup-letter";s.textContent=letters[i];s.setAttribute("data-idx",i);popup.appendChild(s)}var left=x-60,top=y-80;if(left<10)left=10;if(left+180>window.innerWidth)left=window.innerWidth-190;if(top<20)top=20;popup.style.left=left+"px";popup.style.top=top+"px";popup.classList.remove("hidden");popupVisible=true;updateHighlight()}function updateHighlight(){var items=popup.querySelectorAll(".popup-letter");for(var i=0;i<items.length;i++){items[i].classList.toggle("highlight",i===highlightedIndex)}}function hidePopup(){popup.classList.add("hidden");popupVisible=false;popupLetters=[];highlightedIndex=-1;if(longPressTimer){clearTimeout(longPressTimer);longPressTimer=null}longPressKey=null}function selectLetter(l){if(l&&getFilledCount()<4)inputChar(l);hidePopup()}function handlePopupMove(cx,cy){if(!popupVisible)return;var items=popup.querySelectorAll(".popup-letter");var closest=0,minDist=1e9;for(var i=0;i<items.length;i++){var r=items[i].getBoundingClientRect();var d=Math.hypot(cx-(r.left+r.width/2),cy-(r.top+r.height/2));if(d<minDist){minDist=d;closest=i}}if(closest!==highlightedIndex){highlightedIndex=closest;updateHighlight()}}var numpad=document.getElementById("numpad");var keys=numpad.querySelectorAll(".numpad-key:not(.numpad-key-empty)");function handleKeyStart(kv,clientX,clientY){if(kv==="delete"){deleteChar();return}if(kv==="0"||kv==="1"){if(getFilledCount()<4)inputChar(kv);return}longPressKey=kv;longPressTimer=setTimeout(function(){if(longPressKey===kv&&getFilledCount()<4){showPopup(kv,clientX||100,clientY||250)}longPressTimer=null},("ontouchstart"in window?350:500))}function handleKeyEnd(kv){if(popupVisible&&longPressKey===kv){if(highlightedIndex>=0&&highlightedIndex<popupLetters.length){selectLetter(popupLetters[highlightedIndex])}else{hidePopup()}}else{if(longPressTimer){clearTimeout(longPressTimer);longPressTimer=null;if(getFilledCount()<4)inputChar(kv)}}longPressKey=null}for(var k=0;k<keys.length;k++){(function(btn){var kv=btn.getAttribute("data-key");btn.addEventListener("mousedown",function(e){e.preventDefault();handleKeyStart(kv,e.clientX,e.clientY)});btn.addEventListener("touchstart",function(e){e.preventDefault();var t=e.touches[0];handleKeyStart(kv,t?t.clientX:null,t?t.clientY:null)});btn.addEventListener("touchmove",function(e){e.preventDefault();if(popupVisible&&longPressKey===kv){var t=e.touches[0];if(t)handlePopupMove(t.clientX,t.clientY)}else{if(longPressTimer){clearTimeout(longPressTimer);longPressTimer=null}}});btn.addEventListener("touchend",function(e){e.preventDefault();handleKeyEnd(kv)});btn.addEventListener("touchcancel",function(){hidePopup()})})(keys[k])}document.addEventListener("mousemove",function(e){if(popupVisible&&longPressKey){handlePopupMove(e.clientX,e.clientY)}});document.addEventListener("mouseup",function(e){if(popupVisible&&longPressKey){if(highlightedIndex>=0&&highlightedIndex<popupLetters.length){selectLetter(popupLetters[highlightedIndex])}else{hidePopup()}}else{if(longPressTimer){clearTimeout(longPressTimer);longPressTimer=null;if(longPressKey&&getFilledCount()<4)inputChar(longPressKey)}}longPressKey=null});document.addEventListener("click",function(e){if(popupVisible&&!e.target.closest("#numpad")&&!e.target.closest("#letterPopup")){hidePopup()}});verifyBtn.addEventListener("click",function(){var code="";for(var i=0;i<4;i++)code+=inputs[i].value;code=code.toUpperCase();if(code.length!==4){errorDiv.textContent="请输入完整的四位验证码";return}if(code===correctLastFour){sessionStorage.setItem(verifiedFlag,"true");document.getElementById("verifyView").style.display="none";document.getElementById("mainView").classList.remove("hidden");initMain()}else{errorDiv.textContent="验证码错误,请重新输入";for(var j=0;j<4;j++){inputs[j].value="";inputs[j].classList.remove("filled","active")}updateCursor()}});updateCursor()}function initMain(){if(window._mainInitDone)return;window._mainInitDone=true;initMainAsync().catch(function(e){console.error(e)})}async function initMainAsync(){var hasActive=await checkActiveSession();if(hasActive){console.log("active session");return}resetLocation()}function resetLocation(){if(countdownInterval){clearInterval(countdownInterval);countdownInterval=null}countdown=30;locationReady=false;var md=document.getElementById("countdownMsg");if(md)md.style.display="none";var rb=document.getElementById("retryLocationBtn");if(rb)rb.style.display="none";if(navigator.geolocation){navigator.geolocation.getCurrentPosition(function(p){userLoc={lat:p.coords.latitude,lng:p.coords.longitude};locationReady=true;document.getElementById("locStatus").innerText="已获取位置";document.getElementById("locStatus").style.color="#10b981";if(countdownInterval){clearInterval(countdownInterval);countdownInterval=null}var md2=document.getElementById("countdownMsg");if(md2)md2.style.display="none";var rb2=document.getElementById("retryLocationBtn");if(rb2)rb2.style.display="none";document.getElementById("notifyBtn").disabled=false},function(err){document.getElementById("locStatus").innerText="无法获取精确位置";document.getElementById("locStatus").style.color="#ef4444";var rb3=document.getElementById("retryLocationBtn");if(rb3)rb3.style.display="inline-block";if(!locationReady&&!countdownInterval)startCountdown()},{timeout:10000})}else{document.getElementById("locStatus").innerText="浏览器不支持定位";document.getElementById("locStatus").style.color="#ef4444";var rb4=document.getElementById("retryLocationBtn");if(rb4)rb4.style.display="inline-block";if(!locationReady&&!countdownInterval)startCountdown()}}function startCountdown(){countdown=30;var btn=document.getElementById("notifyBtn");var md=document.getElementById("countdownMsg");if(!md)return;md.style.display="block";btn.disabled=true;md.innerText="定位获取失败,等待 "+countdown+" 秒后可发送";countdownInterval=setInterval(function(){countdown--;if(countdown<=0){clearInterval(countdownInterval);countdownInterval=null;btn.disabled=false;md.innerText="现在可以发送通知(位置未获取)"}else{md.innerText="定位获取失败,等待 "+countdown+" 秒后可发送"}},1000)}window.retryLocation=function(){if(!navigator.geolocation){alert("浏览器不支持定位功能");return}var btn=document.getElementById("retryLocationBtn");btn.disabled=true;btn.innerText="获取中...";navigator.geolocation.getCurrentPosition(function(p){userLoc={lat:p.coords.latitude,lng:p.coords.longitude};locationReady=true;document.getElementById("locStatus").innerText="已获取位置";document.getElementById("locStatus").style.color="#10b981";if(countdownInterval){clearInterval(countdownInterval);countdownInterval=null}var md2=document.getElementById("countdownMsg");if(md2)md2.style.display="none";btn.style.display="none";document.getElementById("notifyBtn").disabled=false;btn.disabled=false;btn.innerText="重新获取"},function(err){alert("再次获取位置失败,请允许定位权限");btn.disabled=false;btn.innerText="重新获取";if(!locationReady&&!countdownInterval)startCountdown()},{timeout:10000})};async function checkActiveSession(){try{var res=await fetch("/api/check-status?u="+userKey+"&s="+sessionId);var data=await res.json();if(data.status&&data.status!=="none"){showSuccess(data);pollStatus();return true}}catch(e){}return false}window.setTag=function(t){document.getElementById("msgInput").value=t};window.sendNotify=async function(){var btn=document.getElementById("notifyBtn");if(!locationReady&&countdown>0){alert("尚未获取到您的位置,请给定位权限,或等待 "+countdown+" 秒后再试");return}btn.disabled=true;btn.innerText="正在联络车主...";try{var res=await fetch("/api/notify?u="+userKey,{method:"POST",body:JSON.stringify({message:document.getElementById("msgInput").value,location:userLoc,sessionId:sessionId,clientTimestamp:Date.now()})});var data=await res.json();if(data.success){showSuccess({status:"waiting",sentAt:Date.now()});pollStatus()}else{alert(data.error);btn.disabled=false;btn.innerText="一键通知车主"}}catch(e){alert("服务暂时不可用");btn.disabled=false}};function showSuccess(data){document.getElementById("mainView").classList.add("hidden");document.getElementById("successView").classList.remove("hidden");updateUI(data)}function updateUI(data){var td=document.getElementById("sentTimeDisplay");if(data.sentAt){var s=Math.floor((Date.now()-data.sentAt)/1000);td.innerText="(发送于 "+s+" 秒前)"}else{td.innerText=""}var blogBtn=document.getElementById("blogBtn");var imgContainer=document.getElementById("displayImage");if(showBlogAlways){if(blogBtn)blogBtn.style.display="block"}else if(data.status==="confirmed"){if(blogBtn)blogBtn.style.display="block"}else{if(blogBtn)blogBtn.style.display="none"}if(showImageAlways){if(imgContainer)imgContainer.classList.remove("hidden")}else if(data.status==="confirmed"){if(imgContainer)imgContainer.classList.remove("hidden")}else{if(imgContainer)imgContainer.classList.add("hidden")}if(data.status==="confirmed"){document.getElementById("ownerFeedback").classList.remove("hidden");var rm=data.ownerLocation?.replyMessage||"车主已确认,马上到";var rs=data.ownerLocation?.replyAt?Math.floor((Date.now()-data.ownerLocation.replyAt)/1000):0;document.getElementById("ownerReplyMsg").innerText="车主回复 ("+rs+" 秒前):"+rm;if(data.ownerLocation){document.getElementById("ownerAmap").href=data.ownerLocation.amapUrl||"#";document.getElementById("ownerApple").href=data.ownerLocation.appleUrl||"#"}}}function pollStatus(){if(window.pollInterval)clearInterval(window.pollInterval);window.pollInterval=setInterval(async function(){try{var res=await fetch("/api/check-status?u="+userKey+"&s="+sessionId);updateUI(await res.json())}catch(e){}},5000)}}();</script></body></html>', { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });
}
/** 界面渲染:车主页 **/
function renderOwnerPage(userKey) {
const carTitle = getUserConfig(userKey, 'CAR_TITLE') || '车主';
return new Response('<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>挪车处理</title><style>body{font-family:sans-serif;background:#4f46e5;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:20px}.card{background:white;padding:35px 25px;border-radius:30px;text-align:center;width:100%;max-width:400px;box-shadow:0 20px 40px rgba(0,0,0,0.2)}.btn{background:#10b981;color:white;border:none;width:100%;padding:20px;border-radius:18px;font-size:18px;font-weight:bold;cursor:pointer;margin-top:20px;box-shadow:0 5px 15px rgba(16,185,129,0.3)}.btn-secondary{background:#6b7280}.map-box{background:#f8fafc;padding:20px;border-radius:20px;margin-top:15px;border:1px solid #e2e8f0;display:none}.map-btn{display:inline-block;padding:12px 18px;background:#2563eb;color:white;text-decoration:none;border-radius:12px;margin:5px;font-size:14px}.reply-section{margin-top:20px;border-top:1px solid #e2e8f0;padding-top:15px}.fold-btn{background:#e2e8f0;color:#1e293b;border:none;padding:10px;border-radius:30px;font-size:14px;cursor:pointer;width:100%;margin-bottom:10px}.fold-content{display:none}.textarea-reply{width:100%;min-height:80px;border:1px solid #ccc;border-radius:14px;padding:12px;font-size:16px;margin-top:10px;resize:vertical}.tag{display:inline-block;background:#f1f5f9;padding:8px 12px;border-radius:20px;font-size:14px;margin:5px 3px;cursor:pointer;color:#475569}.tag:hover{background:#e2e8f0}.btn-send{background:#2563eb;color:white;border:none;padding:16px;border-radius:18px;font-size:16px;font-weight:bold;cursor:pointer;width:100%;margin-top:15px}.time-text{font-size:12px;color:#94a3b8;margin:5px 0}.msg-box{background:#f1f5f9;padding:15px;border-radius:16px;margin:10px 0;text-align:left}.msg-content{font-size:16px;color:#1e293b}</style></head><body><div class="card"><div style="font-size:50px">📣</div><h2 style="margin:15px 0;color:#1e293b">' + carTitle + '</h2><div id="visitorMsgBox" class="msg-box" style="display:none"><div class="msg-content" id="visitorMsg"></div><div class="time-text" id="visitorMsgTime"></div></div><p style="color:#64748b">有人正在车旁等您,请确认:</p><div id="mapArea" class="map-box"><p style="font-size:14px;color:#2563eb;margin-bottom:12px;font-weight:bold">对方实时位置 📍</p><a id="amapLink" href="#" class="map-btn">高德地图</a><a id="appleLink" href="#" class="map-btn" style="background:#000">苹果地图</a></div><button id="confirmBtn" class="btn" onclick="confirmMove()">🚀 我已知晓,马上过去</button><div class="reply-section"><button id="foldBtn" class="fold-btn" onclick="toggleFold()">✏️ 发送其他回复</button><div id="foldContent" class="fold-content"><textarea id="customReply" class="textarea-reply" placeholder="请输入您的回复..."></textarea><div style="margin-top:5px"><span class="tag" onclick="setReplyTag(\'🚫 定位错误,请确认在车旁?\')">🚫 定位错误</span><span class="tag" onclick="setReplyTag(\'⏳ 我暂时没法离开,等一会才能来···\')">⏳ 暂时无法离开</span><span class="tag" onclick="setReplyTag(\'🏃 请稍等,🐎上到\')">🏃 马上到</span><span class="tag" onclick="setReplyTag(\'📞 请拨打联系电话\')">📞 拨打电话</span></div><button id="sendCustomBtn" class="btn-send" onclick="sendCustomReply()">📨 发送回复</button><div class="time-text" id="myReplyTime" style="display:none;margin-top:10px"></div></div></div></div><script>const userKey="' + userKey + '";let foldOpen=false;let visitorSentAt=0;let myReplyAt=0;function formatTimeAgo(timestamp){if(!timestamp)return "";const seconds=Math.floor((Date.now()-timestamp)/1000);return seconds+" 秒前"}function updateTimeDisplays(){if(visitorSentAt){document.getElementById("visitorMsgTime").innerText="发送于 "+formatTimeAgo(visitorSentAt)}if(myReplyAt){document.getElementById("myReplyTime").innerText="您的回复发送于 "+formatTimeAgo(myReplyAt)}}window.onload=async()=>{const locRes=await fetch("/api/get-location?u="+userKey);const locData=await locRes.json();if(locData.amapUrl){document.getElementById("mapArea").style.display="block";document.getElementById("amapLink").href=locData.amapUrl;document.getElementById("appleLink").href=locData.appleUrl}const statusRes=await fetch("/api/check-status?u="+userKey);const statusData=await statusRes.json();if(statusData.sentAt){visitorSentAt=statusData.sentAt;const visitorMsg=locData.message||"车旁有人等待";document.getElementById("visitorMsg").innerText=visitorMsg;document.getElementById("visitorMsgBox").style.display="block"}updateTimeDisplays();setInterval(updateTimeDisplays,1000)};function toggleFold(){foldOpen=!foldOpen;document.getElementById("foldContent").style.display=foldOpen?"block":"none";document.getElementById("foldBtn").innerText=foldOpen?"🔽 收起":"✏️ 发送其他回复"}function setReplyTag(text){document.getElementById("customReply").value=text}async function sendConfirmWithReply(replyMessage){const btn=event?.target||document.getElementById("confirmBtn");const originalText=btn.innerText;btn.disabled=true;btn.innerText="发送中...";myReplyAt=Date.now();if(navigator.geolocation){navigator.geolocation.getCurrentPosition(async p=>{const location={lat:p.coords.latitude,lng:p.coords.longitude};await sendConfirmRequest(location,replyMessage);btn.disabled=false;btn.innerText=originalText;document.getElementById("myReplyTime").style.display="block";updateTimeDisplays()},async()=>{await sendConfirmRequest(null,replyMessage);btn.disabled=false;btn.innerText=originalText;document.getElementById("myReplyTime").style.display="block";updateTimeDisplays()})}else{await sendConfirmRequest(null,replyMessage);btn.disabled=false;btn.innerText=originalText;document.getElementById("myReplyTime").style.display="block";updateTimeDisplays()}}async function sendConfirmRequest(location,replyMessage){try{const res=await fetch("/api/owner-confirm?u="+userKey,{method:"POST",body:JSON.stringify({location,replyMessage})});const data=await res.json();if(data.success){alert("回复已发送");if(foldOpen)toggleFold()}else{alert("发送失败,请重试")}}catch(e){alert("网络错误")}}function confirmMove(){sendConfirmWithReply("🚀 马上到")}function sendCustomReply(){const reply=document.getElementById("customReply").value.trim();if(!reply){alert("请输入回复内容");return}sendConfirmWithReply(reply)}</script></body></html>', { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });
}