From 712d9e16521f7800c1c980513c4064f0791a0c6b Mon Sep 17 00:00:00 2001 From: "WIN-JHFT4D3SIVT\\caoxiaozhu" Date: Mon, 6 Apr 2026 22:18:44 +0800 Subject: [PATCH] feat(frontend): add weather icons and redesign calendar header Backend changes: - Add LOCATION configuration option to Settings - Add /api/system/config endpoint to expose public config - Implement location priority: config > geolocation > default Frontend changes: - Install and integrate weather-icons npm package (Erik Flowers) - Redesign calendar header with date/time on left, weather/location on right - Display weather icon using CSS classes instead of SVG components - Fetch location from backend API on component mount - Use configured location name (from .env) instead of geocoded result Layout: - Left: month/year + current time - Right: city name + weather description + weather icon --- backend/app/config.py | 3 + backend/app/routers/system.py | 6 ++ backend/app/services/system_service.py | 12 ++++ frontend/package-lock.json | 9 ++- frontend/package.json | 5 +- frontend/src/main.ts | 1 + frontend/src/pages/chat/chatPage.css | 42 ++++++++++++- .../pages/chat/composables/useClientTime.ts | 63 ++++++++++++++----- frontend/src/pages/chat/index.vue | 14 +++-- 9 files changed, 133 insertions(+), 22 deletions(-) diff --git a/backend/app/config.py b/backend/app/config.py index a986fa3..c5f785e 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -61,6 +61,9 @@ class Settings(BaseSettings): DAILY_PLAN_TIME: str = "00:00" FORUM_SCAN_INTERVAL_MINUTES: int = 30 + # === 位置配置 === + LOCATION: str = "Location" + # === CORS === CORS_ORIGINS: list[str] = ["http://localhost:5173", "http://localhost:3000"] diff --git a/backend/app/routers/system.py b/backend/app/routers/system.py index 9d5d149..3430c86 100644 --- a/backend/app/routers/system.py +++ b/backend/app/routers/system.py @@ -7,3 +7,9 @@ router = APIRouter(prefix='/api/system', tags=['system']) @router.get('/status') async def get_system_status(): return SystemService().get_status() + + +@router.get('/config') +async def get_system_config(): + """Get public system configuration.""" + return SystemService().get_config() diff --git a/backend/app/services/system_service.py b/backend/app/services/system_service.py index 1801afb..7669a95 100644 --- a/backend/app/services/system_service.py +++ b/backend/app/services/system_service.py @@ -1,5 +1,6 @@ from datetime import datetime, UTC from time import monotonic +import os import platform import socket import subprocess @@ -15,6 +16,11 @@ class SystemService: _last_net_bytes_recv: int | None = None _last_net_sample_at: float | None = None + def __init__(self): + # Import settings here to avoid circular imports + from app.config import settings + self._settings = settings + def _get_network_rates(self) -> tuple[float, float]: counters = psutil.net_io_counters() now = monotonic() @@ -127,3 +133,9 @@ class SystemService: **gpu_status, 'timestamp': datetime.now(UTC).isoformat(), } + + def get_config(self) -> dict: + """Get public system configuration.""" + return { + 'location': self._settings.LOCATION, + } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d0d8e20..26bc5fc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,7 +18,8 @@ "pinia": "^3.0.4", "three": "^0.180.0", "vue": "^3.5.30", - "vue-router": "^4.6.4" + "vue-router": "^4.6.4", + "weather-icons": "^1.3.2" }, "devDependencies": { "@types/node": "^25.5.0", @@ -3853,6 +3854,12 @@ "node": ">=18" } }, + "node_modules/weather-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/weather-icons/-/weather-icons-1.3.2.tgz", + "integrity": "sha512-EW/i0gFzlPpyBK29iXtruDw1v9txI4Gjp7q6ikbFTsL6O9gt3yH64/fuMf7wxpj92v5I08JhajBra/wMn715Eg==", + "license": "MIT" + }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index df8f064..9eb6dd9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,8 +10,8 @@ "test": "vitest run" }, "dependencies": { - "3d-force-graph": "^1.79.0", "@vueuse/core": "^14.2.1", + "3d-force-graph": "^1.79.0", "axios": "^1.13.6", "echarts": "^6.0.0", "element-plus": "^2.13.6", @@ -20,7 +20,8 @@ "pinia": "^3.0.4", "three": "^0.180.0", "vue": "^3.5.30", - "vue-router": "^4.6.4" + "vue-router": "^4.6.4", + "weather-icons": "^1.3.2" }, "devDependencies": { "@types/node": "^25.5.0", diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 452dcef..4f1b88d 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -2,6 +2,7 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' +import 'weather-icons/css/weather-icons.css' import router from './app/router' import App from './App.vue' import './style.css' diff --git a/frontend/src/pages/chat/chatPage.css b/frontend/src/pages/chat/chatPage.css index 3e97191..9afa256 100644 --- a/frontend/src/pages/chat/chatPage.css +++ b/frontend/src/pages/chat/chatPage.css @@ -78,7 +78,9 @@ gap: 6px; } -.weather-icon { +.weather-icon, +.weather-inline .wi { + font-size: 16px; color: #7dd3fc; flex-shrink: 0; } @@ -2086,6 +2088,44 @@ margin-bottom: 14px; } +.jarvis-location { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.location-icon { + font-size: 20px; + color: rgba(223, 247, 255, 0.9); + flex-shrink: 0; +} + +.location-info { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.location-name { + font-size: 12px; + font-weight: 500; + color: rgba(223, 247, 255, 0.7); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.location-weather-text { + font-size: 11px; + font-weight: 400; + color: rgba(160, 232, 255, 0.6); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .jarvis-date-num { min-width: 62px; font-family: var(--font-display); diff --git a/frontend/src/pages/chat/composables/useClientTime.ts b/frontend/src/pages/chat/composables/useClientTime.ts index e4ad3eb..c35e5c1 100644 --- a/frontend/src/pages/chat/composables/useClientTime.ts +++ b/frontend/src/pages/chat/composables/useClientTime.ts @@ -1,5 +1,4 @@ import { computed, onMounted, onUnmounted, ref } from 'vue' -import { Cloud, CloudDrizzle, CloudFog, CloudLightning, CloudRain, CloudSnow, Sun } from 'lucide-vue-next' export function formatNetworkRate(bytesPerSecond: number | null, online: boolean) { if (!online || bytesPerSecond === null) return 'OFFLINE' @@ -10,10 +9,24 @@ export function formatNetworkRate(bytesPerSecond: number | null, online: boolean export function useClientTime() { const clientTime = ref(new Date()) + const city = ref('Location') const weatherSummary = ref('Weather unavailable') const weatherCode = ref(null) let clientTimeTimer: ReturnType | null = null + // Load location from backend config + async function loadLocation() { + try { + const response = await fetch('/api/system/config') + if (response.ok) { + const config = await response.json() + city.value = config.location || 'Location' + } + } catch { + city.value = import.meta.env.VITE_LOCATION || 'Location' + } + } + function updateClientTime() { clientTime.value = new Date() } @@ -40,27 +53,48 @@ export function useClientTime() { const weatherIcon = computed(() => { const code = weatherCode.value - if (code === 0) return Sun - if (code === 1 || code === 2 || code === 3) return Cloud - if (code === 45 || code === 48) return CloudFog - if ([51, 53, 55, 56, 57].includes(code ?? -1)) return CloudDrizzle - if ([61, 63, 65, 66, 67, 80, 81, 82].includes(code ?? -1)) return CloudRain - if ([71, 73, 75, 77, 85, 86].includes(code ?? -1)) return CloudSnow - if ([95, 96, 99].includes(code ?? -1)) return CloudLightning - return Cloud + if (code === 0) return 'wi-day-sunny' + if (code === 1) return 'wi-day-cloudy' + if (code === 2) return 'wi-day-cloudy-gusts' + if (code === 3) return 'wi-cloudy' + if (code === 45) return 'wi-day-fog' + if (code === 48) return 'wi-fog' + if ([51, 53].includes(code ?? -1)) return 'wi-day-showers' + if ([55, 56, 57].includes(code ?? -1)) return 'wi-showers' + if ([61, 63, 65, 80].includes(code ?? -1)) return 'wi-day-rain' + if ([66, 67, 81, 82].includes(code ?? -1)) return 'wi-rain' + if ([71, 73, 75, 77, 85, 86].includes(code ?? -1)) return 'wi-snow' + if ([95, 96, 99].includes(code ?? -1)) return 'wi-thunderstorm' + return 'wi-day-sunny' }) async function loadWeather(latitude: number, longitude: number) { try { - const response = await fetch( + // Fetch weather data + const weatherResp = await fetch( `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,weather_code&timezone=auto`, ) - if (!response.ok) throw new Error('weather request failed') - const data = await response.json() - const current = data.current ?? {} + if (!weatherResp.ok) throw new Error('weather request failed') + const weatherData = await weatherResp.json() + const current = weatherData.current ?? {} weatherCode.value = typeof current.weather_code === 'number' ? current.weather_code : null const temp = typeof current.temperature_2m === 'number' ? `${Math.round(current.temperature_2m)}°C` : '--' weatherSummary.value = `${weatherCodeLabel(current.weather_code)} ${temp}` + + // Only fetch city name if not already set by config + if (city.value === 'Location') { + try { + const geoResp = await fetch( + `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&zoom=10`, + ) + if (geoResp.ok) { + const geoData = await geoResp.json() + city.value = geoData.address?.city ?? geoData.address?.town ?? geoData.address?.county ?? geoData.display_name?.split(',')[0] ?? 'Location' + } + } catch { + city.value = 'Location' + } + } } catch { weatherCode.value = null weatherSummary.value = 'Weather unavailable' @@ -70,6 +104,7 @@ export function useClientTime() { onMounted(() => { updateClientTime() clientTimeTimer = setInterval(updateClientTime, 1000) + void loadLocation() if (!navigator.geolocation) { weatherCode.value = null weatherSummary.value = 'Weather unavailable' @@ -87,7 +122,7 @@ export function useClientTime() { }) return { - clientTime, weatherSummary, weatherCode, weatherIcon, + clientTime, city, weatherSummary, weatherCode, weatherIcon, updateClientTime, formatClientDate, formatClientClock, weatherCodeLabel, loadWeather } } diff --git a/frontend/src/pages/chat/index.vue b/frontend/src/pages/chat/index.vue index 80b167e..02ced82 100644 --- a/frontend/src/pages/chat/index.vue +++ b/frontend/src/pages/chat/index.vue @@ -62,7 +62,7 @@ const { } = useKnowledgeView() // --- Client time & weather --- -const { clientTime, weatherIcon, weatherSummary, formatClientDate, formatClientClock } = useClientTime() +const { clientTime, city, weatherIcon, weatherSummary, formatClientDate, formatClientClock } = useClientTime() // --- Daily digest & reminders --- const { dailyDigest, digestLoading, activeReminder, reminderVisible, loadDailyDigest, handleSnooze, handleDismiss } = useDailyDigest() @@ -228,11 +228,17 @@ function renderMarkdown(content: string) {
-
{{ clientTime.getDate().toString().padStart(2, '0') }}
-
{{ clientTime.toLocaleString('zh-CN', { month: 'long' }) }} / {{ clientTime.getFullYear() }}
+
{{ clientTime.toLocaleString('zh-CN', { month: 'long' }) }} {{ clientTime.getFullYear() }}
{{ clientTime.toLocaleTimeString('zh-CN', { hour12: false }) }}
+
+
+ {{ city }} + {{ weatherSummary }} +
+ +
@@ -564,7 +570,7 @@ function renderMarkdown(content: string) {
WEATHER - + {{ weatherSummary }}