diff --git a/pets-be/internal/database/seed.go b/pets-be/internal/database/seed.go index 521270d..dfb4824 100644 --- a/pets-be/internal/database/seed.go +++ b/pets-be/internal/database/seed.go @@ -8,11 +8,77 @@ import ( "github.com/sundynix/pets-be/internal/model" ) -// Seed 初始化种子数据:只建管理员账号。 -// 文章这类内容数据不放代码里——正文几百字塞进 Go 源文件,既看不清也 -// 没法在后台改完再同步回来。上线时直接同步数据库。 +// Seed 初始化种子数据:管理员账号 + 记录类型。 +// +// 文章、养护模板这类**内容**数据不放代码里——正文几百字塞进 Go 源文件,既看不清 +// 也没法在后台改完再同步回来。上线时直接同步数据库。 +// +// 记录类型是另一码事,它是**结构**:表空着的话记录页一个可记事项都没有, +// App 直接不能用。所以和管理员账号一样,只在表完全为空时铺一次底, +// 之后一切以数据库为准,代码里这份不再参与。 func Seed(db *gorm.DB, cfg *config.Config) error { - return seedAdmin(db, cfg) + if err := seedAdmin(db, cfg); err != nil { + return err + } + return seedRecordTypes(db) +} + +// seedRecordTypes 只在表为空时跑。已经有数据(哪怕只有一条)就完全不碰, +// 否则后台删掉的类型会在每次重启后自己长回来 +func seedRecordTypes(db *gorm.DB) error { + var count int64 + if err := db.Model(&model.RecordType{}).Count(&count).Error; err != nil { + return err + } + if count > 0 { + return nil + } + + // locked=true 的这 9 种在 record.go / insight.go / report.go / home.go 里有真实 + // 代码分支(体重回写档案、食便相关性、消费统计、疫苗完成度…),并且被 + // care_template_items.sheet_type 引用,后台不许删,只能停用。 + // form=full 表示 bottom-sheet 里有它专用的 wx:elif 分支。 + full := func(code, label, icon, group string, sort int) model.RecordType { + return model.RecordType{Code: code, Label: label, Icon: icon, GroupKey: group, + Sort: sort, Enabled: true, Form: model.RecordFormFull, Locked: true} + } + simple := func(code, label, icon, group string, sort int) model.RecordType { + return model.RecordType{Code: code, Label: label, Icon: icon, GroupKey: group, + Sort: sort, Enabled: true, Form: model.RecordFormSimple, Locked: false} + } + + d, h, c, cl := model.RecordGroupDaily, model.RecordGroupHealth, model.RecordGroupCare, model.RecordGroupClean + types := []model.RecordType{ + // 日常 + full("weight", "体重", "weight", d, 10), + full("poop", "排便", "poop", d, 20), + full("food", "饮食", "food", d, 30), + simple("water", "喝水", "water", d, 40), + full("cost", "记账", "cost", d, 50), + full("photo", "照片", "photo", d, 60), + // 健康 + full("symptom", "异常", "symptom", h, 10), + full("vaccine", "疫苗", "vaccine", h, 20), + full("deworm", "驱虫", "deworm", h, 30), + simple("checkup", "体检", "checkup", h, 40), + simple("vet", "看病", "vet", h, 50), + full("medicine", "给药", "medicine", h, 60), + simple("supplement", "保健品", "supplement", h, 70), + // 洗护 + simple("bath", "洗澡", "bath", c, 10), + simple("nail", "剪指甲", "nail", c, 20), + simple("ear", "洗耳朵", "ear", c, 30), + simple("tooth", "刷牙", "tooth", c, 40), + simple("brush", "梳毛", "brush", c, 50), + simple("groom", "美容", "groom", c, 60), + // 清洁 + simple("litter", "换猫砂", "litter", cl, 10), + simple("litterbox", "洗猫砂盆", "litterbox", cl, 20), + simple("bowl", "洗食盆", "bowl", cl, 30), + simple("waterbowl", "洗水盆", "waterbowl", cl, 40), + simple("clean", "消毒", "clean", cl, 50), + } + return db.Create(&types).Error } func seedAdmin(db *gorm.DB, cfg *config.Config) error { diff --git a/pets-be/internal/handler/admin.go b/pets-be/internal/handler/admin.go index 1104714..b8c56c2 100644 --- a/pets-be/internal/handler/admin.go +++ b/pets-be/internal/handler/admin.go @@ -280,6 +280,53 @@ func (h *Handler) AdminListPro(c *gin.Context) { // AdminCareTemplateMeta GET /api/admin/care-templates/meta // 给后台下拉用:物种、阶段、可选关联记录/弹层类型、提醒类型 +// ── 记录类型 ── + +type recordTypeReq struct { + ID string `json:"id"` + Code string `json:"code"` + Label string `json:"label"` + Icon string `json:"icon"` + Group string `json:"group"` + Sort int `json:"sort"` + Enabled bool `json:"enabled"` + Form string `json:"form"` +} + +// AdminListRecordTypes GET /api/admin/record-types 连停用的一起返回 +func (h *Handler) AdminListRecordTypes(c *gin.Context) { + response.OK(c, h.svc.AdminListRecordTypes()) +} + +// AdminSaveRecordType POST /api/admin/record-types 新增或更新 +func (h *Handler) AdminSaveRecordType(c *gin.Context) { + var req recordTypeReq + if err := c.ShouldBindJSON(&req); err != nil { + response.FailParams(c, err.Error()) + return + } + t := &model.RecordType{ + Code: req.Code, Label: req.Label, Icon: req.Icon, + GroupKey: req.Group, Sort: req.Sort, Enabled: req.Enabled, Form: req.Form, + } + t.ID = req.ID + if err := h.svc.SaveRecordType(t); err != nil { + respondErr(c, err) + return + } + response.OK(c, t) +} + +// AdminDeleteRecordType DELETE /api/admin/record-types/:id +// locked 的和已有记录的删不了,service 层硬拦 +func (h *Handler) AdminDeleteRecordType(c *gin.Context) { + if err := h.svc.DeleteRecordType(idParam(c, "id")); err != nil { + respondErr(c, err) + return + } + response.OK(c, nil) +} + func (h *Handler) AdminCareTemplateMeta(c *gin.Context) { response.OK(c, gin.H{ "species": []gin.H{ diff --git a/pets-be/internal/handler/record.go b/pets-be/internal/handler/record.go index d1b9703..ce7a51d 100644 --- a/pets-be/internal/handler/record.go +++ b/pets-be/internal/handler/record.go @@ -90,3 +90,9 @@ func (h *Handler) PetInsights(c *gin.Context) { } response.OK(c, list) } + +// RecordTypes GET /api/record-types 记录页可记事项,按分组归好。 +// 类型从表里来而不是前端写死,后台加一种不用发版 +func (h *Handler) RecordTypes(c *gin.Context) { + response.OK(c, h.svc.ListRecordTypes()) +} diff --git a/pets-be/internal/model/base.go b/pets-be/internal/model/base.go index 3d921d9..ebc7180 100644 --- a/pets-be/internal/model/base.go +++ b/pets-be/internal/model/base.go @@ -29,6 +29,7 @@ func AllModels() []any { &User{}, &Pet{}, &HealthRecord{}, + &RecordType{}, &DailyTask{}, &Plan{}, &PlanTask{}, diff --git a/pets-be/internal/model/record_type.go b/pets-be/internal/model/record_type.go new file mode 100644 index 0000000..147c977 --- /dev/null +++ b/pets-be/internal/model/record_type.go @@ -0,0 +1,47 @@ +package model + +// 记录类型。原来这 9 种是写在 health_record.go 里的常量,加一种就得发版; +// 挪到表里之后后台可以直接增删改。 +// +// 注意这里**没有**替换掉那 9 个常量:weight / poop / food / symptom / cost / +// vaccine 在 record.go、insight.go、report.go、home.go 里有真实的代码分支 +// (体重回写宠物档案、食便相关性分析、消费统计、疫苗完成度……),那些分支 +// 按具体常量过滤,不是 switch 全枚举。所以: +// - 表负责「展示什么、能记什么」 +// - 常量负责「哪几种参与分析」 +// 两者各管一段,新增类型不会碰到任何分析逻辑。 + +// 记录类型分组 +const ( + RecordGroupDaily = "daily" // 日常 + RecordGroupHealth = "health" // 健康 + RecordGroupCare = "care" // 洗护 + RecordGroupClean = "clean" // 清洁 +) + +// 弹层形态 +const ( + RecordFormFull = "full" // 有专用表单(原来那 9 种,各自一个 wx:elif 分支) + RecordFormSimple = "simple" // 通用简易表单:发生时间 + 备注 + 可选照片 +) + +// RecordType 一种可记录的事项 +type RecordType struct { + Base + // Code 就是小程序 bottom-sheet 的 type,也是 health_records.type 落库的值。 + // 建过记录之后改 Code 会让历史数据对不上,后台不允许改(只能删了重建)。 + Code string `gorm:"size:32;uniqueIndex" json:"code"` + Label string `gorm:"size:32" json:"label"` + Icon string `gorm:"size:32" json:"icon"` // pt-icon 名,见 styles/iconfont.wxss + + // GroupKey 存成 group_key 而不是 group:group 是 MySQL 保留字, + // GORM 虽然会加反引号,但手写 SQL 排查时很容易踩 + GroupKey string `gorm:"column:group_key;size:16;index" json:"group"` + Sort int `json:"sort"` + Enabled bool `gorm:"default:true" json:"enabled"` + Form string `gorm:"size:16" json:"form"` + + // Locked 有代码分支或模板引用依赖它,后台不许删。 + // 硬拦在 service 层,不是靠前端隐藏按钮。 + Locked bool `json:"locked"` +} diff --git a/pets-be/internal/router/router.go b/pets-be/internal/router/router.go index 54d74ef..0698cb2 100644 --- a/pets-be/internal/router/router.go +++ b/pets-be/internal/router/router.go @@ -57,6 +57,7 @@ func registerUserAPI(api *gin.RouterGroup, h *handler.Handler, jm *appjwt.Manage g.PUT("/pets/:id", h.UpdatePet) g.DELETE("/pets/:id", h.DeletePet) + g.GET("/record-types", h.RecordTypes) g.GET("/pets/:id/records", h.ListRecords) g.POST("/pets/:id/records", h.CreateRecord) g.GET("/pets/:id/records/weight-trend", h.WeightTrend) @@ -147,6 +148,10 @@ func registerAdminAPI(api *gin.RouterGroup, h *handler.Handler, jm *appjwt.Manag g.POST("/articles", h.AdminSaveArticle) g.DELETE("/articles/:id", h.AdminDeleteArticle) + g.GET("/record-types", h.AdminListRecordTypes) + g.POST("/record-types", h.AdminSaveRecordType) + g.DELETE("/record-types/:id", h.AdminDeleteRecordType) + g.GET("/care-templates/meta", h.AdminCareTemplateMeta) g.GET("/care-templates", h.AdminGetCareTemplate) g.PUT("/care-templates", h.AdminSaveCareTemplate) diff --git a/pets-be/internal/service/record_type.go b/pets-be/internal/service/record_type.go new file mode 100644 index 0000000..494e515 --- /dev/null +++ b/pets-be/internal/service/record_type.go @@ -0,0 +1,127 @@ +package service + +import ( + "errors" + "log" + "strconv" + + "gorm.io/gorm" + + "github.com/sundynix/pets-be/internal/model" +) + +// 分组展示顺序。前端按这个顺序渲染,不靠 group_key 的字典序 +var recordGroupOrder = []struct { + Key string + Label string +}{ + {model.RecordGroupDaily, "日常记录"}, + {model.RecordGroupHealth, "健康记录"}, + {model.RecordGroupCare, "洗护记录"}, + {model.RecordGroupClean, "清洁记录"}, +} + +// RecordTypeGroup 一组类型,给小程序记录页直接渲染 +type RecordTypeGroup struct { + Key string `json:"key"` + Label string `json:"label"` + Items []model.RecordType `json:"items"` +} + +// ListRecordTypes 小程序用:只返回启用的,按分组归好 +func (s *Service) ListRecordTypes() []RecordTypeGroup { + var all []model.RecordType + s.db.Where("enabled = ?", true).Order("sort asc, id asc").Find(&all) + if len(all) == 0 { + // 表被清空了。这里不硬编码兜底一份——那等于把「可配置」又变回代码里的常量, + // 下次改配置的人会发现改了没用。宁可返回空让记录页明显是空的,也别静默用旧值。 + log.Println("[warn] record_types 表为空,记录页会没有任何可记事项") + return []RecordTypeGroup{} + } + byGroup := map[string][]model.RecordType{} + for _, t := range all { + byGroup[t.GroupKey] = append(byGroup[t.GroupKey], t) + } + out := make([]RecordTypeGroup, 0, len(recordGroupOrder)) + for _, g := range recordGroupOrder { + items := byGroup[g.Key] + if len(items) == 0 { + continue + } + out = append(out, RecordTypeGroup{Key: g.Key, Label: g.Label, Items: items}) + } + return out +} + +// AdminListRecordTypes 后台用:连禁用的一起返回,按分组顺序再按 sort +func (s *Service) AdminListRecordTypes() []model.RecordType { + var all []model.RecordType + s.db.Order("group_key asc, sort asc, id asc").Find(&all) + if all == nil { + return []model.RecordType{} + } + return all +} + +// SaveRecordType 新增或更新。Code 一旦建过记录就不允许改—— +// 改了历史记录的 type 会对不上任何类型,列表里直接变成一行没图标没名字的东西 +func (s *Service) SaveRecordType(in *model.RecordType) error { + if in.Code == "" || in.Label == "" { + return ErrInvalidParam + } + if in.Form != model.RecordFormFull && in.Form != model.RecordFormSimple { + in.Form = model.RecordFormSimple + } + if in.GroupKey == "" { + in.GroupKey = model.RecordGroupDaily + } + + if in.ID == "" { + // 新增:Code 不能和已有的撞 + var n int64 + s.db.Model(&model.RecordType{}).Where("code = ?", in.Code).Count(&n) + if n > 0 { + return errors.New("这个 code 已经存在了") + } + in.Locked = false // 新加的一律可删;locked 只由 bootstrap 给那 9 种 + return s.db.Create(in).Error + } + + var old model.RecordType + if err := s.db.First(&old, "id = ?", in.ID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrNotFound + } + return err + } + if old.Code != in.Code { + var used int64 + s.db.Model(&model.HealthRecord{}).Where("type = ?", old.Code).Count(&used) + if used > 0 { + return errors.New("已经有 " + strconv.FormatInt(used, 10) + " 条记录用了这个 code,不能改,只能新建一个") + } + } + // locked 和 form 不由后台改:前者是代码依赖,后者对应弹层里真实存在的分支 + return s.db.Model(&model.RecordType{}).Where("id = ?", in.ID).Updates(map[string]any{ + "code": in.Code, "label": in.Label, "icon": in.Icon, + "group_key": in.GroupKey, "sort": in.Sort, "enabled": in.Enabled, + }).Error +} + +// DeleteRecordType locked 的删不了;已经有记录的也删不了(会让历史数据变孤儿)。 +// 想让它从记录页消失应该用 enabled=false,历史记录还能正常显示 +func (s *Service) DeleteRecordType(id string) error { + var t model.RecordType + if err := s.db.First(&t, "id = ?", id).Error; err != nil { + return ErrNotFound + } + if t.Locked { + return errors.New("「" + t.Label + "」有统计和分析逻辑依赖它,不能删。想隐藏请改成停用") + } + var used int64 + s.db.Model(&model.HealthRecord{}).Where("type = ?", t.Code).Count(&used) + if used > 0 { + return errors.New("已经有 " + strconv.FormatInt(used, 10) + " 条「" + t.Label + "」记录,删了这些记录会变成孤儿。想隐藏请改成停用") + } + return s.db.Delete(&model.RecordType{}, "id = ?", id).Error +} diff --git a/pets-fe/adm.txt b/pets-fe/adm.txt new file mode 100644 index 0000000..0a08bb0 --- /dev/null +++ b/pets-fe/adm.txt @@ -0,0 +1 @@ +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjM0MTAyNzAxOTEwMzkzMjQxNiIsImtpbmQiOiJ1c2VyIiwibmFtZSI6IuiCieWeq-eIseWlveiAhTA0NTE3IiwiaXNzIjoicGV0cy1iZSIsImV4cCI6MTc4NTM4MTU3NywiaWF0IjoxNzg1Mzc0Mzc3fQ.iDKxUsk3ICp4JlrqiDvBbj_1NnHV687DD2zNUuRb0z8 \ No newline at end of file diff --git a/pets-fe/preview-v3.html b/pets-fe/preview-v3.html new file mode 100644 index 0000000..ef9d45f --- /dev/null +++ b/pets-fe/preview-v3.html @@ -0,0 +1,414 @@ + + + +肉垫计划 · 视觉稿 v3 + + +

