0327
parent
0ca5070755
commit
6fee91ed80
|
|
@ -1124,9 +1124,9 @@ public class WorkServiceImpl implements WorkService {
|
|||
}
|
||||
|
||||
private String buildLocationCacheKey(double latitude, double longitude) {
|
||||
return BigDecimal.valueOf(latitude).setScale(4, java.math.RoundingMode.HALF_UP).toPlainString()
|
||||
return BigDecimal.valueOf(latitude).setScale(5, java.math.RoundingMode.HALF_UP).toPlainString()
|
||||
+ ","
|
||||
+ BigDecimal.valueOf(longitude).setScale(4, java.math.RoundingMode.HALF_UP).toPlainString();
|
||||
+ BigDecimal.valueOf(longitude).setScale(5, java.math.RoundingMode.HALF_UP).toPlainString();
|
||||
}
|
||||
|
||||
private String buildTencentLocationName(JsonNode addressComponent) {
|
||||
|
|
@ -1282,7 +1282,8 @@ public class WorkServiceImpl implements WorkService {
|
|||
address.path("road").asText(null),
|
||||
address.path("pedestrian").asText(null),
|
||||
address.path("residential").asText(null),
|
||||
address.path("city_block").asText(null)),
|
||||
address.path("city_block").asText(null),
|
||||
address.path("commercial").asText(null)),
|
||||
address.path("house_number").asText(null),
|
||||
firstNonBlank(
|
||||
address.path("neighbourhood").asText(null),
|
||||
|
|
@ -1290,6 +1291,7 @@ public class WorkServiceImpl implements WorkService {
|
|||
address.path("hamlet").asText(null)),
|
||||
firstNonBlank(
|
||||
address.path("building").asText(null),
|
||||
address.path("commercial").asText(null),
|
||||
address.path("amenity").asText(null),
|
||||
address.path("office").asText(null),
|
||||
address.path("shop").asText(null)));
|
||||
|
|
|
|||
|
|
@ -84,12 +84,32 @@ export async function resolveTencentMapLocation() {
|
|||
|
||||
const bestResult = pickBetterLocationResult(sdkResult, browserResult);
|
||||
if (bestResult) {
|
||||
return bestResult;
|
||||
return mergeLocationAddress(bestResult, sdkResult, browserResult);
|
||||
}
|
||||
|
||||
throw new Error(buildLocationFailureMessage(browserError, sdkError));
|
||||
}
|
||||
|
||||
function mergeLocationAddress(
|
||||
bestResult: TencentMapLocation,
|
||||
sdkResult: TencentMapLocation | null,
|
||||
browserResult: TencentMapLocation | null,
|
||||
) {
|
||||
if (bestResult.address) {
|
||||
return bestResult;
|
||||
}
|
||||
|
||||
const fallbackAddress = sdkResult?.address || browserResult?.address || "";
|
||||
if (!fallbackAddress) {
|
||||
return bestResult;
|
||||
}
|
||||
|
||||
return {
|
||||
...bestResult,
|
||||
address: fallbackAddress,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveWithTencentSdk() {
|
||||
const Geolocation = await loadTencentGeolocationSdk();
|
||||
|
||||
|
|
|
|||
|
|
@ -107,17 +107,29 @@ export default function Dashboard() {
|
|||
</header>
|
||||
|
||||
{loading ? (
|
||||
<DashboardSkeleton />
|
||||
<motion.div
|
||||
key="dashboard-skeleton"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.22, ease: "easeOut" }}
|
||||
>
|
||||
<DashboardSkeleton />
|
||||
</motion.div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-2.5 sm:gap-4 xl:grid-cols-4">
|
||||
<motion.div
|
||||
key="dashboard-content"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.24, ease: "easeOut" }}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-x-3 gap-y-4 sm:gap-4 xl:grid-cols-4">
|
||||
{stats.map((stat, i) => (
|
||||
<motion.div
|
||||
key={stat.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
className="crm-card rounded-xl p-3 transition-all hover:shadow-md dark:hover:bg-slate-900 sm:rounded-2xl sm:p-5"
|
||||
className="crm-card min-h-[88px] rounded-xl p-3 transition-all hover:shadow-md dark:hover:bg-slate-900 sm:min-h-[104px] sm:rounded-2xl sm:p-5"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 sm:gap-4">
|
||||
<div className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${stat.bg} sm:h-12 sm:w-12 sm:rounded-xl`}>
|
||||
|
|
@ -132,7 +144,7 @@ export default function Dashboard() {
|
|||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5 md:grid-cols-2 md:gap-6">
|
||||
<div className="mt-5 grid gap-5 md:mt-6 md:grid-cols-2 md:gap-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -263,7 +275,7 @@ export default function Dashboard() {
|
|||
) : null}
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -271,12 +283,12 @@ export default function Dashboard() {
|
|||
|
||||
function DashboardSkeleton() {
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-2.5 sm:gap-4 xl:grid-cols-4">
|
||||
<div className="animate-[fadeIn_220ms_ease-out]">
|
||||
<div className="grid grid-cols-2 gap-x-3 gap-y-4 sm:gap-4 xl:grid-cols-4">
|
||||
{[0, 1, 2, 3].map((item) => (
|
||||
<div
|
||||
key={`dashboard-stat-skeleton-${item}`}
|
||||
className="crm-card rounded-xl p-3 sm:rounded-2xl sm:p-5"
|
||||
className="crm-card min-h-[88px] rounded-xl p-3 sm:min-h-[104px] sm:rounded-2xl sm:p-5"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 sm:gap-4">
|
||||
<div className="h-9 w-9 animate-pulse rounded-lg bg-slate-100 dark:bg-slate-800 sm:h-12 sm:w-12 sm:rounded-xl" />
|
||||
|
|
@ -289,7 +301,7 @@ function DashboardSkeleton() {
|
|||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5 md:grid-cols-2 md:gap-6">
|
||||
<div className="mt-5 grid gap-5 md:mt-6 md:grid-cols-2 md:gap-6">
|
||||
<div className="crm-card crm-card-pad-lg rounded-2xl">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="h-5 w-20 animate-pulse rounded bg-slate-100 dark:bg-slate-800" />
|
||||
|
|
@ -341,7 +353,7 @@ function DashboardSkeleton() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, ty
|
|||
import { format } from "date-fns";
|
||||
import { zhCN } from "date-fns/locale";
|
||||
import { motion } from "motion/react";
|
||||
import { AtSign, Camera, CheckCircle2, FileText, ListTodo, MapPin, NotebookPen, Plus, RefreshCw, Search, Send, Trash2, X } from "lucide-react";
|
||||
import { ArrowUp, AtSign, Camera, CheckCircle2, FileText, ListTodo, MapPin, NotebookPen, Plus, RefreshCw, Search, Send, Trash2, X } from "lucide-react";
|
||||
import { flushSync } from "react-dom";
|
||||
import { Link, Navigate, useLocation } from "react-router-dom";
|
||||
import {
|
||||
|
|
@ -28,7 +28,7 @@ import {
|
|||
type WorkTomorrowPlanItem,
|
||||
} from "@/lib/auth";
|
||||
import { ProtectedImage } from "@/components/ProtectedImage";
|
||||
import { resolveTencentMapLocation } from "@/lib/tencentMap";
|
||||
import { gcj02ToWgs84, resolveTencentMapLocation, wgs84ToGcj02 } from "@/lib/tencentMap";
|
||||
import { loadTencentMapGlApi } from "@/lib/tencentMapGl";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
@ -75,6 +75,8 @@ const defaultReportForm: CreateWorkDailyReportPayload = {
|
|||
|
||||
const LOCATION_ADJUST_RADIUS_METERS = 120;
|
||||
const CHECK_IN_ACCURACY_CONFIRM_THRESHOLD_METERS = 80;
|
||||
const LOCATION_DISPLAY_CACHE_STORAGE_KEY = "work:location-display-cache";
|
||||
const LOCATION_DISPLAY_CACHE_TTL_MS = 3 * 24 * 60 * 60 * 1000;
|
||||
const locationDisplayCache = new Map<string, string>();
|
||||
|
||||
type WorkRelationOption = {
|
||||
|
|
@ -131,6 +133,8 @@ export default function Work() {
|
|||
const hasAutoRefreshedLocation = useRef(false);
|
||||
const photoInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const historyLoadMoreRef = useRef<HTMLDivElement | null>(null);
|
||||
const historyScrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const historyScrollIdleTimerRef = useRef<number | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshingLocation, setRefreshingLocation] = useState(false);
|
||||
const [submittingCheckIn, setSubmittingCheckIn] = useState(false);
|
||||
|
|
@ -152,6 +156,7 @@ export default function Work() {
|
|||
const [historyLoadingMore, setHistoryLoadingMore] = useState(false);
|
||||
const [historyHasMore, setHistoryHasMore] = useState(false);
|
||||
const [historyPage, setHistoryPage] = useState(1);
|
||||
const [showHistoryBackToTop, setShowHistoryBackToTop] = useState(false);
|
||||
const [reportStatus, setReportStatus] = useState<string>();
|
||||
const [currentUser, setCurrentUser] = useState<UserProfile | null>(null);
|
||||
const [profileOverview, setProfileOverview] = useState<ProfileOverview | null>(null);
|
||||
|
|
@ -237,6 +242,43 @@ export default function Work() {
|
|||
return () => observer.disconnect();
|
||||
}, [historyHasMore, historyLoading, historyLoadingMore, historyPage, historySection]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = historyScrollContainerRef.current;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
const shouldShow = container.scrollTop > 280;
|
||||
if (!shouldShow) {
|
||||
setShowHistoryBackToTop(false);
|
||||
if (historyScrollIdleTimerRef.current !== null) {
|
||||
window.clearTimeout(historyScrollIdleTimerRef.current);
|
||||
historyScrollIdleTimerRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setShowHistoryBackToTop(false);
|
||||
if (historyScrollIdleTimerRef.current !== null) {
|
||||
window.clearTimeout(historyScrollIdleTimerRef.current);
|
||||
}
|
||||
historyScrollIdleTimerRef.current = window.setTimeout(() => {
|
||||
setShowHistoryBackToTop(true);
|
||||
}, 180);
|
||||
};
|
||||
|
||||
handleScroll();
|
||||
container.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => {
|
||||
container.removeEventListener("scroll", handleScroll);
|
||||
if (historyScrollIdleTimerRef.current !== null) {
|
||||
window.clearTimeout(historyScrollIdleTimerRef.current);
|
||||
historyScrollIdleTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [mobilePanel, historyData, historyLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!historyDetailItem) {
|
||||
return;
|
||||
|
|
@ -770,7 +812,10 @@ export default function Work() {
|
|||
<MobilePanelToggle mobilePanel={mobilePanel} onChange={setMobilePanel} />
|
||||
</div>
|
||||
|
||||
<div className="max-h-[calc(100vh-12rem)] space-y-4 overflow-y-auto pr-2 scrollbar-hide">
|
||||
<div
|
||||
ref={historyScrollContainerRef}
|
||||
className="relative max-h-[calc(100vh-12rem)] space-y-4 overflow-y-auto pr-2 scrollbar-hide"
|
||||
>
|
||||
{historyLoading && historyData.length === 0 ? <WorkHistorySkeleton /> : null}
|
||||
|
||||
{historyData.map((item, index) => (
|
||||
|
|
@ -798,6 +843,23 @@ export default function Work() {
|
|||
{historyLoadingMore ? "加载更多记录中..." : "下拉到底自动加载更多"}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<motion.button
|
||||
type="button"
|
||||
initial={false}
|
||||
animate={{
|
||||
opacity: showHistoryBackToTop ? 1 : 0,
|
||||
scale: showHistoryBackToTop ? 1 : 0.92,
|
||||
y: showHistoryBackToTop ? 0 : 8,
|
||||
}}
|
||||
transition={{ duration: 0.18, ease: "easeOut" }}
|
||||
onClick={() => historyScrollContainerRef.current?.scrollTo({ top: 0, behavior: "smooth" })}
|
||||
className="sticky bottom-4 ml-auto flex h-10 w-10 items-center justify-center rounded-full border border-violet-200 bg-white/92 text-violet-600 shadow-lg backdrop-blur transition-colors hover:bg-violet-50 dark:border-violet-500/30 dark:bg-slate-900/92 dark:text-violet-300 dark:hover:bg-slate-800"
|
||||
style={{ pointerEvents: showHistoryBackToTop ? "auto" : "none" }}
|
||||
aria-label="回到历史记录顶部"
|
||||
>
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -950,6 +1012,8 @@ function LocationAdjustModal({
|
|||
const [selectionHint, setSelectionHint] = useState("");
|
||||
const [resolvingAddress, setResolvingAddress] = useState(false);
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
const originMapPoint = useMemo(() => wgs84ToGcj02(origin.latitude, origin.longitude), [origin.latitude, origin.longitude]);
|
||||
const currentMapPoint = useMemo(() => wgs84ToGcj02(currentPoint.latitude, currentPoint.longitude), [currentPoint.latitude, currentPoint.longitude]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
|
@ -970,7 +1034,7 @@ function LocationAdjustModal({
|
|||
|
||||
const createLatLng = (latitude: number, longitude: number) => new TMap.LatLng(latitude, longitude);
|
||||
const map = new TMap.Map(mapContainerRef.current, {
|
||||
center: createLatLng(currentPoint.latitude, currentPoint.longitude),
|
||||
center: createLatLng(currentMapPoint.latitude, currentMapPoint.longitude),
|
||||
zoom: 18,
|
||||
pitch: 0,
|
||||
rotation: 0,
|
||||
|
|
@ -991,7 +1055,7 @@ function LocationAdjustModal({
|
|||
{
|
||||
id: "origin",
|
||||
styleId: "origin",
|
||||
position: createLatLng(origin.latitude, origin.longitude),
|
||||
position: createLatLng(originMapPoint.latitude, originMapPoint.longitude),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -1010,7 +1074,7 @@ function LocationAdjustModal({
|
|||
{
|
||||
id: "range",
|
||||
styleId: "allowed",
|
||||
center: createLatLng(origin.latitude, origin.longitude),
|
||||
center: createLatLng(originMapPoint.latitude, originMapPoint.longitude),
|
||||
radius,
|
||||
},
|
||||
],
|
||||
|
|
@ -1024,17 +1088,18 @@ function LocationAdjustModal({
|
|||
return;
|
||||
}
|
||||
|
||||
const nextPoint = {
|
||||
latitude: Number(latitude.toFixed(6)),
|
||||
longitude: Number(longitude.toFixed(6)),
|
||||
const nextPoint = gcj02ToWgs84(latitude, longitude);
|
||||
const normalizedPoint = {
|
||||
latitude: Number(nextPoint.latitude.toFixed(6)),
|
||||
longitude: Number(nextPoint.longitude.toFixed(6)),
|
||||
};
|
||||
const distance = calculateDistanceMeters(origin, nextPoint);
|
||||
const distance = calculateDistanceMeters(origin, normalizedPoint);
|
||||
if (distance > radius) {
|
||||
setSelectionHint(`仅可在原始定位附近 ${radius} 米内微调,当前超出约 ${Math.round(distance - radius)} 米。`);
|
||||
} else {
|
||||
setSelectionHint(`当前准星距原始定位约 ${Math.round(distance)} 米。`);
|
||||
}
|
||||
setSelectedPoint(nextPoint);
|
||||
setSelectedPoint(normalizedPoint);
|
||||
};
|
||||
|
||||
map.on("moveend", syncCenterSelection);
|
||||
|
|
@ -1066,7 +1131,7 @@ function LocationAdjustModal({
|
|||
mapRef.current.map?.destroy?.();
|
||||
mapRef.current = {};
|
||||
};
|
||||
}, [currentPoint.latitude, currentPoint.longitude, origin.latitude, origin.longitude]);
|
||||
}, [currentMapPoint.latitude, currentMapPoint.longitude, originMapPoint.latitude, originMapPoint.longitude, origin.latitude, origin.longitude, radius]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
|
@ -1156,7 +1221,7 @@ function LocationAdjustModal({
|
|||
setSelectedPoint(origin);
|
||||
setSelectionHint("已恢复到原始定位。");
|
||||
if (mapRef.current.createLatLng) {
|
||||
mapRef.current.map?.panTo?.(mapRef.current.createLatLng(origin.latitude, origin.longitude));
|
||||
mapRef.current.map?.panTo?.(mapRef.current.createLatLng(originMapPoint.latitude, originMapPoint.longitude));
|
||||
}
|
||||
}}
|
||||
className="crm-btn min-w-0 flex-1 rounded-xl border border-slate-200 bg-white text-slate-700 transition-colors hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
|
||||
|
|
@ -2146,29 +2211,93 @@ function isReadableLocationText(value?: string) {
|
|||
return normalizedValue.length >= 6;
|
||||
}
|
||||
|
||||
function hasDetailedLocationFeature(value?: string) {
|
||||
const normalizedValue = normalizeLocationText(value);
|
||||
if (!normalizedValue) {
|
||||
return false;
|
||||
}
|
||||
return /(街道|大道|路|街|巷|号|栋|座|室|园区|商务区|广场|大厦|中心|大楼|社区)/.test(normalizedValue);
|
||||
}
|
||||
|
||||
function buildLocationCacheKey(latitude: number, longitude: number) {
|
||||
return `${latitude.toFixed(4)},${longitude.toFixed(4)}`;
|
||||
return `${latitude.toFixed(5)},${longitude.toFixed(5)}`;
|
||||
}
|
||||
|
||||
function readPersistedLocationCacheValue(cacheKey: string) {
|
||||
if (typeof window === "undefined") {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
const rawValue = window.localStorage.getItem(LOCATION_DISPLAY_CACHE_STORAGE_KEY);
|
||||
if (!rawValue) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const cacheRecord = JSON.parse(rawValue) as Record<string, { value?: string; expiresAt?: number }>;
|
||||
const cacheEntry = cacheRecord?.[cacheKey];
|
||||
if (!cacheEntry || typeof cacheEntry.expiresAt !== "number" || cacheEntry.expiresAt <= Date.now()) {
|
||||
if (cacheEntry) {
|
||||
delete cacheRecord[cacheKey];
|
||||
window.localStorage.setItem(LOCATION_DISPLAY_CACHE_STORAGE_KEY, JSON.stringify(cacheRecord));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
return normalizeLocationText(cacheEntry.value);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function persistLocationCacheValue(cacheKey: string, value: string) {
|
||||
if (typeof window === "undefined" || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const rawValue = window.localStorage.getItem(LOCATION_DISPLAY_CACHE_STORAGE_KEY);
|
||||
const cacheRecord = rawValue
|
||||
? JSON.parse(rawValue) as Record<string, { value?: string; expiresAt?: number }>
|
||||
: {};
|
||||
|
||||
cacheRecord[cacheKey] = {
|
||||
value,
|
||||
expiresAt: Date.now() + LOCATION_DISPLAY_CACHE_TTL_MS,
|
||||
};
|
||||
window.localStorage.setItem(LOCATION_DISPLAY_CACHE_STORAGE_KEY, JSON.stringify(cacheRecord));
|
||||
} catch {
|
||||
// ignore local persistence failures
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveLocationDisplayName(latitude: number, longitude: number, preferredText?: string) {
|
||||
const cacheKey = buildLocationCacheKey(latitude, longitude);
|
||||
const normalizedPreferredText = normalizeLocationText(preferredText);
|
||||
if (isReadableLocationText(normalizedPreferredText)) {
|
||||
locationDisplayCache.set(buildLocationCacheKey(latitude, longitude), normalizedPreferredText);
|
||||
if (isReadableLocationText(normalizedPreferredText) && hasDetailedLocationFeature(normalizedPreferredText)) {
|
||||
locationDisplayCache.set(cacheKey, normalizedPreferredText);
|
||||
persistLocationCacheValue(cacheKey, normalizedPreferredText);
|
||||
return normalizedPreferredText;
|
||||
}
|
||||
|
||||
const cacheKey = buildLocationCacheKey(latitude, longitude);
|
||||
const cachedLocation = locationDisplayCache.get(cacheKey);
|
||||
if (isReadableLocationText(cachedLocation)) {
|
||||
if (isReadableLocationText(cachedLocation) && hasDetailedLocationFeature(cachedLocation)) {
|
||||
return cachedLocation;
|
||||
}
|
||||
|
||||
const persistedLocation = readPersistedLocationCacheValue(cacheKey);
|
||||
if (isReadableLocationText(persistedLocation) && hasDetailedLocationFeature(persistedLocation)) {
|
||||
locationDisplayCache.set(cacheKey, persistedLocation);
|
||||
return persistedLocation;
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
try {
|
||||
const address = await reverseWorkGeocode(latitude, longitude);
|
||||
if (isReadableLocationText(address)) {
|
||||
const normalizedAddress = normalizeLocationText(address);
|
||||
locationDisplayCache.set(cacheKey, normalizedAddress);
|
||||
persistLocationCacheValue(cacheKey, normalizedAddress);
|
||||
return normalizedAddress;
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -2176,6 +2305,16 @@ async function resolveLocationDisplayName(latitude: number, longitude: number, p
|
|||
}
|
||||
}
|
||||
|
||||
if (isReadableLocationText(persistedLocation)) {
|
||||
return normalizeLocationText(persistedLocation);
|
||||
}
|
||||
if (isReadableLocationText(cachedLocation)) {
|
||||
return normalizeLocationText(cachedLocation);
|
||||
}
|
||||
if (isReadableLocationText(normalizedPreferredText)) {
|
||||
return normalizedPreferredText;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -309,88 +309,38 @@
|
|||
|
||||
@media (max-width: 1024px) {
|
||||
.login-page-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 28px;
|
||||
align-items: start;
|
||||
padding-top: max(24px, env(safe-area-inset-top));
|
||||
padding-bottom: max(24px, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.login-page-brand {
|
||||
justify-content: flex-start;
|
||||
gap: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.login-hero-copy h2 {
|
||||
font-size: clamp(2.2rem, 10vw, 3.6rem);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 0;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
padding:
|
||||
max(20px, env(safe-area-inset-top))
|
||||
20px
|
||||
max(24px, calc(20px + env(safe-area-inset-bottom)));
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
order: -1;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-page-brand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-panel-card {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.login-page-grid {
|
||||
gap: 20px;
|
||||
padding:
|
||||
max(16px, env(safe-area-inset-top))
|
||||
16px
|
||||
max(20px, calc(16px + env(safe-area-inset-bottom)));
|
||||
}
|
||||
|
||||
.login-brand-lockup {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.login-brand-mark {
|
||||
height: 56px;
|
||||
width: 56px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.login-brand-kicker {
|
||||
margin-bottom: 4px;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
}
|
||||
|
||||
.login-brand-lockup h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.login-page-brand {
|
||||
gap: 22px;
|
||||
}
|
||||
|
||||
.login-hero-tag {
|
||||
margin-bottom: 14px;
|
||||
padding: 7px 12px;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.login-hero-copy h2 {
|
||||
font-size: clamp(1.9rem, 10vw, 2.6rem);
|
||||
line-height: 1.04;
|
||||
}
|
||||
|
||||
.login-hero-copy p:last-child {
|
||||
margin-top: 14px;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.login-brand-meta {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-brand-meta span {
|
||||
padding: 8px 12px;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.login-panel-card {
|
||||
max-width: none;
|
||||
padding: 20px 16px;
|
||||
|
|
|
|||
Loading…
Reference in New Issue