diff --git a/deploy/.env.example b/deploy/.env.example index 0291a61..92a96f5 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -17,3 +17,6 @@ WECHAT_APP_SECRET= ADMIN_USERNAME=sundynix ADMIN_PASSWORD=sundynix TAG=latest + +# 高德 Web 服务 Key +AMAP_KEY= diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 84af1ea..c40c4d1 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -5,6 +5,7 @@ services: ports: - "4000:9090" environment: + PETS_AMAP_KEY: ${AMAP_KEY:-} PETS_SERVER_PORT: "9090" PETS_SERVER_MODE: release PETS_POSTGRES_HOST: ${POSTGRES_HOST:-192.168.100.127} diff --git a/pets-be/configs/config.example.yaml b/pets-be/configs/config.example.yaml index e563072..8c9f4cc 100644 --- a/pets-be/configs/config.example.yaml +++ b/pets-be/configs/config.example.yaml @@ -31,3 +31,7 @@ auth: admin: username: sundynix password: sundynix + +# 高德 Web 服务:首页所在城市与实况天气 +amap: + key: "" diff --git a/pets-be/configs/config.yaml b/pets-be/configs/config.yaml index 361ab1d..6a1a66c 100644 --- a/pets-be/configs/config.yaml +++ b/pets-be/configs/config.yaml @@ -30,3 +30,7 @@ auth: admin: username: sundynix password: sundynix + +# 高德 Web 服务:首页所在城市与实况天气 +amap: + key: "c6e0887a6cd0aeaf8f1fd3dd5704b8c3" diff --git a/pets-be/internal/config/config.go b/pets-be/internal/config/config.go index d52d256..11af12c 100644 --- a/pets-be/internal/config/config.go +++ b/pets-be/internal/config/config.go @@ -9,7 +9,12 @@ import ( "github.com/spf13/viper" ) +type AMapConfig struct { + Key string `mapstructure:"key"` +} + type Config struct { + AMap AMapConfig `mapstructure:"amap"` Server ServerConfig `mapstructure:"server"` Postgres PostgresConfig `mapstructure:"postgres"` MinIO MinIOConfig `mapstructure:"minio"` @@ -67,6 +72,7 @@ type AdminConfig struct { func Load() (*Config, error) { v := viper.New() + v.SetDefault("amap.key", "") v.SetDefault("server.port", 9090) v.SetDefault("server.mode", "debug") v.SetDefault("postgres.host", "192.168.100.127") diff --git a/pets-be/internal/environment/amap.go b/pets-be/internal/environment/amap.go new file mode 100644 index 0000000..7a8f050 --- /dev/null +++ b/pets-be/internal/environment/amap.go @@ -0,0 +1,146 @@ +package environment + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "net/http" + "net/url" + "strings" + "sync" + "time" +) + +var ErrNotConfigured = errors.New("位置和天气服务暂未配置") +var ErrCoordinates = errors.New("定位坐标无效") + +type Weather struct { + Description string `json:"description"` + Temperature string `json:"temperature"` + ReportedAt string `json:"reported_at"` +} +type Result struct { + LocationName string `json:"location_name"` + City string `json:"city"` + District string `json:"district"` + Weather *Weather `json:"weather"` +} +type cached struct { + result Result + expires time.Time +} +type Client struct { + key string + baseURL string + http *http.Client + mu sync.Mutex + cache map[string]cached +} + +func New(key string) *Client { + return &Client{key: key, baseURL: "https://restapi.amap.com", http: &http.Client{Timeout: 5 * time.Second}, cache: make(map[string]cached)} +} + +// AMap returns [] instead of an empty string for some address components. +func addressText(raw json.RawMessage) string { + var text string + _ = json.Unmarshal(raw, &text) + return text +} +func (c *Client) get(ctx context.Context, path string, params url.Values, target any) error { + params.Set("key", c.key) + params.Set("output", "JSON") + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path+"?"+params.Encode(), nil) + if err != nil { + return errors.New("位置服务请求失败") + } + res, err := c.http.Do(req) + // Never return the transport error: it may contain the URL and API key. + if err != nil { + return errors.New("位置服务暂时不可用") + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return errors.New("位置服务暂时不可用") + } + if err := json.NewDecoder(io.LimitReader(res.Body, 1<<20)).Decode(target); err != nil { + return errors.New("位置服务响应无效") + } + return nil +} +func (c *Client) Lookup(ctx context.Context, latitude, longitude float64) (Result, error) { + if math.IsNaN(latitude) || math.IsNaN(longitude) || math.IsInf(latitude, 0) || math.IsInf(longitude, 0) || latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180 { + return Result{}, ErrCoordinates + } + if c.key == "" { + return Result{}, ErrNotConfigured + } + cacheKey := fmt.Sprintf("%.3f,%.3f", longitude, latitude) + c.mu.Lock() + hit, found := c.cache[cacheKey] + c.mu.Unlock() + if found && time.Now().Before(hit.expires) { + return hit.result, nil + } + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + var geo struct { + Status string `json:"status"` + Regeocode struct { + Address struct { + Province json.RawMessage `json:"province"` + City json.RawMessage `json:"city"` + District json.RawMessage `json:"district"` + Adcode string `json:"adcode"` + } `json:"addressComponent"` + } `json:"regeocode"` + } + err := c.get(ctx, "/v3/geocode/regeo", url.Values{"location": {fmt.Sprintf("%.6f,%.6f", longitude, latitude)}, "extensions": {"base"}}, &geo) + if err != nil { + return Result{}, err + } + if geo.Status != "1" { + return Result{}, errors.New("所在城市获取失败,请稍后重试") + } + address := geo.Regeocode.Address + city := addressText(address.City) + if city == "" { + city = addressText(address.Province) + } + district := addressText(address.District) + if city == "" || address.Adcode == "" { + return Result{}, errors.New("当前位置暂不支持天气查询") + } + result := Result{City: city, District: district, LocationName: city} + if district != "" && district != city { + result.LocationName += " · " + district + } + var weather struct { + Status string `json:"status"` + Lives []struct { + Description string `json:"weather"` + Temperature string `json:"temperature"` + ReportedAt string `json:"reporttime"` + } `json:"lives"` + } + err = c.get(ctx, "/v3/weather/weatherInfo", url.Values{"city": {address.Adcode}, "extensions": {"base"}}, &weather) + if err == nil && weather.Status == "1" && len(weather.Lives) > 0 { + live := weather.Lives[0] + if strings.TrimSpace(live.Description) != "" && live.Temperature != "" { + result.Weather = &Weather{live.Description, live.Temperature, live.ReportedAt} + } + } + // Keep the city even if the weather provider fails; retry missing weather next time. + if result.Weather != nil { + c.mu.Lock() + if len(c.cache) >= 256 { + clear(c.cache) + } + c.cache[cacheKey] = cached{result, time.Now().Add(15 * time.Minute)} + c.mu.Unlock() + } + return result, nil +} diff --git a/pets-be/internal/environment/amap_test.go b/pets-be/internal/environment/amap_test.go new file mode 100644 index 0000000..643e0a4 --- /dev/null +++ b/pets-be/internal/environment/amap_test.go @@ -0,0 +1,86 @@ +package environment + +import ( + "context" + "fmt" + "math" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" +) + +func TestLookupMunicipalityAndCache(t *testing.T) { + var calls atomic.Int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls.Add(1) + if r.URL.Query().Get("key") != "test-key" { + t.Error("key missing") + } + switch r.URL.Path { + case "/v3/geocode/regeo": + if r.URL.Query().Get("location") != "121.473700,31.230400" { + t.Errorf("wrong coordinate order: %s", r.URL.Query().Get("location")) + } + fmt.Fprint(w, `{"status":"1","regeocode":{"addressComponent":{"province":"上海市","city":[],"district":"黄浦区","adcode":"310101"}}}`) + case "/v3/weather/weatherInfo": + if r.URL.Query().Get("city") != "310101" { + t.Error("weather must use adcode") + } + fmt.Fprint(w, `{"status":"1","lives":[{"weather":"晴","temperature":"24","reporttime":"2026-10-04 10:00:00"}]}`) + default: + t.Error("unexpected path") + } + })) + defer srv.Close() + client := New("test-key") + client.baseURL = srv.URL + for range 2 { + result, err := client.Lookup(context.Background(), 31.2304, 121.4737) + if err != nil { + t.Fatal(err) + } + if result.LocationName != "上海市 · 黄浦区" || result.Weather == nil || result.Weather.Temperature != "24" { + t.Fatalf("unexpected result: %+v", result) + } + } + if calls.Load() != 2 { + t.Errorf("cache did not reduce calls: %d", calls.Load()) + } +} +func TestPartialWeatherFailure(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/v3/geocode/regeo" { + fmt.Fprint(w, `{"status":"1","regeocode":{"addressComponent":{"province":"浙江省","city":"杭州市","district":"西湖区","adcode":"330106"}}}`) + } else { + fmt.Fprint(w, `{"status":"0","info":"INVALID_USER_KEY"}`) + } + })) + defer srv.Close() + client := New("test-key") + client.baseURL = srv.URL + result, err := client.Lookup(context.Background(), 30.2, 120.1) + if err != nil || result.LocationName != "杭州市 · 西湖区" || result.Weather != nil { + t.Fatalf("must retain city without fabricated weather: %+v %v", result, err) + } +} +func TestInvalidInputsAndProviderError(t *testing.T) { + client := New("") + if _, err := client.Lookup(context.Background(), 30, 120); err != ErrNotConfigured { + t.Fatal(err) + } + for _, latitude := range []float64{91, math.NaN(), math.Inf(1)} { + if _, err := client.Lookup(context.Background(), latitude, 120); err != ErrCoordinates { + t.Fatal(err) + } + } + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `{"status":"0","info":"INVALID_USER_KEY"}`) + })) + defer srv.Close() + client = New("secret-key") + client.baseURL = srv.URL + if _, err := client.Lookup(context.Background(), 30, 120); err == nil { + t.Fatal("provider rejection accepted") + } +} diff --git a/pets-be/internal/httpapi/environment.go b/pets-be/internal/httpapi/environment.go new file mode 100644 index 0000000..47fd174 --- /dev/null +++ b/pets-be/internal/httpapi/environment.go @@ -0,0 +1,31 @@ +package httpapi + +import ( + "errors" + "github.com/gin-gonic/gin" + "github.com/sundynix/pets-be/internal/environment" + "net/http" + "strconv" +) + +func (a *API) environment(c *gin.Context) { + lat, latErr := strconv.ParseFloat(c.Query("latitude"), 64) + lon, lonErr := strconv.ParseFloat(c.Query("longitude"), 64) + if latErr != nil || lonErr != nil { + fail(c, http.StatusBadRequest, "请提供有效的定位坐标") + return + } + result, err := a.env.Lookup(c.Request.Context(), lat, lon) + if err != nil { + status := http.StatusBadGateway + if errors.Is(err, environment.ErrCoordinates) { + status = http.StatusBadRequest + } + if errors.Is(err, environment.ErrNotConfigured) { + status = http.StatusServiceUnavailable + } + fail(c, status, err.Error()) + return + } + ok(c, result) +} diff --git a/pets-be/internal/httpapi/server.go b/pets-be/internal/httpapi/server.go index 83c51ac..934fceb 100644 --- a/pets-be/internal/httpapi/server.go +++ b/pets-be/internal/httpapi/server.go @@ -12,18 +12,20 @@ import ( "gorm.io/gorm" "github.com/sundynix/pets-be/internal/config" + "github.com/sundynix/pets-be/internal/environment" "github.com/sundynix/pets-be/internal/model" "github.com/sundynix/pets-be/internal/storage" ) type API struct { + env *environment.Client db *gorm.DB storage *storage.Storage cfg *config.Config } func New(db *gorm.DB, storage *storage.Storage, cfg *config.Config) *API { - return &API{db: db, storage: storage, cfg: cfg} + return &API{db: db, storage: storage, cfg: cfg, env: environment.New(cfg.AMap.Key)} } func (a *API) Register(r *gin.Engine) { @@ -38,6 +40,7 @@ func (a *API) Register(r *gin.Engine) { user := api.Group("") user.Use(a.requireRole("user")) + user.GET("/environment", a.environment) user.GET("/me", a.me) user.GET("/pets", a.listPets) user.POST("/pets", a.createPet) diff --git a/pets-fe/app.json b/pets-fe/app.json index 39e1b6b..80ec895 100644 --- a/pets-fe/app.json +++ b/pets-fe/app.json @@ -13,7 +13,7 @@ "window": { "navigationStyle": "custom", "navigationBarTextStyle": "black", - "backgroundColor": "#f6f9f6", + "backgroundColor": "#fff9f3", "backgroundTextStyle": "dark" }, "usingComponents": { @@ -22,7 +22,7 @@ }, "permission": { "scope.userLocation": { - "desc": "遛狗时记录轨迹,并在你主动开启分享后展示实时位置" + "desc": "首页显示所在城市与天气,遛狗时记录轨迹,并在你主动开启分享后展示实时位置" } }, "requiredPrivateInfos": [ diff --git a/pets-fe/app.wxss b/pets-fe/app.wxss index 6026f47..dcb90c1 100644 --- a/pets-fe/app.wxss +++ b/pets-fe/app.wxss @@ -1,8 +1,8 @@ page { height: 100vh; overflow: hidden; - background: #f6f9f6; - color: #173e36; + background: #fff9f3; + color: #604a40; font-family: ui-rounded, "Hiragino Maru Gothic ProN", "PingFang SC", sans-serif; font-size: 28rpx; line-height: 1.5; @@ -32,7 +32,7 @@ button::after { padding: 16rpx 32rpx calc(170rpx + env(safe-area-inset-bottom)); } .eyebrow { - color: #8fa39b; + color: #886c5f; font-size: 22rpx; font-weight: 600; letter-spacing: 2rpx; @@ -43,17 +43,17 @@ button::after { margin: 38rpx 0 20rpx; } .muted { - color: #8b9f96; + color: #886c5f; } .card { background: #fff; border-radius: 34rpx; - box-shadow: 0 15rpx 45rpx rgba(30, 70, 55, 0.05); + box-shadow: 0 15rpx 45rpx rgba(123, 84, 58, 0.06); padding: 28rpx; margin-bottom: 20rpx; } .primary { - background: #194f42; + background: #b95738; color: white; border-radius: 23rpx; font-size: 28rpx; @@ -62,8 +62,8 @@ button::after { line-height: 1.3; } .secondary { - background: #e7f0e7; - color: #28674e; + background: #fff0e2; + color: #b95738; border-radius: 23rpx; font-size: 27rpx; font-weight: 600; @@ -78,14 +78,14 @@ button::after { display: inline-flex; align-items: center; border-radius: 99rpx; - background: #e9f3e8; - color: #36805c; + background: #fff0e2; + color: #b95738; padding: 8rpx 18rpx; font-size: 23rpx; } .empty { text-align: center; - color: #9daf9f; + color: #886c5f; padding: 64rpx 24rpx; } .field { @@ -93,7 +93,7 @@ button::after { } .field-label { font-size: 24rpx; - color: #718b7e; + color: #886c5f; margin-bottom: 12rpx; } .field input, diff --git a/pets-fe/components/app-icon/index.js b/pets-fe/components/app-icon/index.js index a2ab771..86b77cf 100644 --- a/pets-fe/components/app-icon/index.js +++ b/pets-fe/components/app-icon/index.js @@ -2,6 +2,6 @@ Component({ properties: { kind: { type: String, value: "paw" }, size: { type: Number, value: 48 }, - color: { type: String, value: "#398668" }, + color: { type: String, value: "#b95738" }, }, }); diff --git a/pets-fe/components/app-icon/index.wxml b/pets-fe/components/app-icon/index.wxml index 600ac20..a73f1e1 100644 --- a/pets-fe/components/app-icon/index.wxml +++ b/pets-fe/components/app-icon/index.wxml @@ -1,6 +1,10 @@ + + + + diff --git a/pets-fe/components/app-icon/index.wxss b/pets-fe/components/app-icon/index.wxss index 866a8d7..00065aa 100644 --- a/pets-fe/components/app-icon/index.wxss +++ b/pets-fe/components/app-icon/index.wxss @@ -10,3 +10,12 @@ .app-icon-three { right: 24%; top: 3%; transform: rotate(12deg); } .app-icon-four { right: 0; top: 26%; transform: rotate(28deg); } .app-icon-pad { left: 24%; top: 48%; width: 52%; height: 42%; border-radius: 50% 50% 45% 45%; } +.app-icon-pet { position: relative; width: 85%; height: 85%; } +.app-icon-ear { position: absolute; top: 22%; width: 27%; height: 52%; border: 3rpx solid currentColor; border-radius: 55% 40% 70% 70%; } +.app-icon-ear-left { left: -3%; transform: rotate(25deg); } +.app-icon-ear-right { right: -3%; transform: rotate(-25deg); } +.app-icon-face { position: absolute; bottom: 0; left: 12%; width: 76%; height: 68%; border: 3rpx solid currentColor; border-radius: 42% 42% 48% 48%; background: #fff; } +.app-icon-eye { position: absolute; top: 29%; width: 9%; height: 14%; border-radius: 50%; background: currentColor; } +.app-icon-eye-left { left: 23%; } +.app-icon-eye-right { right: 23%; } +.app-icon-nose { position: absolute; left: 40%; bottom: 22%; width: 20%; height: 14%; border-radius: 40% 40% 60% 60%; background: currentColor; } diff --git a/pets-fe/components/page-layout/index.js b/pets-fe/components/page-layout/index.js index a556bab..7db5261 100644 --- a/pets-fe/components/page-layout/index.js +++ b/pets-fe/components/page-layout/index.js @@ -3,6 +3,8 @@ const { navigationGeometry } = require("../../utils/layout"); Component({ properties: { title: String, + subtitle: String, + location: { type: Boolean, value: false }, back: { type: Boolean, value: false }, }, data: navigationGeometry(), @@ -17,6 +19,9 @@ Component({ }, }, methods: { + locate() { + this.triggerEvent("locationtap"); + }, updateLayout() { const info = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync(); let menu = {}; diff --git a/pets-fe/components/page-layout/index.wxml b/pets-fe/components/page-layout/index.wxml index b0f3ac1..6e1df94 100644 --- a/pets-fe/components/page-layout/index.wxml +++ b/pets-fe/components/page-layout/index.wxml @@ -1,9 +1,13 @@ + + {{title}} + {{subtitle}} + - {{title}} + {{title}} diff --git a/pets-fe/components/page-layout/index.wxss b/pets-fe/components/page-layout/index.wxss index 4183a5d..16c4d9f 100644 --- a/pets-fe/components/page-layout/index.wxss +++ b/pets-fe/components/page-layout/index.wxss @@ -4,7 +4,7 @@ left: 0; right: 0; z-index: 25; - background: #f6f9f6; + background: #fff9f3; } .navigation-row { position: absolute; @@ -19,11 +19,66 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - color: #194f42; + color: #604a40; font-size: 19px; font-weight: 600; letter-spacing: 0.5px; } +.local-header { + width: 100%; + min-width: 0; +} +.local-title { + height: 19px; + display: flex; + align-items: center; + gap: 6px; + color: #604a40; + font-size: 16px; + line-height: 19px; + font-weight: 600; +} +.local-title text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.location-pin { + position: relative; + width: 9px; + height: 9px; + flex-shrink: 0; + border: 1.5px solid #b95738; + border-radius: 50% 50% 50% 0; + transform: rotate(-45deg); +} +.location-pin::after { + content: ""; + position: absolute; + width: 2px; + height: 2px; + left: 3.5px; + top: 3.5px; + border-radius: 50%; + background: #b95738; +} +.location-chevron { + width: 4px; + height: 4px; + flex-shrink: 0; + border-right: 1px solid #886c5f; + border-bottom: 1px solid #886c5f; + transform: rotate(45deg); +} +.local-meta { + margin-top: 2px; + color: #886c5f; + font-size: 10px; + line-height: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} .back { width: 28px; height: 36px; @@ -35,8 +90,8 @@ .back-arrow { width: 10px; height: 10px; - border-left: 2px solid #194f42; - border-bottom: 2px solid #194f42; + border-left: 2px solid #604a40; + border-bottom: 2px solid #604a40; transform: rotate(45deg); } .content { diff --git a/pets-fe/components/pet-picker/index.wxss b/pets-fe/components/pet-picker/index.wxss index ab55d3a..900825c 100644 --- a/pets-fe/components/pet-picker/index.wxss +++ b/pets-fe/components/pet-picker/index.wxss @@ -8,16 +8,16 @@ align-items: center; gap: 8rpx; background: white; - border: 2rpx solid #e0eae2; - color: #527164; + border: 2rpx solid #ece2d8; + color: #886c5f; padding: 11rpx 18rpx; border-radius: 99rpx; font-size: 24rpx; } .chip.chosen { - background: #dff0e1; - border-color: #55a273; - color: #145441; + background: #fff0e2; + border-color: #b95738; + color: #b95738; font-weight: 700; } .chip image { @@ -31,5 +31,5 @@ } .select-all { border-style: dashed; - color: #2c8663; + color: #b95738; } diff --git a/pets-fe/components/tab-bar/index.json b/pets-fe/components/tab-bar/index.json index 67f8e03..60315ea 100644 --- a/pets-fe/components/tab-bar/index.json +++ b/pets-fe/components/tab-bar/index.json @@ -1 +1 @@ -{ "component": true } +{ "component": true, "usingComponents": { "app-icon": "/components/app-icon/index" } } diff --git a/pets-fe/components/tab-bar/index.wxml b/pets-fe/components/tab-bar/index.wxml index 3c6d56c..c2a82f7 100644 --- a/pets-fe/components/tab-bar/index.wxml +++ b/pets-fe/components/tab-bar/index.wxml @@ -28,7 +28,7 @@ - + {{item.label}} diff --git a/pets-fe/components/tab-bar/index.wxss b/pets-fe/components/tab-bar/index.wxss index d7e2337..a0b1664 100644 --- a/pets-fe/components/tab-bar/index.wxss +++ b/pets-fe/components/tab-bar/index.wxss @@ -7,9 +7,9 @@ display: flex; align-items: stretch; background: rgba(255, 255, 255, 0.98); - border: 2rpx solid #e7eee8; + border: 2rpx solid #ece2d8; border-radius: 42rpx; - box-shadow: 0 12rpx 42rpx rgba(26, 74, 58, 0.13); + box-shadow: 0 12rpx 42rpx rgba(123, 84, 58, 0.12); z-index: 20; } .item { @@ -20,10 +20,10 @@ flex-direction: column; align-items: center; justify-content: center; - color: #9aaa9e; + color: #9d8779; } .item.active { - color: #194f42; + color: #b95738; } .icon-wrap { width: 58rpx; @@ -34,7 +34,7 @@ border-radius: 22rpx; } .active .icon-wrap { - background: #e7f3e9; + background: #fff0e2; transform: translateY(-4rpx); } .label { @@ -48,7 +48,7 @@ width: 7rpx; height: 7rpx; border-radius: 50%; - background: #65b88f; + background: #df936a; } .home-icon { position: relative; @@ -113,7 +113,7 @@ } .friend-left { left: 1rpx; } .friend-right { right: 1rpx; background: #fff; } -.active .friend-right { background: #e7f3e9; } +.active .friend-right { background: #fff0e2; } .task-icon { position: relative; width: 35rpx; @@ -135,7 +135,6 @@ width: 46rpx; height: 46rpx; border-radius: 50%; - opacity: 0.55; } .active .pet-icon { opacity: 1; } .walk-bubble { @@ -146,22 +145,22 @@ display: flex; align-items: center; justify-content: center; - border: 8rpx solid #f6f9f6; + border: 8rpx solid #fff9f3; border-radius: 38rpx; background: linear-gradient(145deg, #f8bd8e, #e38e69); box-shadow: 0 10rpx 24rpx rgba(178, 105, 72, 0.26); transform: rotate(-8deg); } .walk.active .walk-bubble { - background: linear-gradient(145deg, #276a55, #174d40); - box-shadow: 0 10rpx 24rpx rgba(24, 74, 59, 0.25); + background: linear-gradient(145deg, #df8b61, #b95738); + box-shadow: 0 10rpx 24rpx rgba(185, 87, 56, 0.22); } .walk .label { position: absolute; bottom: 13rpx; color: #aa7356; } -.walk.active .label { color: #174d40; } +.walk.active .label { color: #b95738; } .paw { position: relative; width: 62rpx; diff --git a/pets-fe/pages/home/home.js b/pets-fe/pages/home/home.js index 24e3537..b801be1 100644 --- a/pets-fe/pages/home/home.js +++ b/pets-fe/pages/home/home.js @@ -1,7 +1,13 @@ const { secure, toast } = require("../../utils/api"); const { localDate } = require("../../utils/date"); +function dateLabel() { + const now = new Date(); + return `${now.getMonth() + 1}月${now.getDate()}日 周${"日一二三四五六"[now.getDay()]}`; +} Page({ data: { + locationName: "当前位置", + headerSubtitle: dateLabel() + " · 轻触获取天气", pets: [], selected: [], tasks: [], @@ -11,6 +17,49 @@ Page({ }, onShow() { this.load(); + this.updateEnvironmentLabel(); + if (!this.environmentAt || Date.now() - this.environmentAt > 15 * 60 * 1000) { + this.refreshEnvironment(); + } + }, + updateEnvironmentLabel() { + this.setData({ headerSubtitle: dateLabel() + " · " + (this.weatherLabel || "轻触获取天气") }); + }, + async refreshEnvironment(event) { + if (this.environmentBusy) return; + this.environmentBusy = true; + const tapped = !!event; + let positioned = false; + try { + const settings = await new Promise((resolve, reject) => wx.getSetting({ success: resolve, fail: reject })); + if (settings.authSetting["scope.userLocation"] === false) { + if (!tapped) throw new Error("location-denied"); + const enabled = await new Promise((resolve, reject) => wx.openSetting({ success: resolve, fail: reject })); + if (!enabled.authSetting["scope.userLocation"]) throw new Error("location-denied"); + } + this.weatherLabel = "正在获取天气"; + this.updateEnvironmentLabel(); + const point = await new Promise((resolve, reject) => wx.getLocation({ + type: "gcj02", + success: resolve, + fail: reject, + })); + positioned = true; + const result = await secure("/environment", "GET", { latitude: point.latitude, longitude: point.longitude }); + this.setData({ locationName: result.location_name }); + this.weatherLabel = result.weather + ? `${result.weather.description} ${result.weather.temperature}°C` + : "天气暂不可用"; + this.environmentAt = result.weather ? Date.now() : 0; + } catch (error) { + const denied = /denied|deny|auth/.test(error.errMsg || error.message || ""); + this.setData({ locationName: positioned ? "位置暂不可用" : denied ? "开启定位" : "定位暂不可用" }); + this.weatherLabel = denied ? "轻触开启定位" : "轻触重试"; + if (tapped && !denied) wx.showToast({ title: positioned ? "位置和天气获取失败,请重试" : "定位失败,请重试", icon: "none" }); + } finally { + this.environmentBusy = false; + this.updateEnvironmentLabel(); + } }, async load() { try { diff --git a/pets-fe/pages/home/home.wxml b/pets-fe/pages/home/home.wxml index 0ce3d6b..7aef008 100644 --- a/pets-fe/pages/home/home.wxml +++ b/pets-fe/pages/home/home.wxml @@ -1,4 +1,4 @@ - + diff --git a/pets-fe/pages/home/home.wxss b/pets-fe/pages/home/home.wxss index d4a4f80..366f3d8 100644 --- a/pets-fe/pages/home/home.wxss +++ b/pets-fe/pages/home/home.wxss @@ -1,12 +1,12 @@ .hero { min-height: 330rpx; - background: #194f42; + background: linear-gradient(135deg, #fff0dd, #ffdec7); border-radius: 38rpx; display: flex; align-items: center; gap: 12rpx; padding: 35rpx; - color: white; + color: #694c3f; } .hero-copy { display: flex; @@ -15,7 +15,7 @@ min-width: 0; } .hero-small { - color: #abd9b8; + color: #7c5b46; font-size: 23rpx; font-weight: 600; } @@ -26,11 +26,11 @@ } .hero-meta { font-size: 23rpx; - color: #d9eadd; + color: #7c5b46; } .hero-caption { font-size: 20rpx; - color: #b5e2be; + color: #7c5b46; margin-top: 36rpx; } .hero image { @@ -43,7 +43,7 @@ } .link { font-size: 24rpx; - color: #398668; + color: #b95738; font-weight: 600; } .walk-card { @@ -58,7 +58,7 @@ width: 145rpx; height: 145rpx; border-radius: 26rpx; - background: #dfeedd; + background: #fff0e2; display: grid; place-items: center; font-size: 72rpx; @@ -73,7 +73,7 @@ } .picker-label { font-size: 23rpx; - color: #7d9989; + color: #886c5f; margin: 27rpx 0 16rpx; } .start { @@ -95,11 +95,11 @@ background: #f7eadc; } .stats > view:nth-child(3) { - background: #e8efea; + background: #edf2fb; } .stats text { display: block; - color: #8da49a; + color: #886c5f; font-size: 22rpx; } .stats strong { @@ -109,7 +109,7 @@ } .stats small { font-size: 22rpx; - color: #94a69d; + color: #886c5f; } .tasks { padding: 12rpx 25rpx; @@ -119,7 +119,7 @@ align-items: center; gap: 17rpx; padding: 22rpx 0; - border-bottom: 1rpx solid #edf1ed; + border-bottom: 1rpx solid #ece2d8; } .task:last-child { border: 0; @@ -127,21 +127,21 @@ .check { width: 40rpx; height: 40rpx; - border: 3rpx solid #cfdfd2; + border: 3rpx solid #ece2d8; border-radius: 13rpx; text-align: center; line-height: 37rpx; color: white; } .check.done { - background: #3b9164; - border-color: #3b9164; + background: #b95738; + border-color: #b95738; } .task-icon { width: 65rpx; height: 65rpx; border-radius: 16rpx; - background: #f0f5ee; + background: #fff0e2; display: grid; place-items: center; font-size: 34rpx; diff --git a/pets-fe/pages/meetups/meetups.wxss b/pets-fe/pages/meetups/meetups.wxss index 9582951..068b1c2 100644 --- a/pets-fe/pages/meetups/meetups.wxss +++ b/pets-fe/pages/meetups/meetups.wxss @@ -1,5 +1,5 @@ .banner { - background: #dfeee2; + background: #edf2fb; border-radius: 34rpx; min-height: 245rpx; padding: 30rpx; @@ -16,7 +16,7 @@ display: block; } .banner-copy text { - color: #549375; + color: #576c88; font-size: 22rpx; } .banner-title { @@ -35,7 +35,7 @@ } .add { font-size: 24rpx; - color: #328965; + color: #b95738; font-weight: 600; } .meetup strong { @@ -44,30 +44,30 @@ margin: 15rpx 0; } .badge { - background: #e8f4ea; - color: #429369; + background: #edf2fb; + color: #b95738; border-radius: 99rpx; padding: 8rpx 15rpx; font-size: 22rpx; } .arrow { font-size: 39rpx; - color: #a1b2a8; + color: #9d8779; } .meta { font-size: 24rpx; - color: #748c7d; + color: #886c5f; margin-top: 9rpx; } .desc { font-size: 24rpx; - color: #9aaa9f; + color: #886c5f; margin-top: 15rpx; } .overlay { position: fixed; inset: 0; - background: #102a24a0; + background: #604a40a0; z-index: 30; display: flex; align-items: flex-end; @@ -86,7 +86,7 @@ } .detail { font-size: 25rpx; - color: #61816e; + color: #886c5f; line-height: 2; margin: 25rpx 0; } diff --git a/pets-fe/pages/pet-edit/pet-edit.wxml b/pets-fe/pages/pet-edit/pet-edit.wxml index e141f4d..35299e6 100644 --- a/pets-fe/pages/pet-edit/pet-edit.wxml +++ b/pets-fe/pages/pet-edit/pet-edit.wxml @@ -22,11 +22,11 @@ 性别 diff --git a/pets-fe/pages/pet-edit/pet-edit.wxss b/pets-fe/pages/pet-edit/pet-edit.wxss index 25aea1d..3c90e94 100644 --- a/pets-fe/pages/pet-edit/pet-edit.wxss +++ b/pets-fe/pages/pet-edit/pet-edit.wxss @@ -19,7 +19,7 @@ bottom: 0; left: 0; right: 0; - background: #194f42b5; + background: #b95738b5; color: white; text-align: center; font-size: 18rpx; diff --git a/pets-fe/pages/pets/pets.wxss b/pets-fe/pages/pets/pets.wxss index ede5c1a..b1ab14d 100644 --- a/pets-fe/pages/pets/pets.wxss +++ b/pets-fe/pages/pets/pets.wxss @@ -1,7 +1,7 @@ .family { - background: #194f42; + background: #f0e8f7; border-radius: 35rpx; - color: white; + color: #746086; min-height: 220rpx; padding: 35rpx; display: flex; @@ -17,7 +17,7 @@ display: block; } .family text { - color: #a4d6b8; + color: #765984; font-size: 23rpx; } .family strong { @@ -31,7 +31,7 @@ } .add { font-weight: 600; - color: #368764; + color: #b95738; } .pet { display: flex; @@ -64,11 +64,11 @@ } .pet-info text { font-size: 23rpx; - color: #8a9c90; + color: #886c5f; } .arrow { font-size: 42rpx; - color: #97a99d; + color: #886c5f; } .add-button { margin-top: 25rpx; diff --git a/pets-fe/pages/share/share.wxss b/pets-fe/pages/share/share.wxss index 4b9ec56..ecf46db 100644 --- a/pets-fe/pages/share/share.wxss +++ b/pets-fe/pages/share/share.wxss @@ -1,5 +1,5 @@ page { - background: #f6f9f6; + background: #fff9f3; } .card { margin-top: 20rpx; diff --git a/pets-fe/pages/tasks/tasks.wxml b/pets-fe/pages/tasks/tasks.wxml index 6a91c46..d92a094 100644 --- a/pets-fe/pages/tasks/tasks.wxml +++ b/pets-fe/pages/tasks/tasks.wxml @@ -78,14 +78,14 @@ 类型 @@ -93,7 +93,7 @@ 其他 @@ -112,7 +112,7 @@ 每天 @@ -120,7 +120,7 @@ 工作日 @@ -128,7 +128,7 @@ 周末 diff --git a/pets-fe/pages/tasks/tasks.wxss b/pets-fe/pages/tasks/tasks.wxss index 368e970..5dbdfd9 100644 --- a/pets-fe/pages/tasks/tasks.wxss +++ b/pets-fe/pages/tasks/tasks.wxss @@ -1,12 +1,12 @@ .summary { - background: #194f42; - color: white; + background: #e8f0fa; + color: #526986; border-radius: 34rpx; padding: 34rpx; } .summary > view:first-child { font-size: 24rpx; - color: #bbe3c4; + color: #576c88; } .summary strong { display: block; @@ -15,28 +15,28 @@ } .summary strong text { font-size: 30rpx; - color: #b4d8be; + color: #576c88; } .progress { height: 13rpx; - background: #3f7566; + background: #dbe4f2; border-radius: 99rpx; overflow: hidden; } .progress view { height: 100%; - background: #e9ad6a; + background: #e49b75; border-radius: 99rpx; } .add { - color: #2b8a63; + color: #b95738; font-size: 24rpx; font-weight: 600; } .date-nav { display: flex; justify-content: space-between; - color: #327c5e; + color: #b95738; font-size: 23rpx; margin: 0 0 22rpx; } @@ -48,7 +48,7 @@ align-items: center; gap: 15rpx; padding: 22rpx 0; - border-bottom: 1rpx solid #eef2ee; + border-bottom: 1rpx solid #ece2d8; } .task:last-child { border: 0; @@ -56,20 +56,20 @@ .check { width: 42rpx; height: 42rpx; - border: 3rpx solid #cbdcd1; + border: 3rpx solid #ece2d8; border-radius: 13rpx; text-align: center; line-height: 37rpx; color: white; } .check.done { - background: #389867; - border-color: #389867; + background: #b95738; + border-color: #b95738; } .icon { width: 65rpx; height: 65rpx; - background: #eff5ee; + background: #fff0e2; border-radius: 18rpx; display: grid; place-items: center; @@ -84,24 +84,24 @@ } .info text { font-size: 21rpx; - color: #92a498; + color: #886c5f; margin-top: 5rpx; } .remove { font-size: 36rpx; - color: #c4d0c7; + color: #9d8779; } .overlay { position: fixed; inset: 0; - background: #102a24a0; + background: #604a40a0; z-index: 30; display: flex; align-items: flex-end; } .sheet { width: 100%; - background: #f6f9f6; + background: #fff9f3; border-radius: 35rpx 35rpx 0 0; padding: 32rpx 32rpx calc(env(safe-area-inset-bottom) + 30rpx); max-height: 85vh; diff --git a/pets-fe/pages/walk-records/walk-records.js b/pets-fe/pages/walk-records/walk-records.js index a279356..01fc893 100644 --- a/pets-fe/pages/walk-records/walk-records.js +++ b/pets-fe/pages/walk-records/walk-records.js @@ -23,7 +23,7 @@ Page({ petNames: (walk.pet_ids || []).map((id) => names[id] || "已归档宠物").join("、"), polyline: walk.points && walk.points.length > 1 ? [{ points: walk.points.map((point) => ({ latitude: point.latitude, longitude: point.longitude })), - color: "#268e6a", + color: "#b95738", width: 5, }] : [], lastPoint: walk.points && walk.points.length ? walk.points[walk.points.length - 1] : null, diff --git a/pets-fe/pages/walk-records/walk-records.wxss b/pets-fe/pages/walk-records/walk-records.wxss index e70cd8e..3a32299 100644 --- a/pets-fe/pages/walk-records/walk-records.wxss +++ b/pets-fe/pages/walk-records/walk-records.wxss @@ -1,10 +1,10 @@ -.back { display: block; color: #327c5e; margin-bottom: 34rpx; font-size: 26rpx; } +.back { display: block; color: #b95738; margin-bottom: 34rpx; font-size: 26rpx; } .record strong, .detail strong { font-size: 28rpx; } -.record .row text { font-size: 38rpx; color: #8da89a; } -.pet-names, .detail > text { display: block; color: #547b65; margin-top: 16rpx; } -.record-stats { color: #91a59a; font-size: 24rpx; margin-top: 12rpx; } -.metrics { display: flex; gap: 28rpx; margin-top: 28rpx; border-top: 1rpx solid #e8efe9; padding-top: 22rpx; } +.record .row text { font-size: 38rpx; color: #886c5f; } +.pet-names, .detail > text { display: block; color: #886c5f; margin-top: 16rpx; } +.record-stats { color: #886c5f; font-size: 24rpx; margin-top: 12rpx; } +.metrics { display: flex; gap: 28rpx; margin-top: 28rpx; border-top: 1rpx solid #ece2d8; padding-top: 22rpx; } .metrics view { flex: 1; } .metrics strong, .metrics text { display: block; } -.metrics text { color: #8da89a; font-size: 22rpx; margin-top: 7rpx; } +.metrics text { color: #886c5f; font-size: 22rpx; margin-top: 7rpx; } .route-map { width: 100%; height: 600rpx; border-radius: 30rpx; overflow: hidden; } diff --git a/pets-fe/pages/walk/walk.js b/pets-fe/pages/walk/walk.js index e65d49b..836b57d 100644 --- a/pets-fe/pages/walk/walk.js +++ b/pets-fe/pages/walk/walk.js @@ -131,7 +131,7 @@ Page({ walk, points: pts, polyline: - pts.length > 1 ? [{ points: pts, color: "#268e6a", width: 5 }] : [], + pts.length > 1 ? [{ points: pts, color: "#b95738", width: 5 }] : [], latitude: last ? last.latitude : this.data.latitude, longitude: last ? last.longitude : this.data.longitude, distance: ((walk.distance_meters || 0) / 1000).toFixed(2), diff --git a/pets-fe/pages/walk/walk.wxml b/pets-fe/pages/walk/walk.wxml index 095bd8e..973421f 100644 --- a/pets-fe/pages/walk/walk.wxml +++ b/pets-fe/pages/walk/walk.wxml @@ -69,7 +69,7 @@ 分享实时位置 打开后可分享给朋友,结束遛狗会自动关闭 - +