肉垫计划 · 视觉稿 v3

+

+用的是 app.wxss 的真 token 值和真图标资源(Tabler mask,含本次新增的 15 个),不是另画一套。 +rpx 按 2:1 折成 px,所以这里的尺寸就是小程序里的尺寸。
+要定的只有第 ① 项:门面色块和浅底内容的接缝怎么处理。 +

+ +

① 接缝方案 —— 三选一

+

+关键约束(跟规划里写的不一样,实际读代码才发现):nav-barposition:fixed, +背景是半透明奶白 + backdrop-filter,底边还有一层 mask 淡出——当初就是为了别和页面渐变留一道硬接缝。
+所以品牌色块要盖到最顶部,导航栏必须参与:得给 nav-bar 加一个 tone 属性, +在色块上时不铺奶白、文字转深棕。A / B 需要这个改动,C 不需要。 +

+
+
+
+ +
钞票奶盖
+
+
+
🐶
+
钞票
+
柯基2岁7个月射手座
+
+
+
我们一起生活的第 862
+
+
+
+

A · 色块只到宠物切换条
门面卡压在接缝上,一半在色块里。

+
+
+
+ +
钞票奶盖
+
+
+
🐶
+
钞票
+
柯基2岁7个月射手座
+
+
+
我们一起生活的第 862
+
+
+
+

