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}}
+ {{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 @@
分享实时位置
打开后可分享给朋友,结束遛狗会自动关闭
-
+