B · 色块整个包住门面卡
最接近竞品。白卡完整浮在黄底上。

+
+
+
+ +
钞票奶盖
+
+
+
🐶
+
钞票
+
柯基2岁7个月射手座
+
+
+
我们一起生活的第 862
+
+
+
+

C · 色块渐隐,没有硬边界
不动 nav-bar。改动最小,也最不像竞品。

+
+
+ +

② 记录页:24 种 4 组 新增 15 种

+

+四组各一个色系,全部走现成的 --primary / green / purple / bluesoft/ink 二件套,没有新色值。 +竞品看着「彩色」的真正原因是浅色圆底 + 分组配色,不是插画;图标仍是线性 Tabler mask,零美术成本。 +

+
+
+
+ +
日常6
体重
+
排便
+
饮食
+
喝水
+
记账
+
照片
+
健康7
异常
+
疫苗
+
驱虫
+
体检
+
看病
+
给药
+
保健品
+
洗护6
洗澡
+
剪指甲
+
洗耳朵
+
刷牙
+
梳毛
+
美容
+
清洁5
换猫砂
+
洗猫砂盆
+
洗食盆
+
洗水盆
+
消毒
+
+

新:24 种 4 组
一屏看到日常+健康,滚一下到洗护/清洁。

+
+
+
+ +
体重
+
便便
+
饮食
+
异常
+
消费
+
用药
+
照片
+
疫苗
+
驱虫
+
+

现状:9 种一锅
全是低频医疗和记账。今天打开大概率没什么可记。

+
+
+
+
新增的 15 个图标
+
喝水
+
洗澡
+
剪指甲
+
洗耳朵
+
刷牙
+
梳毛
+
美容
+
换猫砂
+
洗猫砂盆
+
洗食盆
+
洗水盆
+
消毒
+
体检
+
看病
+
保健品
+

+ 从本地那份 tabler-icons-3.46.0 里挑的真源,不是我凭记忆画路径。 + 已解码抽检确认路径非空——Tabler 是 stroke 描边,这套方案唯一的坑就是空字形, + 转字体必踩,走 mask 不会。

+
+
+
+ +

③ 身份卡

+

+绘制复用现有海报那套 canvas 2d(bottom-sheet.js 270-380 行),换的是内容不是功能。
+唯一新增后端工作:小程序码要调微信 wxacode.getUnlimited,AppID/Secret 配置里已有、接口还没写。 +身份 ID 用「品种码 + 生日 + 序号」本地拼,不入库。 +

+
+
+
+ +
+
+
+
+
🐶
+
+
姓名钞票
+
性别  品种柯基
+
出生2023 年 12 月 15 日
+
介绍汪星人来地球啦~
+
+
+
身份 ID1116 9832 0231 2150
+
+
+
+
宠星人地球移民
+
居民身份卡
+
签发机构 肉垫计划
有效期限 2023.12.15 - 永远
+
+
小程序码
(待接)
+
+
+
存相册
+
分享
+
+
+
+
+

身份卡
价值全在「可炫耀、可分享」,不在信息量。

+
+
+ + diff --git a/pets-fe/styles/iconfont.wxss b/pets-fe/styles/iconfont.wxss index 100f691..0e08a25 100644 --- a/pets-fe/styles/iconfont.wxss +++ b/pets-fe/styles/iconfont.wxss @@ -42,3 +42,18 @@ .pt-i-clock{--m:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgPjxwYXRoIGQ9Ik0zIDEyYTkgOSAwIDEgMCAxOCAwYTkgOSAwIDAgMCAtMTggMCIgLz48cGF0aCBkPSJNMTIgN3Y1bDMgMyIgLz48L3N2Zz4=")} .pt-i-up{--m:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgPjxwYXRoIGQ9Ik02IDE1bDYgLTZsNiA2IiAvPjwvc3ZnPg==")} .pt-i-down{--m:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgPjxwYXRoIGQ9Ik02IDlsNiA2bDYgLTYiIC8+PC9zdmc+")} +.pt-i-water{--m:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgPjxwYXRoIGQ9Ik03LjUwMiAxOS40MjNjMi42MDIgMi4xMDUgNi4zOTUgMi4xMDUgOC45OTYgMGMyLjYwMiAtMi4xMDUgMy4yNjIgLTUuNzA4IDEuNTY2IC04LjU0NmwtNC44OSAtNy4yNmMtLjQyIC0uNjI1IC0xLjI4NyAtLjgwMyAtMS45MzYgLS4zOTdhMS4zNzYgMS4zNzYgMCAwIDAgLS40MSAuMzk3bC00Ljg5MyA3LjI2Yy0xLjY5NSAyLjgzOCAtMS4wMzUgNi40NDEgMS41NjcgOC41NDYiIC8+PC9zdmc+")} +.pt-i-bath{--m:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgPjxwYXRoIGQ9Ik00IDEyaDE2YTEgMSAwIDAgMSAxIDF2M2E0IDQgMCAwIDEgLTQgNGgtMTBhNCA0IDAgMCAxIC00IC00di0zYTEgMSAwIDAgMSAxIC0xIiAvPiA8cGF0aCBkPSJNNiAxMnYtN2EyIDIgMCAwIDEgMiAtMmgzdjIuMjUiIC8+IDxwYXRoIGQ9Ik00IDIxbDEgLTEuNSIgLz4gPHBhdGggZD0iTTIwIDIxbC0xIC0xLjUiIC8+PC9zdmc+")} +.pt-i-nail{--m:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgPjxwYXRoIGQ9Ik0zIDdhMyAzIDAgMSAwIDYgMGEzIDMgMCAxIDAgLTYgMCIgLz4gPHBhdGggZD0iTTMgMTdhMyAzIDAgMSAwIDYgMGEzIDMgMCAxIDAgLTYgMCIgLz4gPHBhdGggZD0iTTguNiA4LjZsMTAuNCAxMC40IiAvPiA8cGF0aCBkPSJNOC42IDE1LjRsMTAuNCAtMTAuNCIgLz48L3N2Zz4=")} +.pt-i-ear{--m:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgPjxwYXRoIGQ9Ik02IDEwYTcgNyAwIDEgMSAxMyAzLjZhMTAgMTAgMCAwIDEgLTIgMmE4IDggMCAwIDAgLTIgM2E0LjUgNC41IDAgMCAxIC02LjggMS40IiAvPiA8cGF0aCBkPSJNMTAgMTBhMyAzIDAgMSAxIDUgMi4yIiAvPjwvc3ZnPg==")} +.pt-i-tooth{--m:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgPjxwYXRoIGQ9Ik0xMiA1LjVjLTEuMDc0IC0uNTg2IC0yLjU4MyAtMS41IC00IC0xLjVjLTIuMSAwIC00IDEuMjQ3IC00IDVjMCA0Ljg5OSAxLjA1NiA4LjQxIDIuNjcxIDEwLjUzN2MuNTczIC43NTYgMS45NyAuNTIxIDIuNTY3IC0uMjM2Yy4zOTggLS41MDUgLjgxOSAtMS40MzkgMS4yNjIgLTIuODAxYy4yOTIgLS43NzEgLjg5MiAtMS41MDQgMS41IC0xLjVjLjYwMiAwIDEuMjEgLjczNyAxLjUgMS41Yy40NDMgMS4zNjIgLjg2NCAyLjI5NSAxLjI2MiAyLjhjLjU5NyAuNzU5IDIgLjk5MyAyLjU2NyAuMjM3YzEuNjE1IC0yLjEyNyAyLjY3MSAtNS42MzcgMi42NzEgLTEwLjUzN2MwIC0zLjc0IC0xLjkwOCAtNSAtNCAtNWMtMS40MjMgMCAtMi45MiAuOTExIC00IDEuNSIgLz4gPHBhdGggZD0iTTEyIDUuNWwzIDEuNSIgLz48L3N2Zz4=")} +.pt-i-brush{--m:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgPjxwYXRoIGQ9Ik0zIDIxdi00YTQgNCAwIDEgMSA0IDRoLTQiIC8+IDxwYXRoIGQ9Ik0yMSAzYTE2IDE2IDAgMCAwIC0xMi44IDEwLjIiIC8+IDxwYXRoIGQ9Ik0yMSAzYTE2IDE2IDAgMCAxIC0xMC4yIDEyLjgiIC8+IDxwYXRoIGQ9Ik0xMC42IDlhOSA5IDAgMCAxIDQuNCA0LjQiIC8+PC9zdmc+")} +.pt-i-groom{--m:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgPjxwYXRoIGQ9Ik0xNiAxOGEyIDIgMCAwIDEgMiAyYTIgMiAwIDAgMSAyIC0yYTIgMiAwIDAgMSAtMiAtMmEyIDIgMCAwIDEgLTIgMm0wIC0xMmEyIDIgMCAwIDEgMiAyYTIgMiAwIDAgMSAyIC0yYTIgMiAwIDAgMSAtMiAtMmEyIDIgMCAwIDEgLTIgMm0tNyAxMmE2IDYgMCAwIDEgNiAtNmE2IDYgMCAwIDEgLTYgLTZhNiA2IDAgMCAxIC02IDZhNiA2IDAgMCAxIDYgNiIgLz48L3N2Zz4=")} +.pt-i-litter{--m:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgPjxwYXRoIGQ9Ik00IDdhOCA0IDAgMSAwIDE2IDBhOCA0IDAgMSAwIC0xNiAwIiAvPiA8cGF0aCBkPSJNNCA3YzAgLjY2NCAuMDg4IDEuMzI0IC4yNjMgMS45NjVsMi43MzcgMTAuMDM1Yy41IDEuNSAyLjIzOSAyIDUgMnM0LjUgLS41IDUgLTJjLjMzMyAtMSAxLjI0NiAtNC4zNDUgMi43MzcgLTEwLjAzNWE3LjQ1IDcuNDUgMCAwIDAgLjI2MyAtMS45NjUiIC8+PC9zdmc+")} +.pt-i-litterbox{--m:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgPjxwYXRoIGQ9Ik01IDE2bDEuNDY1IDEuNjM4YTIgMiAwIDEgMSAtMy4wMTUgLjA5OWwxLjU1IC0xLjczNyIgLz4gPHBhdGggZD0iTTEzLjczNyA5LjczN2MyLjI5OSAtMi4zIDMuMjMgLTUuMDk1IDIuMDgxIC02LjI0NWMtMS4xNSAtMS4xNSAtMy45NDUgLS4yMTcgLTYuMjQ0IDIuMDgyYy0yLjMgMi4yOTkgLTMuMjMxIDUuMDk1IC0yLjA4MiA2LjI0NGMxLjE1IDEuMTUgMy45NDYgLjIxOCA2LjI0NSAtMi4wODEiIC8+IDxwYXRoIGQ9Ik03LjQ5MiAxMS44MThjLjM2MiAuMzYyIC43NjggLjY3NiAxLjIwOCAuOTM0bDYuODk1IDQuMDQ3YzEuMDc4IC41NTcgMi4yNTUgLS4wNzUgMy42OTIgLTEuNTEyYzEuNDM3IC0xLjQzNyAyLjA3IC0yLjYxNCAxLjUxMiAtMy42OTJjLS4zNzIgLS43MTggLTEuNzIgLTMuMDE3IC00LjA0NyAtNi44OTVhNi4wMTUgNi4wMTUgMCAwIDAgLS45MzQgLTEuMjA4IiAvPjwvc3ZnPg==")} +.pt-i-bowl{--m:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgPjxwYXRoIGQ9Ik00IDhoMTZhMSAxIDAgMCAxIDEgMXYuNWMwIDEuNSAtMi41MTcgNS41NzMgLTQgNi41djFhMSAxIDAgMCAxIC0xIDFoLThhMSAxIDAgMCAxIC0xIC0xdi0xYy0xLjY4NyAtMS4wNTQgLTQgLTUgLTQgLTYuNXYtLjVhMSAxIDAgMCAxIDEgLTEiIC8+PC9zdmc+")} +.pt-i-waterbowl{--m:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgPjxwYXRoIGQ9Ik04IDIxbDggMCIgLz4gPHBhdGggZD0iTTEyIDE1bDAgNiIgLz4gPHBhdGggZD0iTTE3IDNsMSA3YzAgMy4wMTIgLTIuNjg2IDUgLTYgNXMtNiAtMS45ODggLTYgLTVsMSAtN2gxMCIgLz4gPHBhdGggZD0iTTYgMTBhNSA1IDAgMCAxIDYgMGE1IDUgMCAwIDAgNiAwIiAvPjwvc3ZnPg==")} +.pt-i-clean{--m:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgPjxwYXRoIGQ9Ik00IDEyYTIgMiAwIDAgMSAyIC0yaDRhMiAyIDAgMCAxIDIgMnY3YTIgMiAwIDAgMSAtMiAyaC00YTIgMiAwIDAgMSAtMiAtMmwwIC03IiAvPiA8cGF0aCBkPSJNNiAxMHYtNGExIDEgMCAwIDEgMSAtMWgyYTEgMSAwIDAgMSAxIDF2NCIgLz4gPHBhdGggZD0iTTE1IDdoLjAxIiAvPiA8cGF0aCBkPSJNMTggOWguMDEiIC8+IDxwYXRoIGQ9Ik0xOCA1aC4wMSIgLz4gPHBhdGggZD0iTTIxIDNoLjAxIiAvPiA8cGF0aCBkPSJNMjEgN2guMDEiIC8+IDxwYXRoIGQ9Ik0yMSAxMWguMDEiIC8+IDxwYXRoIGQ9Ik0xMCA3aDEiIC8+PC9zdmc+")} +.pt-i-checkup{--m:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgPjxwYXRoIGQ9Ik02IDRoLTFhMiAyIDAgMCAwIC0yIDJ2My41YTUuNSA1LjUgMCAwIDAgMTEgMHYtMy41YTIgMiAwIDAgMCAtMiAtMmgtMSIgLz4gPHBhdGggZD0iTTggMTVhNiA2IDAgMSAwIDEyIDB2LTMiIC8+IDxwYXRoIGQ9Ik0xMSAzdjIiIC8+IDxwYXRoIGQ9Ik02IDN2MiIgLz4gPHBhdGggZD0iTTE4IDEwYTIgMiAwIDEgMCA0IDBhMiAyIDAgMSAwIC00IDAiIC8+PC9zdmc+")} +.pt-i-vet{--m:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgPjxwYXRoIGQ9Ik04IDh2LTJhMiAyIDAgMCAxIDIgLTJoNGEyIDIgMCAwIDEgMiAydjIiIC8+IDxwYXRoIGQ9Ik00IDEwYTIgMiAwIDAgMSAyIC0yaDEyYTIgMiAwIDAgMSAyIDJ2OGEyIDIgMCAwIDEgLTIgMmgtMTJhMiAyIDAgMCAxIC0yIC0ybDAgLTgiIC8+IDxwYXRoIGQ9Ik0xMCAxNGg0IiAvPiA8cGF0aCBkPSJNMTIgMTJ2NCIgLz48L3N2Zz4=")} +.pt-i-supplement{--m:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgPjxwYXRoIGQ9Ik00LjUgMTIuNWw4IC04YTQuOTQgNC45NCAwIDAgMSA3IDdsLTggOGE0Ljk0IDQuOTQgMCAwIDEgLTcgLTciIC8+IDxwYXRoIGQ9Ik04LjUgOC41bDcgNyIgLz48L3N2Zz4=")}