feat: require couple binding and refine miniapp settings
Gate record features and storage entry points with invitation guidance; enforce record access in the backend. Add homepage greetings, right-swipe profile dismissal, and move the smoking switch to settings. Simplify API base configuration and remove the unused legacy tab bar component.
This commit is contained in:
+1
-1
@@ -23,7 +23,7 @@
|
|||||||
本地配置包含 AppSecret,已加入 Git 忽略;新环境可复制 `config.dev.example.yaml` 后填写。
|
本地配置包含 AppSecret,已加入 Git 忽略;新环境可复制 `config.dev.example.yaml` 后填写。
|
||||||
管理端地址:`http://localhost:8080/backend/admin/`。
|
管理端地址:`http://localhost:8080/backend/admin/`。
|
||||||
|
|
||||||
小程序 `weapp/config/env.js` 根据微信运行版本选择地址:开发版为 `http://127.0.0.1:8080/api`,体验版/正式版为线上地址。
|
小程序在 `weapp/config/env.js` 中手动配置 `API_BASE`。本地调试填写本机或局域网地址;上传体验版、正式版前改为 `https://we.sundynix.cn/api`。
|
||||||
开发工具需关闭合法域名校验(项目已设置);真机调试将 dev 地址改为电脑的局域网 IP。
|
开发工具需关闭合法域名校验(项目已设置);真机调试将 dev 地址改为电脑的局域网 IP。
|
||||||
小程序 AppID 为 `wxb463820bf36dd5d6`,AppSecret 仅存放在后端本地配置。
|
小程序 AppID 为 `wxb463820bf36dd5d6`,AppSecret 仅存放在后端本地配置。
|
||||||
dev 继续使用当前 `past-go` 数据库与 MinIO bucket,并非独立测试数据。
|
dev 继续使用当前 `past-go` 数据库与 MinIO bucket,并非独立测试数据。
|
||||||
|
|||||||
@@ -284,35 +284,12 @@ func runPostgresAPI(t *testing.T, stringsMode bool) {
|
|||||||
if err := db.ORM.Model(&model.User{}).Where("id=?", aID).Update("storage_quota_mb", 0).Error; err != nil {
|
if err := db.ORM.Model(&model.User{}).Where("id=?", aID).Update("storage_quota_mb", 0).Error; err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
personalDiary := id(post("diary_add", aToken, M{"content": "Personal diary", "image_file_ids": []string{personalImage}}), "id")
|
// Every record endpoint rejects an authenticated but unbound account.
|
||||||
get("diary_detail", aToken, M{"id": personalDiary})
|
for _, action := range []string{"diary_list", "diary_add", "timeline_add", "album_add", "photos", "photo_add", "photo_upload", "set_album_cover", "songs_add", "checkin_add", "smoking_card_visible", "period_daily_save", "pet_adopt"} {
|
||||||
request("diary_detail", "GET", otherToken, M{"id": personalDiary}, 404)
|
denied := request(action, "POST", aToken, M{}, 403)
|
||||||
personalMemory := id(post("timeline_add", aToken, M{"title": "Personal memory", "event_date": today(), "bg_image_file_id": personalImage}), "id")
|
if store.ToInt(denied["code"]) != 10002 {
|
||||||
get("timeline_detail", aToken, M{"id": personalMemory})
|
t.Fatalf("missing binding code for %s: %v", action, denied)
|
||||||
request("timeline_detail", "GET", otherToken, M{"id": personalMemory}, 404)
|
|
||||||
// Personal albums work before binding and remain scoped to the creator.
|
|
||||||
personalAlbum := id(post("album_add", aToken, M{"title": "Personal album"}), "id")
|
|
||||||
personalFile := id(uploadFile("upload", aToken, "file", nil), "file_id")
|
|
||||||
personalPhoto := id(post("photo_add", aToken, M{"album_id": personalAlbum, "file_id": personalFile}), "id")
|
|
||||||
uploadFile("set_album_cover", aToken, "cover", M{"album_id": personalAlbum})
|
|
||||||
uploadFile("photo_upload", aToken, "photos", M{"album_id": personalAlbum})
|
|
||||||
post("photo_update", aToken, M{"id": personalPhoto, "taken_at": today(), "location": "Personal place", "activity": "Personal photo"})
|
|
||||||
post("album_update", aToken, M{"id": personalAlbum, "title": "Personal edited"})
|
|
||||||
if got := get("photos", aToken, M{"album_id": personalAlbum, "page": 1, "page_size": 20}); store.ToInt(got["total"]) != 2 {
|
|
||||||
t.Fatalf("personal photos missing: %v", got)
|
|
||||||
}
|
}
|
||||||
if got := get("photos", otherToken, M{"album_id": personalAlbum, "page": 1, "page_size": 20}); store.ToInt(got["total"]) != 0 {
|
|
||||||
t.Fatalf("personal photos leaked: %v", got)
|
|
||||||
}
|
|
||||||
if got := get("album_list", otherToken, M{"page": 1, "page_size": 20}); store.ToInt(got["total"]) != 0 {
|
|
||||||
t.Fatalf("personal albums leaked: %v", got)
|
|
||||||
}
|
|
||||||
request("photo_add", "POST", otherToken, M{"album_id": personalAlbum, "file_id": personalFile}, 404)
|
|
||||||
request("photo_update", "POST", otherToken, M{"id": personalPhoto, "taken_at": "", "location": "", "activity": ""}, 404)
|
|
||||||
request("album_delete", "POST", otherToken, M{"id": personalAlbum}, 404)
|
|
||||||
var personal model.Album
|
|
||||||
if err := db.ORM.First(&personal, "id=?", personalAlbum).Error; err != nil || personal.CoupleId != nil {
|
|
||||||
t.Fatalf("personal album relation: %+v %v", personal, err)
|
|
||||||
}
|
}
|
||||||
link := post("generate_invite_link", aToken, nil)
|
link := post("generate_invite_link", aToken, nil)
|
||||||
_ = link
|
_ = link
|
||||||
@@ -326,8 +303,34 @@ func runPostgresAPI(t *testing.T, stringsMode bool) {
|
|||||||
t.Fatalf("binding mismatch: %+v %v", user, err)
|
t.Fatalf("binding mismatch: %+v %v", user, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
personalDiary := id(post("diary_add", aToken, M{"content": "Personal diary", "image_file_ids": []string{personalImage}}), "id")
|
||||||
|
get("diary_detail", aToken, M{"id": personalDiary})
|
||||||
|
request("diary_detail", "GET", otherToken, M{"id": personalDiary}, 403)
|
||||||
|
personalMemory := id(post("timeline_add", aToken, M{"title": "Personal memory", "event_date": today(), "bg_image_file_id": personalImage}), "id")
|
||||||
|
get("timeline_detail", aToken, M{"id": personalMemory})
|
||||||
|
request("timeline_detail", "GET", otherToken, M{"id": personalMemory}, 403)
|
||||||
|
// Bound users can use albums and all associated file operations.
|
||||||
|
personalAlbum := id(post("album_add", aToken, M{"title": "Personal album"}), "id")
|
||||||
|
personalFile := id(uploadFile("upload", aToken, "file", nil), "file_id")
|
||||||
|
personalPhoto := id(post("photo_add", aToken, M{"album_id": personalAlbum, "file_id": personalFile}), "id")
|
||||||
|
uploadFile("set_album_cover", aToken, "cover", M{"album_id": personalAlbum})
|
||||||
|
uploadFile("photo_upload", aToken, "photos", M{"album_id": personalAlbum})
|
||||||
|
post("photo_update", aToken, M{"id": personalPhoto, "taken_at": today(), "location": "Personal place", "activity": "Personal photo"})
|
||||||
|
post("album_update", aToken, M{"id": personalAlbum, "title": "Personal edited"})
|
||||||
|
if got := get("photos", aToken, M{"album_id": personalAlbum, "page": 1, "page_size": 20}); store.ToInt(got["total"]) != 2 {
|
||||||
|
t.Fatalf("personal photos missing: %v", got)
|
||||||
|
}
|
||||||
|
request("photos", "GET", otherToken, M{"album_id": personalAlbum}, 403)
|
||||||
|
request("album_list", "GET", otherToken, nil, 403)
|
||||||
|
request("photo_add", "POST", otherToken, M{"album_id": personalAlbum, "file_id": personalFile}, 403)
|
||||||
|
request("photo_update", "POST", otherToken, M{"id": personalPhoto}, 403)
|
||||||
|
request("album_delete", "POST", otherToken, M{"id": personalAlbum}, 403)
|
||||||
|
var personal model.Album
|
||||||
|
if err := db.ORM.First(&personal, "id=?", personalAlbum).Error; err != nil || personal.CoupleId == nil || *personal.CoupleId != cid {
|
||||||
|
t.Fatalf("personal album relation: %+v %v", personal, err)
|
||||||
|
}
|
||||||
if got := get("photos", bToken, M{"album_id": personalAlbum, "page": 1, "page_size": 20}); store.ToInt(got["total"]) != 2 {
|
if got := get("photos", bToken, M{"album_id": personalAlbum, "page": 1, "page_size": 20}); store.ToInt(got["total"]) != 2 {
|
||||||
t.Fatalf("partner cannot view pre-binding photos: %v", got)
|
t.Fatalf("partner cannot view shared photos: %v", got)
|
||||||
}
|
}
|
||||||
post("photo_update", bToken, M{"id": personalPhoto, "taken_at": today(), "location": "Shared now", "activity": "Partner edit"})
|
post("photo_update", bToken, M{"id": personalPhoto, "taken_at": today(), "location": "Shared now", "activity": "Partner edit"})
|
||||||
request("account_deletion_preview", "POST", aToken, nil, 409)
|
request("account_deletion_preview", "POST", aToken, nil, 409)
|
||||||
@@ -462,7 +465,7 @@ func runPostgresAPI(t *testing.T, stringsMode bool) {
|
|||||||
}
|
}
|
||||||
request(action, "GET", otherToken, args, 0)
|
request(action, "GET", otherToken, args, 0)
|
||||||
}
|
}
|
||||||
request("diary_update", "POST", otherToken, M{"id": diaryID, "title": "intrusion", "content": "bad"}, 404)
|
request("diary_update", "POST", otherToken, M{"id": diaryID, "title": "intrusion", "content": "bad"}, 403)
|
||||||
detail := get("diary_detail", aToken, M{"id": diaryID})
|
detail := get("diary_detail", aToken, M{"id": diaryID})
|
||||||
if strings.Contains(fmt.Sprint(detail), "intrusion") {
|
if strings.Contains(fmt.Sprint(detail), "intrusion") {
|
||||||
t.Fatal("foreign account overwrote diary")
|
t.Fatal("foreign account overwrote diary")
|
||||||
@@ -614,11 +617,7 @@ func runPostgresAPI(t *testing.T, stringsMode bool) {
|
|||||||
if n := get("announcement", aToken, nil); n["enabled"] != false || n["content"] != "" || n["revision"] == notice["revision"] {
|
if n := get("announcement", aToken, nil); n["enabled"] != false || n["content"] != "" || n["revision"] == notice["revision"] {
|
||||||
t.Fatalf("announcement disable failed: %v", n)
|
t.Fatalf("announcement disable failed: %v", n)
|
||||||
}
|
}
|
||||||
post("smoking_card_visible", aToken, M{"visible": 0})
|
request("smoking_card_visible", "POST", aToken, M{"visible": 0}, 403)
|
||||||
if me := get("check_auth", aToken, nil)["user"].(M); store.ToInt(me["smokingCardVisible"]) != 0 {
|
|
||||||
t.Fatalf("preference not persisted: %v", me)
|
|
||||||
}
|
|
||||||
post("smoking_card_visible", aToken, M{"visible": 1})
|
|
||||||
post("admin_announcement", "admin", M{"mode": "always", "title": "Always", "content": "Every visit"})
|
post("admin_announcement", "admin", M{"mode": "always", "title": "Always", "content": "Every visit"})
|
||||||
if n := get("announcement", aToken, nil); n["mode"] != "always" || n["enabled"] != true {
|
if n := get("announcement", aToken, nil); n["mode"] != "always" || n["enabled"] != true {
|
||||||
t.Fatalf("always mode failed: %v", n)
|
t.Fatalf("always mode failed: %v", n)
|
||||||
@@ -650,14 +649,32 @@ func runPostgresAPI(t *testing.T, stringsMode bool) {
|
|||||||
if uniqueUpload.Error != "" {
|
if uniqueUpload.Error != "" {
|
||||||
t.Fatal(uniqueUpload.Error)
|
t.Fatal(uniqueUpload.Error)
|
||||||
}
|
}
|
||||||
ownedAlbum := id(post("album_add", deleteToken, M{"title": "Delete personal"}), "id")
|
// Seed historical personal content directly: new unbound writes are forbidden,
|
||||||
post("photo_add", deleteToken, M{"album_id": ownedAlbum, "file_id": ownFile})
|
// but account deletion must still remove records created under the old policy.
|
||||||
post("photo_add", deleteToken, M{"album_id": ownedAlbum, "file_id": uniqueUpload.FileId})
|
legacyAlbum := model.Album{Title: "Delete personal", CreatedBy: &otherID}
|
||||||
deletedDiary := id(post("diary_add", deleteToken, M{"content": "Delete this diary", "image_file_ids": []string{ownFile}}), "id")
|
if err := db.ORM.Create(&legacyAlbum).Error; err != nil {
|
||||||
post("diary_delete", deleteToken, M{"id": deletedDiary})
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, fid := range []string{ownFile, uniqueUpload.FileId} {
|
||||||
|
photo := model.Photo{AlbumId: legacyAlbum.Id, FileId: &fid, Path: "/api/files/" + fid, CreatedBy: &otherID}
|
||||||
|
if err := db.ORM.Create(&photo).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
legacyDiary := model.Diary{Title: "Legacy diary", Content: "Delete this diary", DiaryDate: today(), CreatedBy: &otherID}
|
||||||
|
if err := db.ORM.Create(&legacyDiary).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
deletedDiary := legacyDiary.Id
|
||||||
|
if err := db.ORM.Delete(&legacyDiary).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
request("account_delete", "POST", deleteToken, M{}, 400)
|
request("account_delete", "POST", deleteToken, M{}, 400)
|
||||||
preview := post("account_deletion_preview", deleteToken, nil)
|
preview := post("account_deletion_preview", deleteToken, nil)
|
||||||
post("album_add", deleteToken, M{"title": "Added after preview"})
|
addedLegacy := model.Album{Title: "Added after preview", CreatedBy: &otherID}
|
||||||
|
if err := db.ORM.Create(&addedLegacy).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
confirm := M{"confirmed": true, "confirmation_text": "注销并删除全部数据", "confirmation_token": preview["confirmation_token"]}
|
confirm := M{"confirmed": true, "confirmation_text": "注销并删除全部数据", "confirmation_token": preview["confirmation_token"]}
|
||||||
request("account_delete", "POST", deleteToken, confirm, 409)
|
request("account_delete", "POST", deleteToken, confirm, 409)
|
||||||
preview = post("account_deletion_preview", deleteToken, nil)
|
preview = post("account_deletion_preview", deleteToken, nil)
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package httpapi
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// 记录功能仅供已绑定情侣的用户使用;资料、头像、存储套餐与邀请独立开放。
|
||||||
|
func recordAction(action string) bool {
|
||||||
|
for _, prefix := range []string{"diary_", "timeline_", "album_", "photo_", "songs_", "checkin_", "smoking_", "pet_", "period_"} {
|
||||||
|
if strings.HasPrefix(action, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return action == "photos" || action == "set_album_cover"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) requireRecordBinding() error {
|
||||||
|
if !recordAction(c.Action) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if c.CoupleID() == "" {
|
||||||
|
return c.ErrCode(403, "请先绑定情侣,再使用记录功能", 10002)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package httpapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRecordBindingAccess(t *testing.T) {
|
||||||
|
for _, action := range []string{"diary_list", "diary_add", "diary_comment_add", "timeline_detail", "album_add", "photos", "photo_upload", "set_album_cover", "songs_list", "checkin_do", "smoking_card_visible", "pet_adopt", "period_daily_save"} {
|
||||||
|
t.Run(action, func(t *testing.T) {
|
||||||
|
for _, cid := range []string{"", "bound-couple"} {
|
||||||
|
out := httptest.NewRecorder()
|
||||||
|
g, _ := gin.CreateTestContext(out)
|
||||||
|
c := &Ctx{G: g, Action: action, coupleResolved: true, coupleID: cid}
|
||||||
|
err := c.requireRecordBinding()
|
||||||
|
if cid == "" {
|
||||||
|
if !IsResponded(err) || out.Code != 403 {
|
||||||
|
t.Fatalf("unbound allowed: %v %d", err, out.Code)
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, action := range []string{"upload", "upload_avatar", "update_profile", "couple_info", "invite_partner", "storage_info", "account_delete", "announcement", "admin_contents"} {
|
||||||
|
if recordAction(action) {
|
||||||
|
t.Fatalf("unrelated feature blocked: %s", action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -241,6 +241,10 @@ func (r *Router) serveAPI(g *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := c.requireRecordBinding(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if c.DB != nil {
|
if c.DB != nil {
|
||||||
c.DB = c.DB.WithActor(c.uid, adminActions[action] && action != "admin_login")
|
c.DB = c.DB.WithActor(c.uid, adminActions[action] && action != "admin_login")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
const { tabForPage, paths } = require('../../utils/navigation');
|
|
||||||
Component({
|
|
||||||
data: { active: '' },
|
|
||||||
lifetimes: { attached: function() { this.syncActive(); } },
|
|
||||||
pageLifetimes: { show: function() { this.syncActive(); } },
|
|
||||||
methods: {
|
|
||||||
syncActive: function() {
|
|
||||||
var pages = getCurrentPages();
|
|
||||||
var current = pages[pages.length - 1];
|
|
||||||
this.setData({ active: tabForPage(current && current.route) });
|
|
||||||
},
|
|
||||||
navigate: function(e) {
|
|
||||||
var tab = e.currentTarget.dataset.tab;
|
|
||||||
if (!paths[tab]) return;
|
|
||||||
var pages = getCurrentPages();
|
|
||||||
// 当前主页面重复点击不重新加载。
|
|
||||||
if (pages.length && pages[pages.length - 1].route === paths[tab]) return;
|
|
||||||
wx.switchTab({ url: '/' + paths[tab] });
|
|
||||||
}
|
|
||||||
} });
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"component":true}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<view class="tabs" wx:if="{{active}}">
|
|
||||||
<view class="tab {{active === 'home' ? 'active' : ''}}" data-tab="home" bindtap="navigate" hover-class="tab-pressed" aria-label="我们" aria-role="button">
|
|
||||||
<view class="tab-icon"><image src="{{active === 'home' ? '/components/app-tabbar/icons/us-active.svg' : '/components/app-tabbar/icons/us.svg'}}" mode="aspectFit" /></view><text class="tab-label">我们</text>
|
|
||||||
</view>
|
|
||||||
<view class="tab {{active === 'records' ? 'active' : ''}}" data-tab="records" bindtap="navigate" hover-class="tab-pressed" aria-label="记录" aria-role="button">
|
|
||||||
<view class="tab-icon"><image src="{{active === 'records' ? '/components/app-tabbar/icons/journal-active.svg' : '/components/app-tabbar/icons/journal.svg'}}" mode="aspectFit" /></view><text class="tab-label">记录</text>
|
|
||||||
</view>
|
|
||||||
<view class="tab {{active === 'profile' ? 'active' : ''}}" data-tab="profile" bindtap="navigate" hover-class="tab-pressed" aria-label="我的" aria-role="button">
|
|
||||||
<view class="tab-icon"><image src="{{active === 'profile' ? '/components/app-tabbar/icons/me-active.svg' : '/components/app-tabbar/icons/me.svg'}}" mode="aspectFit" /></view><text class="tab-label">我的</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
.tabs {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 900;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background: #FFF0F5;
|
|
||||||
border-top: 1rpx solid #F3DCE5;
|
|
||||||
padding: 10rpx 20rpx calc(12rpx + env(safe-area-inset-bottom));
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.tab {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 92rpx;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4rpx;
|
|
||||||
padding: 4rpx 0;
|
|
||||||
color: #8B827B;
|
|
||||||
}
|
|
||||||
.tab-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 88rpx;
|
|
||||||
height: 54rpx;
|
|
||||||
border-radius: 32rpx;
|
|
||||||
background: transparent;
|
|
||||||
transition: background-color .18s ease;
|
|
||||||
}
|
|
||||||
.tab-icon image { width: 48rpx; height: 48rpx; }
|
|
||||||
.tab-label { font-size: 23rpx; line-height: 1.4; letter-spacing: 1rpx; }
|
|
||||||
.active { color: #B74768; }
|
|
||||||
.active .tab-icon { background: #FADCE7; }
|
|
||||||
.active .tab-label { font-weight: 600; }
|
|
||||||
.tab-pressed .tab-icon { background: #FCE5ED; }
|
|
||||||
|
|
||||||
@import '../../styles/typography.wxss';
|
|
||||||
@@ -8,6 +8,7 @@ Component({
|
|||||||
lifetimes: { attached(){this.load();} },
|
lifetimes: { attached(){this.load();} },
|
||||||
pageLifetimes: { show(){this.load();} },
|
pageLifetimes: { show(){this.load();} },
|
||||||
methods: {
|
methods: {
|
||||||
|
openDisplaySettings(){wx.navigateTo({url:'/pages/settings/settings'});},
|
||||||
async load(force) {
|
async load(force) {
|
||||||
if(this._loading) return this._loading;
|
if(this._loading) return this._loading;
|
||||||
if(force)cache.invalidate(['smokingInfo']);
|
if(force)cache.invalidate(['smokingInfo']);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<view class="smoke-card">
|
<view class="smoke-card">
|
||||||
|
<text wx:if="{{!detailed}}" class="smoke-setting-hint" bindtap="openDisplaySettings">可在「我的 → 更多」关闭此事件 ›</text>
|
||||||
<view class="smoke-head" bindtap="openDetail"><view><text class="smoke-eyebrow">默认事件 · 按支记录</text><text class="smoke-title">抽烟记录</text></view><text class="smoke-link" wx:if="{{!detailed}}">明细 ›</text></view>
|
<view class="smoke-head" bindtap="openDetail"><view><text class="smoke-eyebrow">默认事件 · 按支记录</text><text class="smoke-title">抽烟记录</text></view><text class="smoke-link" wx:if="{{!detailed}}">明细 ›</text></view>
|
||||||
<view class="owners" wx:if="{{detailed && info && info.partner_id}}"><view class="owner {{!viewUserId ? 'selected' : ''}}" data-user="0" bindtap="switchOwner">我的记录</view><view class="owner {{viewUserId ? 'selected' : ''}}" data-user="{{info.partner_id}}" bindtap="switchOwner">TA的记录</view></view>
|
<view class="owners" wx:if="{{detailed && info && info.partner_id}}"><view class="owner {{!viewUserId ? 'selected' : ''}}" data-user="0" bindtap="switchOwner">我的记录</view><view class="owner {{viewUserId ? 'selected' : ''}}" data-user="{{info.partner_id}}" bindtap="switchOwner">TA的记录</view></view>
|
||||||
<view wx:if="{{loading}}" class="smoke-hint">正在加载记录…</view>
|
<view wx:if="{{loading}}" class="smoke-hint">正在加载记录…</view>
|
||||||
|
|||||||
@@ -16,3 +16,5 @@
|
|||||||
.smoke-periods{display:flex;gap:16rpx}.smoke-period{flex:1;min-width:0;padding:22rpx 18rpx;border:1rpx solid #F1DCE4;border-radius:20rpx;background:#FFF5F8}.smoke-period-head{display:flex;align-items:center;justify-content:space-between;gap:8rpx;font-size:25rpx;color:#64424D}.smoke-period-head text+text{font-size:18rpx;color:#A39386}.smoke-period-row{display:flex;justify-content:space-between;gap:10rpx;font-size:23rpx;margin-top:18rpx;color:#85776D}.smoke-period-row text+text{color:#B74768;overflow-wrap:anywhere}.smoke-period-hint{display:block;margin-top:14rpx;font-size:20rpx;color:#A39386;line-height:1.6}
|
.smoke-periods{display:flex;gap:16rpx}.smoke-period{flex:1;min-width:0;padding:22rpx 18rpx;border:1rpx solid #F1DCE4;border-radius:20rpx;background:#FFF5F8}.smoke-period-head{display:flex;align-items:center;justify-content:space-between;gap:8rpx;font-size:25rpx;color:#64424D}.smoke-period-head text+text{font-size:18rpx;color:#A39386}.smoke-period-row{display:flex;justify-content:space-between;gap:10rpx;font-size:23rpx;margin-top:18rpx;color:#85776D}.smoke-period-row text+text{color:#B74768;overflow-wrap:anywhere}.smoke-period-hint{display:block;margin-top:14rpx;font-size:20rpx;color:#A39386;line-height:1.6}
|
||||||
|
|
||||||
.smoke-ledger-footer{text-align:center;padding-top:20rpx}
|
.smoke-ledger-footer{text-align:center;padding-top:20rpx}
|
||||||
|
|
||||||
|
.smoke-setting-hint{display:block;text-align:right;color:#A39386;font-size:20rpx;line-height:1.6;margin-bottom:12rpx;}
|
||||||
|
|||||||
+7
-13
@@ -1,13 +1,7 @@
|
|||||||
// 开发工具使用本机服务;真机调试时把 dev.apiBase 改为电脑的局域网 IP。
|
// 上传前将此地址改为正式 API 地址。
|
||||||
const environments = {
|
//const API_BASE = 'http://192.168.100.3:8080/api';
|
||||||
dev: { apiBase: 'http://127.0.0.1:8080/api' },
|
|
||||||
trial: { apiBase: 'https://we.sundynix.cn/api' },
|
const API_BASE = 'https://we.sundynix.cn/api';
|
||||||
release: { apiBase: 'https://we.sundynix.cn/api' }
|
|
||||||
};
|
|
||||||
let version = 'release';
|
module.exports = { API_BASE };
|
||||||
try {
|
|
||||||
const account = wx.getAccountInfoSync();
|
|
||||||
version = account.miniProgram.envVersion || 'release';
|
|
||||||
} catch (e) {}
|
|
||||||
const env = version === 'develop' ? 'dev' : version === 'trial' ? 'trial' : 'release';
|
|
||||||
module.exports = { env, API_BASE: environments[env].apiBase };
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { requireRecordAccess } = require('../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../utils/subscribe');
|
const { tryLoginRenewal } = require('../utils/subscribe');
|
||||||
const { tabForPage, paths } = require('../utils/navigation');
|
const { tabForPage, paths } = require('../utils/navigation');
|
||||||
Component({
|
Component({
|
||||||
@@ -15,6 +16,7 @@ Component({
|
|||||||
tryLoginRenewal();
|
tryLoginRenewal();
|
||||||
var tab = e.currentTarget.dataset.tab;
|
var tab = e.currentTarget.dataset.tab;
|
||||||
if (!paths[tab]) return;
|
if (!paths[tab]) return;
|
||||||
|
if (tab === 'records' && !requireRecordAccess()) return;
|
||||||
var pages = getCurrentPages();
|
var pages = getCurrentPages();
|
||||||
// 当前主页面重复点击不重新加载。
|
// 当前主页面重复点击不重新加载。
|
||||||
if (pages.length && pages[pages.length - 1].route === paths[tab]) return;
|
if (pages.length && pages[pages.length - 1].route === paths[tab]) return;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { withRecordAccess } = require('../../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const { uploadReference } = require('../../utils/file-media');
|
const { uploadReference } = require('../../utils/file-media');
|
||||||
const { itemsOf, startList, current, mergeUnique, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
const { itemsOf, startList, current, mergeUnique, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||||
@@ -8,7 +9,7 @@ const { getLayout } = require('../../utils/layout');
|
|||||||
// pages/album/album.js - 日记式分组相册
|
// pages/album/album.js - 日记式分组相册
|
||||||
const { api, getImageUrl, API_BASE, uploadFileCompat } = require('../../utils/request');
|
const { api, getImageUrl, API_BASE, uploadFileCompat } = require('../../utils/request');
|
||||||
const { guardStorageSpace } = require('../../utils/storage-guard');
|
const { guardStorageSpace } = require('../../utils/storage-guard');
|
||||||
Page(withDataCache(withListPagination({
|
Page(withRecordAccess(withDataCache(withListPagination({
|
||||||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||||||
onResize: function() { this.setData(getLayout()); },
|
onResize: function() { this.setData(getLayout()); },
|
||||||
data: { loading: true, loadError: false,
|
data: { loading: true, loadError: false,
|
||||||
@@ -534,4 +535,4 @@ Page(withDataCache(withListPagination({
|
|||||||
goToAlbums: function() { wx.navigateBack(); },
|
goToAlbums: function() { wx.navigateBack(); },
|
||||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||||
}, 'loadData'), {"loadData": ["photos", "albumList"]}));
|
}, 'loadData'), {"loadData": ["photos", "albumList"]})));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<view capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
<view wx:if="{{!recordAccessBlocked}}" capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||||
<launch-transition />
|
<launch-transition />
|
||||||
<!-- pages/album/album.wxml -->
|
<!-- pages/album/album.wxml -->
|
||||||
<app-nav title="相册详情" back="{{true}}" />
|
<app-nav title="相册详情" back="{{true}}" />
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
const { withRecordAccess } = require('../../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||||
const { withDataCache } = require('../../utils/page-cache');
|
const { withDataCache } = require('../../utils/page-cache');
|
||||||
const { getLayout } = require('../../utils/layout');
|
const { getLayout } = require('../../utils/layout');
|
||||||
// pages/albums/albums.js
|
// pages/albums/albums.js
|
||||||
const { api, getImageUrl } = require('../../utils/request');
|
const { api, getImageUrl } = require('../../utils/request');
|
||||||
Page(withDataCache(withListPagination({
|
Page(withRecordAccess(withDataCache(withListPagination({
|
||||||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||||||
onResize: function() { this.setData(getLayout()); },
|
onResize: function() { this.setData(getLayout()); },
|
||||||
data: {
|
data: {
|
||||||
@@ -177,4 +178,4 @@ Page(withDataCache(withListPagination({
|
|||||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||||
goBack: function() { wx.navigateBack(); },
|
goBack: function() { wx.navigateBack(); },
|
||||||
goToProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
goToProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||||
}, 'loadAlbums'), {"loadAlbums": ["albumList"]}));
|
}, 'loadAlbums'), {"loadAlbums": ["albumList"]})));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<view capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
<view wx:if="{{!recordAccessBlocked}}" capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||||
<launch-transition />
|
<launch-transition />
|
||||||
<!-- pages/albums/albums.wxml - 相册列表 -->
|
<!-- pages/albums/albums.wxml - 相册列表 -->
|
||||||
<app-nav title="{{uploadMode ? '选择相册' : '相册'}}" back="{{true}}" />
|
<app-nav title="{{uploadMode ? '选择相册' : '相册'}}" back="{{true}}" />
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { withRecordAccess } = require('../../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const { uploadReference } = require('../../utils/file-media');
|
const { uploadReference } = require('../../utils/file-media');
|
||||||
const { withDataCache } = require('../../utils/page-cache');
|
const { withDataCache } = require('../../utils/page-cache');
|
||||||
@@ -7,7 +8,7 @@ const { api, getImageUrl, API_BASE, uploadFileCompat } = require('../../utils/re
|
|||||||
const { createSwipeBack } = require('../../utils/util');
|
const { createSwipeBack } = require('../../utils/util');
|
||||||
const { guardStorageSpace } = require('../../utils/storage-guard');
|
const { guardStorageSpace } = require('../../utils/storage-guard');
|
||||||
const COOLDOWN = 5000;
|
const COOLDOWN = 5000;
|
||||||
Page(withDataCache({
|
Page(withRecordAccess(withDataCache({
|
||||||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||||||
onResize: function() { this.setData(getLayout()); },
|
onResize: function() { this.setData(getLayout()); },
|
||||||
data: { loading: true, loadError: false, item: null, logs: [], logsPage: 1, logsTotal: 0, logsHasMore: false, logsLoading: false, logsError: false, freqLabel: '', typeLabel: '', myStreak: 0, partnerStreak: 0, showCheckModal: false, checkPhoto: '', checkNote: '', lastActionTime: 0, detailId: 0, myAvatar: '', partnerAvatar: '', myName: '我', partnerName: 'TA' },
|
data: { loading: true, loadError: false, item: null, logs: [], logsPage: 1, logsTotal: 0, logsHasMore: false, logsLoading: false, logsError: false, freqLabel: '', typeLabel: '', myStreak: 0, partnerStreak: 0, showCheckModal: false, checkPhoto: '', checkNote: '', lastActionTime: 0, detailId: 0, myAvatar: '', partnerAvatar: '', myName: '我', partnerName: 'TA' },
|
||||||
@@ -223,4 +224,4 @@ Page(withDataCache({
|
|||||||
goBack: function() { wx.navigateBack(); },
|
goBack: function() { wx.navigateBack(); },
|
||||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||||
}, {"loadDetail": ["checkinDetail", "coupleInfo"]}));
|
}, {"loadDetail": ["checkinDetail", "coupleInfo"]})));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<view capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
<view wx:if="{{!recordAccessBlocked}}" capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||||
<launch-transition />
|
<launch-transition />
|
||||||
<!-- pages/checkin-detail/checkin-detail.wxml - 打卡详情 -->
|
<!-- pages/checkin-detail/checkin-detail.wxml - 打卡详情 -->
|
||||||
<app-nav title="打卡详情" back="{{true}}" />
|
<app-nav title="打卡详情" back="{{true}}" />
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { withRecordAccess } = require('../../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||||
const { withDataCache } = require('../../utils/page-cache');
|
const { withDataCache } = require('../../utils/page-cache');
|
||||||
@@ -6,7 +7,7 @@ const { getLayout } = require('../../utils/layout');
|
|||||||
const { api, getImageUrl } = require('../../utils/request');
|
const { api, getImageUrl } = require('../../utils/request');
|
||||||
const { diaryEntry } = require('../../utils/journal');
|
const { diaryEntry } = require('../../utils/journal');
|
||||||
const { saveScrollPosition, getScrollPosition } = require('../../utils/scroll-pos');
|
const { saveScrollPosition, getScrollPosition } = require('../../utils/scroll-pos');
|
||||||
Page(withDataCache(withListPagination({
|
Page(withRecordAccess(withDataCache(withListPagination({
|
||||||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||||||
onResize: function() { this.setData(getLayout()); },
|
onResize: function() { this.setData(getLayout()); },
|
||||||
data: {
|
data: {
|
||||||
@@ -166,4 +167,4 @@ Page(withDataCache(withListPagination({
|
|||||||
goHome: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/index/index' }); },
|
goHome: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/index/index' }); },
|
||||||
goBack: function() { wx.navigateBack(); },
|
goBack: function() { wx.navigateBack(); },
|
||||||
goProfile: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/profile/profile' }); }
|
goProfile: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||||
}, 'loadData'), {"loadData": ["diaryList", "coupleInfo"]}));
|
}, 'loadData'), {"loadData": ["diaryList", "coupleInfo"]})));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<import src="../../templates/journal.wxml" />
|
<import src="../../templates/journal.wxml" />
|
||||||
<launch-transition />
|
<launch-transition />
|
||||||
<view capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}"><app-nav title="日记" back="{{true}}" />
|
<view wx:if="{{!recordAccessBlocked}}" capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}"><app-nav title="日记" back="{{true}}" />
|
||||||
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bindscrolltolower="onScrollToLower" lower-threshold="100" bindscroll="onScroll" scroll-top="{{scrollTop}}">
|
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bindscrolltolower="onScrollToLower" lower-threshold="100" bindscroll="onScroll" scroll-top="{{scrollTop}}">
|
||||||
<view class="j-body"><view class="j-row"><view><text class="j-eyebrow">LIFE IN WORDS</text><text class="j-page-title">生活里的每一页</text></view><button class="j-button j-button-small" bindtap="goToPublish">+ 写日记</button></view>
|
<view class="j-body"><view class="j-row"><view><text class="j-eyebrow">LIFE IN WORDS</text><text class="j-page-title">生活里的每一页</text></view><button class="j-button j-button-small" bindtap="goToPublish">+ 写日记</button></view>
|
||||||
<page-state wx:if="{{loading}}" kind="loading" title="正在翻开日记" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" action="重试" bind:action="loadData" />
|
<page-state wx:if="{{loading}}" kind="loading" title="正在翻开日记" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" action="重试" bind:action="loadData" />
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
const { withRecordAccess } = require('../../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const { itemsOf, totalOf, mergeUnique } = require('../../utils/list-pagination');
|
const { itemsOf, totalOf, mergeUnique } = require('../../utils/list-pagination');
|
||||||
const { withDataCache } = require('../../utils/page-cache');
|
const { withDataCache } = require('../../utils/page-cache');
|
||||||
const { getLayout } = require('../../utils/layout');
|
const { getLayout } = require('../../utils/layout');
|
||||||
const { api, getImageUrl } = require('../../utils/request');
|
const { api, getImageUrl } = require('../../utils/request');
|
||||||
Page(withDataCache({
|
Page(withRecordAccess(withDataCache({
|
||||||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||||||
noop: function() {},
|
noop: function() {},
|
||||||
onResize: function() { this.setData(getLayout()); },
|
onResize: function() { this.setData(getLayout()); },
|
||||||
@@ -279,4 +280,4 @@ Page(withDataCache({
|
|||||||
|
|
||||||
goBack: function() { wx.navigateBack(); },
|
goBack: function() { wx.navigateBack(); },
|
||||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); }
|
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); }
|
||||||
}, {"loadDiary": ["diaryDetail"], "loadComments": ["diaryCommentList"]}));
|
}, {"loadDiary": ["diaryDetail"], "loadComments": ["diaryCommentList"]})));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<view capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
<view wx:if="{{!recordAccessBlocked}}" capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||||
<launch-transition />
|
<launch-transition />
|
||||||
<app-nav title="日记详情" back="{{true}}" /><view class="page-bg">
|
<app-nav title="日记详情" back="{{true}}" /><view class="page-bg">
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { withRecordAccess } = require('../../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const { uploadReference } = require('../../utils/file-media');
|
const { uploadReference } = require('../../utils/file-media');
|
||||||
const { getLayout } = require('../../utils/layout');
|
const { getLayout } = require('../../utils/layout');
|
||||||
@@ -7,7 +8,7 @@ const { guardStorageSpace } = require('../../utils/storage-guard');
|
|||||||
var MOOD_OPTIONS = ['😊', '🥰', '😢', '😤', '😴', '🤩', '🥺', '😎', '🤔', '😭', '😋', '😌'];
|
var MOOD_OPTIONS = ['😊', '🥰', '😢', '😤', '😴', '🤩', '🥺', '😎', '🤔', '😭', '😋', '😌'];
|
||||||
var WEATHER_OPTIONS = ['☀️', '⛅', '☁️', '🌧️', '⛈️', '❄️', '🌤️', '🌥️', '🌈', '🌪️', '🌫️', '🌙'];
|
var WEATHER_OPTIONS = ['☀️', '⛅', '☁️', '🌧️', '⛈️', '❄️', '🌤️', '🌥️', '🌈', '🌪️', '🌫️', '🌙'];
|
||||||
|
|
||||||
Page({
|
Page(withRecordAccess({
|
||||||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||||||
onResize: function() { this.setData(getLayout()); },
|
onResize: function() { this.setData(getLayout()); },
|
||||||
data: {
|
data: {
|
||||||
@@ -165,4 +166,4 @@ Page({
|
|||||||
goBack: function() {
|
goBack: function() {
|
||||||
wx.navigateBack();
|
wx.navigateBack();
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<view capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
<view wx:if="{{!recordAccessBlocked}}" capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||||
<launch-transition />
|
<launch-transition />
|
||||||
<app-nav title="编辑日记" back="{{true}}" />
|
<app-nav title="编辑日记" back="{{true}}" />
|
||||||
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" style="height:100vh;">
|
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" style="height:100vh;">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { guardRecordEntries, withRecordAccess } = require('../../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const { itemsOf, totalOf } = require('../../utils/list-pagination');
|
const { itemsOf, totalOf } = require('../../utils/list-pagination');
|
||||||
const { withDataCache } = require('../../utils/page-cache');
|
const { withDataCache } = require('../../utils/page-cache');
|
||||||
@@ -7,7 +8,7 @@ const { api } = require('../../utils/request');
|
|||||||
const { diaryEntry, albumEntry, memoryEntry } = require('../../utils/journal');
|
const { diaryEntry, albumEntry, memoryEntry } = require('../../utils/journal');
|
||||||
const { saveScrollPosition, resetScrollOnTabShow } = require('../../utils/scroll-pos');
|
const { saveScrollPosition, resetScrollOnTabShow } = require('../../utils/scroll-pos');
|
||||||
|
|
||||||
Page(withDataCache({
|
Page(withRecordAccess(guardRecordEntries(withDataCache({
|
||||||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||||||
onResize: function() { this.setData(getLayout()); },
|
onResize: function() { this.setData(getLayout()); },
|
||||||
data: {
|
data: {
|
||||||
@@ -100,4 +101,4 @@ Page(withDataCache({
|
|||||||
this._savePosBeforeLeave();
|
this._savePosBeforeLeave();
|
||||||
wx.navigateTo({ url: '/pages/period/period' });
|
wx.navigateTo({ url: '/pages/period/period' });
|
||||||
}
|
}
|
||||||
}, {"loadStats": ["timelineList", "albumList", "diaryList"]}));
|
}, {"loadStats": ["timelineList", "albumList", "diaryList"]}), ['showPublish', '_publish', 'uploadPhotos', 'openJournalEntry', 'openAlbum', 'goToDiaryFromDiscover', 'goToTimeline', 'goToAlbum', 'goToSongs', 'goToCheckin', 'goToPet', 'goToPeriod', 'selectFilter'])));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<import src="../../templates/journal.wxml" />
|
<import src="../../templates/journal.wxml" />
|
||||||
<launch-transition />
|
<launch-transition />
|
||||||
<view capture-bind:tap="onLoginSubscribeTap" class="app-shell j-handwritten-page" style="{{layoutStyle}}"><view class="j-fixed-header"><view class="j-fixed-heading" style="height:{{barHeight}}px;padding-right:{{navSide}}px;"><text class="j-eyebrow">PAGES OF US</text><text class="j-page-title">我们的生活手账</text></view></view>
|
<view wx:if="{{!recordAccessBlocked}}" capture-bind:tap="onLoginSubscribeTap" class="app-shell j-handwritten-page" style="{{layoutStyle}}"><view class="j-fixed-header"><view class="j-fixed-heading" style="height:{{barHeight}}px;padding-right:{{navSide}}px;"><text class="j-eyebrow">PAGES OF US</text><text class="j-page-title">我们的生活手账</text></view></view>
|
||||||
<scroll-view class="page-bg j-main-scroll" scroll-y show-scrollbar="{{false}}" bindscroll="onScroll" scroll-top="{{scrollTop}}" refresher-enabled="{{true}}" refresher-triggered="{{refreshing}}" bindrefresherrefresh="onRefresh">
|
<scroll-view class="page-bg j-main-scroll" scroll-y show-scrollbar="{{false}}" bindscroll="onScroll" scroll-top="{{scrollTop}}" refresher-enabled="{{true}}" refresher-triggered="{{refreshing}}" bindrefresherrefresh="onRefresh">
|
||||||
<view class="j-body"><view class="records-action-row"><text class="j-small-title">一起的日常</text><button class="j-button j-button-small" bindtap="showPublish">+ 记录</button></view>
|
<view class="j-body"><view class="records-action-row"><text class="j-small-title">一起的日常</text><button class="j-button j-button-small" bindtap="showPublish">+ 记录</button></view>
|
||||||
<view class="j-tools records-tools"><view class="j-tool-grid"><view bindtap="goToSongs"><image src="/images/stat-music.svg" /><text>歌曲</text></view><view bindtap="goToCheckin"><image src="/images/stat-check.svg" /><text>打卡</text></view><view bindtap="goToPet"><image src="/images/stat-pet.svg" /><text>宠物</text></view><view bindtap="goToPeriod"><image src="/images/stat-j-flower.svg" /><text>姨姨</text></view></view></view>
|
<view class="j-tools records-tools"><view class="j-tool-grid"><view bindtap="goToSongs"><image src="/images/stat-music.svg" /><text>歌曲</text></view><view bindtap="goToCheckin"><image src="/images/stat-check.svg" /><text>打卡</text></view><view bindtap="goToPet"><image src="/images/stat-pet.svg" /><text>宠物</text></view><view bindtap="goToPeriod"><image src="/images/stat-j-flower.svg" /><text>姨姨</text></view></view></view>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { guardRecordEntries } = require('../../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const announcementSession = require('../../utils/announcement-session');
|
const announcementSession = require('../../utils/announcement-session');
|
||||||
const { itemsOf, totalOf } = require('../../utils/list-pagination');
|
const { itemsOf, totalOf } = require('../../utils/list-pagination');
|
||||||
@@ -10,11 +11,12 @@ const { api, getImageUrl } = require('../../utils/request');
|
|||||||
const { diaryEntry } = require('../../utils/journal');
|
const { diaryEntry } = require('../../utils/journal');
|
||||||
const { saveScrollPosition, resetScrollOnTabShow } = require('../../utils/scroll-pos');
|
const { saveScrollPosition, resetScrollOnTabShow } = require('../../utils/scroll-pos');
|
||||||
|
|
||||||
Page(withDataCache({
|
Page(guardRecordEntries(withDataCache({
|
||||||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||||||
onResize: function() { this.setData(getLayout()); },
|
onResize: function() { this.setData(getLayout()); },
|
||||||
data: {
|
data: {
|
||||||
showAnnouncement:false, announcement:null,
|
showAnnouncement:false, announcement:null,
|
||||||
|
greetingDate:'', greetingText:'你好', greetingNote:'把今天的小美好,也留在这里。',
|
||||||
userId: null,
|
userId: null,
|
||||||
coupleId: null,
|
coupleId: null,
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ Page(withDataCache({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onLoad: function() {
|
onLoad: function() {
|
||||||
|
this.updateGreeting();
|
||||||
this.setData(getLayout());
|
this.setData(getLayout());
|
||||||
this.setData({
|
this.setData({
|
||||||
userId: wx.getStorageSync('userId'),
|
userId: wx.getStorageSync('userId'),
|
||||||
@@ -57,6 +60,7 @@ Page(withDataCache({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onShow: function() {
|
onShow: function() {
|
||||||
|
this.updateGreeting();
|
||||||
this._announcementActive=true;
|
this._announcementActive=true;
|
||||||
this.loadAnnouncement();
|
this.loadAnnouncement();
|
||||||
resetScrollOnTabShow(this, 'index');
|
resetScrollOnTabShow(this, 'index');
|
||||||
@@ -78,6 +82,18 @@ Page(withDataCache({
|
|||||||
this.fetchIpLocation();
|
this.fetchIpLocation();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateGreeting: function() {
|
||||||
|
const now = new Date(), hour = now.getHours();
|
||||||
|
const weekdays = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
|
||||||
|
let text, note;
|
||||||
|
if (hour < 5) { text='夜深了'; note='记下心里的小事,也别忘了早点休息。'; }
|
||||||
|
else if (hour < 11) { text='早上好'; note='新的一天,也有值得珍藏的小美好。'; }
|
||||||
|
else if (hour < 14) { text='中午好'; note='好好吃饭,慢慢收藏生活里的甜。'; }
|
||||||
|
else if (hour < 18) { text='下午好'; note='让忙碌暂停一会儿,记下此刻的心情。'; }
|
||||||
|
else { text='晚上好'; note='今天的故事,想留在哪一页?'; }
|
||||||
|
this.setData({greetingDate:(now.getMonth()+1)+'月'+now.getDate()+'日 '+weekdays[now.getDay()],greetingText:text,greetingNote:note});
|
||||||
|
},
|
||||||
|
|
||||||
onHide: function() { this._announcementActive=false; },
|
onHide: function() { this._announcementActive=false; },
|
||||||
onUnload: function() { this._announcementActive=false; },
|
onUnload: function() { this._announcementActive=false; },
|
||||||
loadAnnouncement: function() {
|
loadAnnouncement: function() {
|
||||||
@@ -241,6 +257,10 @@ Page(withDataCache({
|
|||||||
},
|
},
|
||||||
|
|
||||||
loadStats: async function() {
|
loadStats: async function() {
|
||||||
|
if (!this.data.coupleId) {
|
||||||
|
this.setData({recentDiary:[],recentAlbums:[],upcomingMemory:null,statsLoading:false,statsError:false,stats:{timeline:0,album:0,diary:0,songs:0,checkin:0,pet:0}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.setData({ statsLoading: !this._hasCachedData, statsError: false });
|
this.setData({ statsLoading: !this._hasCachedData, statsError: false });
|
||||||
try {
|
try {
|
||||||
const [timeline, album, diary, songs, checkin, pet] = await Promise.all([
|
const [timeline, album, diary, songs, checkin, pet] = await Promise.all([
|
||||||
@@ -368,4 +388,4 @@ Page(withDataCache({
|
|||||||
imageUrl: '/images/share.png'
|
imageUrl: '/images/share.png'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, {"loadData": ["coupleInfo", "timelineList", "albumList", "diaryList", "songsList", "checkinList", "petInfo", "getBgConfig"]}));
|
}, {"loadData": ["coupleInfo", "timelineList", "albumList", "diaryList", "songsList", "checkinList", "petInfo", "getBgConfig"]}), ['goToDiary', 'goToTimeline', 'goToAlbum', 'goToSongs', 'goToCheckin', 'goToPet', 'goToPeriod', 'openJournalEntry', 'writeDiary', 'openAlbumPreview', 'openRecentDiary', 'showMore']));
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
<view class="home-fixed-header"><view class="home-fixed-heading" style="height:{{barHeight}}px;padding-right:{{navSide}}px;"><text class="j-eyebrow">OUR LITTLE WORLD</text><view class="home-handwritten-title"><text class="j-page-title">让所有的爱有迹可循</text></view></view></view>
|
<view class="home-fixed-header"><view class="home-fixed-heading" style="height:{{barHeight}}px;padding-right:{{navSide}}px;"><text class="j-eyebrow">OUR LITTLE WORLD</text><view class="home-handwritten-title"><text class="j-page-title">让所有的爱有迹可循</text></view></view></view>
|
||||||
<scroll-view class="page-bg home-scroll" scroll-y show-scrollbar="{{false}}" refresher-enabled="{{true}}" refresher-triggered="{{refreshing}}" bindrefresherrefresh="onRefresh" bindscroll="onScroll" scroll-top="{{scrollTop}}">
|
<scroll-view class="page-bg home-scroll" scroll-y show-scrollbar="{{false}}" refresher-enabled="{{true}}" refresher-triggered="{{refreshing}}" bindrefresherrefresh="onRefresh" bindscroll="onScroll" scroll-top="{{scrollTop}}">
|
||||||
<view class="j-body home-body">
|
<view class="j-body home-body">
|
||||||
|
<view class="home-greeting">
|
||||||
|
<text class="home-greeting-date">{{greetingDate}}</text>
|
||||||
|
<text class="home-greeting-title">{{greetingText}},{{partner1Name && partner1Name !== '?' ? partner1Name : '亲爱的'}}</text>
|
||||||
|
<text class="home-greeting-note">{{greetingNote}}</text>
|
||||||
|
</view>
|
||||||
<page-state wx:if="{{homeError}}" kind="error" title="首页加载失败" description="请检查网络后重新加载" action="重试" bind:action="loadData" />
|
<page-state wx:if="{{homeError}}" kind="error" title="首页加载失败" description="请检查网络后重新加载" action="重试" bind:action="loadData" />
|
||||||
<page-state wx:elif="{{!relationshipKnown}}" kind="loading" title="正在打开你的空间" />
|
<page-state wx:elif="{{!relationshipKnown}}" kind="loading" title="正在打开你的空间" />
|
||||||
<view wx:else class="j-cover">
|
<view wx:else class="j-cover">
|
||||||
|
|||||||
@@ -57,3 +57,11 @@
|
|||||||
.home-has-background .home-couple-panel { padding:24rpx; border:1rpx solid rgba(233,221,208,.65); border-radius:24rpx; background:rgba(255,253,248,.82); }
|
.home-has-background .home-couple-panel { padding:24rpx; border:1rpx solid rgba(233,221,208,.65); border-radius:24rpx; background:rgba(255,253,248,.82); }
|
||||||
|
|
||||||
.notice-overlay {position:fixed;inset:0;z-index:12000;display:flex;align-items:center;justify-content:center;background:rgba(49,36,42,.46);padding:48rpx;box-sizing:border-box;}.notice-dialog{width:100%;max-width:640rpx;background:#FFFCF8;border-radius:36rpx;padding:44rpx;box-sizing:border-box;}.notice-label{display:block;color:#9B7B83;font-size:22rpx;}.notice-title{display:block;font-size:36rpx;color:#64424D;margin:16rpx 0 28rpx;}.notice-content{max-height:50vh;white-space:pre-wrap;font-size:28rpx;line-height:1.8;color:#5F5557;}.notice-dismiss{margin-top:32rpx;background:#667E75;color:#fff;border-radius:20rpx;font-size:28rpx;}
|
.notice-overlay {position:fixed;inset:0;z-index:12000;display:flex;align-items:center;justify-content:center;background:rgba(49,36,42,.46);padding:48rpx;box-sizing:border-box;}.notice-dialog{width:100%;max-width:640rpx;background:#FFFCF8;border-radius:36rpx;padding:44rpx;box-sizing:border-box;}.notice-label{display:block;color:#9B7B83;font-size:22rpx;}.notice-title{display:block;font-size:36rpx;color:#64424D;margin:16rpx 0 28rpx;}.notice-content{max-height:50vh;white-space:pre-wrap;font-size:28rpx;line-height:1.8;color:#5F5557;}.notice-dismiss{margin-top:32rpx;background:#667E75;color:#fff;border-radius:20rpx;font-size:28rpx;}
|
||||||
|
|
||||||
|
/* 首页问候沿用手账字体与柔和玫瑰色,随正文一起滚动。 */
|
||||||
|
.home-greeting { margin:0 0 48rpx; padding:0 4rpx; }
|
||||||
|
.home-greeting-date { display:block; color:#9C747F; font-size:24rpx; line-height:1.6; letter-spacing:1rpx; }
|
||||||
|
.home-greeting-title { display:block; color:#A44E6B; font-size:44rpx; line-height:1.45; margin-top:8rpx; overflow-wrap:break-word; word-break:break-all; }
|
||||||
|
.home-greeting-note { display:block; color:#948179; font-size:25rpx; line-height:1.7; margin-top:10rpx; }
|
||||||
|
.home-has-background .home-greeting { background:rgba(255,253,248,.86); border-radius:24rpx; padding:24rpx 28rpx; }
|
||||||
|
@media (max-width:350px) { .home-greeting-title { font-size:38rpx; } }
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
const { withRecordAccess } = require('../../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||||
const { withDataCache } = require('../../utils/page-cache');
|
const { withDataCache } = require('../../utils/page-cache');
|
||||||
const { getLayout } = require('../../utils/layout');
|
const { getLayout } = require('../../utils/layout');
|
||||||
// pages/lovewords/lovewords.js - 一起听过的歌
|
// pages/lovewords/lovewords.js - 一起听过的歌
|
||||||
const { api } = require('../../utils/request');
|
const { api } = require('../../utils/request');
|
||||||
Page(withDataCache(withListPagination({
|
Page(withRecordAccess(withDataCache(withListPagination({
|
||||||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||||||
onResize: function() { this.setData(getLayout()); },
|
onResize: function() { this.setData(getLayout()); },
|
||||||
data: { loading: true, loadError: false,
|
data: { loading: true, loadError: false,
|
||||||
@@ -126,4 +127,4 @@ Page(withDataCache(withListPagination({
|
|||||||
goBack: function() { wx.navigateBack(); },
|
goBack: function() { wx.navigateBack(); },
|
||||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||||
}, 'loadData'), {"loadData": ["songsList"]}));
|
}, 'loadData'), {"loadData": ["songsList"]})));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<view capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
<view wx:if="{{!recordAccessBlocked}}" capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||||
<launch-transition />
|
<launch-transition />
|
||||||
<!-- pages/lovewords/lovewords.wxml - 一起听过的歌 -->
|
<!-- pages/lovewords/lovewords.wxml - 一起听过的歌 -->
|
||||||
<app-nav title="一起听的歌" back="{{true}}" />
|
<app-nav title="一起听的歌" back="{{true}}" />
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
const { withRecordAccess } = require('../../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const { getLayout } = require('../../utils/layout');
|
const { getLayout } = require('../../utils/layout');
|
||||||
// pages/period/period.js - 姨姨(美柚风格日历模式)
|
// pages/period/period.js - 姨姨(美柚风格日历模式)
|
||||||
const { api } = require('../../utils/request');
|
const { api } = require('../../utils/request');
|
||||||
|
|
||||||
|
|
||||||
Page({
|
Page(withRecordAccess({
|
||||||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||||||
onResize: function() { this.setData(getLayout()); },
|
onResize: function() { this.setData(getLayout()); },
|
||||||
data: {
|
data: {
|
||||||
@@ -1680,4 +1681,4 @@ Page({
|
|||||||
goBack: function() { wx.navigateBack(); },
|
goBack: function() { wx.navigateBack(); },
|
||||||
goHome: function() { wx.redirectTo({ url: '/pages/index/index' }); },
|
goHome: function() { wx.redirectTo({ url: '/pages/index/index' }); },
|
||||||
goProfile: function() { wx.redirectTo({ url: '/pages/profile/profile' }); }
|
goProfile: function() { wx.redirectTo({ url: '/pages/profile/profile' }); }
|
||||||
});
|
}));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<view capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
<view wx:if="{{!recordAccessBlocked}}" capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||||
<launch-transition />
|
<launch-transition />
|
||||||
<!-- pages/period/period.wxml - 姨姨(美柚风格日历模式) -->
|
<!-- pages/period/period.wxml - 姨姨(美柚风格日历模式) -->
|
||||||
<app-nav title="姨姨" back="{{true}}" /><view class="period-loading" wx:if="{{accessLoading}}"><page-state kind="loading" title="正在加载姨姨记录" /></view>
|
<app-nav title="姨姨" back="{{true}}" /><view class="period-loading" wx:if="{{accessLoading}}"><page-state kind="loading" title="正在加载姨姨记录" /></view>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
const { withRecordAccess } = require('../../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const { withDataCache } = require('../../utils/page-cache');
|
const { withDataCache } = require('../../utils/page-cache');
|
||||||
const { getLayout } = require('../../utils/layout');
|
const { getLayout } = require('../../utils/layout');
|
||||||
// pages/pet/achievements.js
|
// pages/pet/achievements.js
|
||||||
const { api } = require('../../utils/request');
|
const { api } = require('../../utils/request');
|
||||||
|
|
||||||
Page(withDataCache({
|
Page(withRecordAccess(withDataCache({
|
||||||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||||||
onResize: function() { this.setData(getLayout()); },
|
onResize: function() { this.setData(getLayout()); },
|
||||||
data: { loading: true, loadError: false,
|
data: { loading: true, loadError: false,
|
||||||
@@ -63,4 +64,4 @@ Page(withDataCache({
|
|||||||
goBack: function() {
|
goBack: function() {
|
||||||
wx.navigateBack();
|
wx.navigateBack();
|
||||||
}
|
}
|
||||||
}, {"loadAchievements": ["petAchievements"]}));
|
}, {"loadAchievements": ["petAchievements"]})));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<view capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
<view wx:if="{{!recordAccessBlocked}}" capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||||
<launch-transition />
|
<launch-transition />
|
||||||
<!-- pages/pet/achievements.wxml -->
|
<!-- pages/pet/achievements.wxml -->
|
||||||
<app-nav title="宠物成就" back="{{true}}" />
|
<app-nav title="宠物成就" back="{{true}}" />
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { withRecordAccess } = require('../../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const { withDataCache } = require('../../utils/page-cache');
|
const { withDataCache } = require('../../utils/page-cache');
|
||||||
const { getLayout } = require('../../utils/layout');
|
const { getLayout } = require('../../utils/layout');
|
||||||
@@ -20,7 +21,7 @@ const BREED_EMOJIS = {
|
|||||||
|
|
||||||
const STAGE_NAMES = ['', '幼犬期', '少年期', '青年期', '成年期', '终极期'];
|
const STAGE_NAMES = ['', '幼犬期', '少年期', '青年期', '成年期', '终极期'];
|
||||||
|
|
||||||
Page(withDataCache({
|
Page(withRecordAccess(withDataCache({
|
||||||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||||||
onResize: function() { this.setData(getLayout()); },
|
onResize: function() { this.setData(getLayout()); },
|
||||||
data: { loading: true, loadError: false, needsBinding: false,
|
data: { loading: true, loadError: false, needsBinding: false,
|
||||||
@@ -324,4 +325,4 @@ Page(withDataCache({
|
|||||||
},
|
},
|
||||||
|
|
||||||
stopPropagation: function() {}
|
stopPropagation: function() {}
|
||||||
}, {"loadPetInfo": ["coupleInfo", "petInfo"]}));
|
}, {"loadPetInfo": ["coupleInfo", "petInfo"]})));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<view capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
<view wx:if="{{!recordAccessBlocked}}" capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||||
<launch-transition />
|
<launch-transition />
|
||||||
<!-- pages/pet/pet.wxml -->
|
<!-- pages/pet/pet.wxml -->
|
||||||
<app-nav title="宠物养成" back="{{true}}" />
|
<app-nav title="宠物养成" back="{{true}}" />
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { requireRecordAccess, hasRecordAccess } = require('../../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const { uploadReference } = require('../../utils/file-media');
|
const { uploadReference } = require('../../utils/file-media');
|
||||||
const { itemsOf, totalOf } = require('../../utils/list-pagination');
|
const { itemsOf, totalOf } = require('../../utils/list-pagination');
|
||||||
@@ -163,7 +164,8 @@ Page(withDataCache({
|
|||||||
this._editSwipe = null;
|
this._editSwipe = null;
|
||||||
const point = e.changedTouches && e.changedTouches[0];
|
const point = e.changedTouches && e.changedTouches[0];
|
||||||
if (!start || !point) return;
|
if (!start || !point) return;
|
||||||
const dx = start.x - point.clientX, dy = Math.abs(start.y - point.clientY);
|
// 从左往右滑动返回;纵向滚动、反向滑动与输入框操作不处理。
|
||||||
|
const dx = point.clientX - start.x, dy = Math.abs(start.y - point.clientY);
|
||||||
if (dx < 80 || dx <= dy * 1.5) return;
|
if (dx < 80 || dx <= dy * 1.5) return;
|
||||||
if (this.data.showEditPartnerModal && (this.data.partnerSaving || this.data.partnerAvatarUploading)) return;
|
if (this.data.showEditPartnerModal && (this.data.partnerSaving || this.data.partnerAvatarUploading)) return;
|
||||||
if (this.data.showEditProfileModal) this.hideEditProfileModal();
|
if (this.data.showEditProfileModal) this.hideEditProfileModal();
|
||||||
@@ -1087,11 +1089,16 @@ Page(withDataCache({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
goStorage: function() {
|
goStorage: function() {
|
||||||
|
if (!requireRecordAccess()) return;
|
||||||
this._savePosBeforeLeave();
|
this._savePosBeforeLeave();
|
||||||
wx.navigateTo({ url: '/pages/storage/storage' });
|
wx.navigateTo({ url: '/pages/storage/storage' });
|
||||||
},
|
},
|
||||||
|
|
||||||
loadProfileStats: async function() {
|
loadProfileStats: async function() {
|
||||||
|
if (!hasRecordAccess()) {
|
||||||
|
this.setData({statisticItems:this.data.statisticItems.map(item=>({...item,count:item.key==='pet'?'未领养':0})),statsLoading:false,statsError:false});
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.setData({statsLoading:true,statsError:false});
|
this.setData({statsLoading:true,statsError:false});
|
||||||
const keys=['timeline','album','diary','songs','checkin','pet'];
|
const keys=['timeline','album','diary','songs','checkin','pet'];
|
||||||
const results=await Promise.allSettled([
|
const results=await Promise.allSettled([
|
||||||
@@ -1115,12 +1122,13 @@ Page(withDataCache({
|
|||||||
const routes={timeline:'timeline',album:'albums',diary:'diary',songs:'lovewords',checkin:'punch',pet:'pet',period:'period',settings:'settings'};
|
const routes={timeline:'timeline',album:'albums',diary:'diary',songs:'lovewords',checkin:'punch',pet:'pet',period:'period',settings:'settings'};
|
||||||
const route=routes[e.currentTarget.dataset.key];
|
const route=routes[e.currentTarget.dataset.key];
|
||||||
if(!route)return;
|
if(!route)return;
|
||||||
|
if(!requireRecordAccess())return;
|
||||||
this._savePosBeforeLeave();
|
this._savePosBeforeLeave();
|
||||||
wx.navigateTo({url:'/pages/'+route+'/'+route});
|
wx.navigateTo({url:'/pages/'+route+'/'+route});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 页面跳转
|
// 页面跳转
|
||||||
manageRelationship: function() { wx.navigateTo({ url: '/pages/unbind/unbind' }); },
|
manageRelationship: function() { wx.navigateTo({ url: '/pages/unbind/unbind' }); },
|
||||||
goToSettings: function() { this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/settings/settings' }); },
|
goToSettings: function() { if (!requireRecordAccess()) return; this._savePosBeforeLeave(); wx.navigateTo({ url: '/pages/settings/settings' }); },
|
||||||
goHome: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/index/index' }); }
|
goHome: function() { this._savePosBeforeLeave(); wx.switchTab({ url: '/pages/index/index' }); }
|
||||||
}, {"loadData": ["coupleInfo"], "loadProfileStats": ["timelineList","albumList","diaryList","songsList","checkinList","petInfo"], "loadStorageInfo": ["storageInfo"]}));
|
}, {"loadData": ["coupleInfo"], "loadProfileStats": ["timelineList","albumList","diaryList","songsList","checkinList","petInfo"], "loadStorageInfo": ["storageInfo"]}));
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { withRecordAccess } = require('../../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const { uploadReference } = require('../../utils/file-media');
|
const { uploadReference } = require('../../utils/file-media');
|
||||||
const { getLayout } = require('../../utils/layout');
|
const { getLayout } = require('../../utils/layout');
|
||||||
@@ -17,7 +18,7 @@ var TYPE_CONFIG = {
|
|||||||
checkin: { title: '添加打卡', btn: '保存打卡', api: 'checkinAdd' }
|
checkin: { title: '添加打卡', btn: '保存打卡', api: 'checkinAdd' }
|
||||||
};
|
};
|
||||||
|
|
||||||
Page({
|
Page(withRecordAccess({
|
||||||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||||||
onResize: function() { this.setData(getLayout()); },
|
onResize: function() { this.setData(getLayout()); },
|
||||||
data: {
|
data: {
|
||||||
@@ -479,4 +480,4 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
goBack: function() { wx.navigateBack(); }
|
goBack: function() { wx.navigateBack(); }
|
||||||
});
|
}));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<view capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
<view wx:if="{{!recordAccessBlocked}}" capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||||
<launch-transition />
|
<launch-transition />
|
||||||
<!-- pages/publish/publish.wxml - 统一发布页(增强版编辑器) -->
|
<!-- pages/publish/publish.wxml - 统一发布页(增强版编辑器) -->
|
||||||
<app-nav title="{{titleText}}" back="{{true}}" />
|
<app-nav title="{{titleText}}" back="{{true}}" />
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
|
const { withRecordAccess } = require('../../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||||
const { withDataCache } = require('../../utils/page-cache');
|
const { withDataCache } = require('../../utils/page-cache');
|
||||||
const { getLayout } = require('../../utils/layout');
|
const { getLayout } = require('../../utils/layout');
|
||||||
// pages/punch/punch.js - 情侣打卡
|
// pages/punch/punch.js - 情侣打卡
|
||||||
const { api, getImageUrl } = require('../../utils/request');
|
const { api, getImageUrl } = require('../../utils/request');
|
||||||
Page(withDataCache(withListPagination({
|
Page(withRecordAccess(withDataCache(withListPagination({
|
||||||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||||||
onResize: function() { this.setData(getLayout()); },
|
onResize: function() { this.setData(getLayout()); },
|
||||||
data: { loadError: false,
|
data: { loadError: false,
|
||||||
checkins: [], smokingVisible: false, smokingKnown: false, smokingSaving: false, smokingError: false,
|
checkins: [], smokingVisible: true, smokingKnown: false, smokingError: false,
|
||||||
coupleInfo: null,
|
coupleInfo: null,
|
||||||
myAvatar: '', partnerAvatar: '',
|
myAvatar: '', partnerAvatar: '',
|
||||||
myName: '我', partnerName: 'TA',
|
myName: '我', partnerName: 'TA',
|
||||||
@@ -28,14 +29,7 @@ Page(withDataCache(withListPagination({
|
|||||||
try { const auth=await api.checkAuth(); this.setData({smokingVisible:Number(auth.user.smoking_card_visible)!==0, smokingKnown:true, smokingError:false}); }
|
try { const auth=await api.checkAuth(); this.setData({smokingVisible:Number(auth.user.smoking_card_visible)!==0, smokingKnown:true, smokingError:false}); }
|
||||||
catch(e) { this.setData({smokingError:true}); }
|
catch(e) { this.setData({smokingError:true}); }
|
||||||
},
|
},
|
||||||
toggleSmoking: async function(e) {
|
|
||||||
if (this.data.smokingSaving || !this.data.smokingKnown) return;
|
|
||||||
const previous=this.data.smokingVisible;
|
|
||||||
this.setData({smokingSaving:true});
|
|
||||||
try { const result=await api.smokingCardVisible({visible:e.detail.value?1:0}); this.setData({smokingVisible:Number(result.card_visible)===1}); }
|
|
||||||
catch(error) { this.setData({smokingVisible:previous}); wx.showToast({title:error.error||'保存失败,请重试',icon:'none'}); }
|
|
||||||
finally { this.setData({smokingSaving:false}); }
|
|
||||||
},
|
|
||||||
onSmokingUpdated: function() { this.loadData(); },
|
onSmokingUpdated: function() { this.loadData(); },
|
||||||
|
|
||||||
onPullDownRefresh: async function() {
|
onPullDownRefresh: async function() {
|
||||||
@@ -134,4 +128,4 @@ Page(withDataCache(withListPagination({
|
|||||||
goBack: function() { wx.navigateBack(); },
|
goBack: function() { wx.navigateBack(); },
|
||||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||||
}, 'loadData'), {"loadData": ["checkinList", "checkinStats", "coupleInfo"], "loadSmokingPreference":["checkAuth"], "loadCoupleInfo":["coupleInfo"]}));
|
}, 'loadData'), {"loadData": ["checkinList", "checkinStats", "coupleInfo"], "loadSmokingPreference":["checkAuth"], "loadCoupleInfo":["coupleInfo"]})));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<view capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
<view wx:if="{{!recordAccessBlocked}}" capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||||
<launch-transition />
|
<launch-transition />
|
||||||
<!-- pages/punch/punch.wxml - 情侣打卡 -->
|
<!-- pages/punch/punch.wxml - 情侣打卡 -->
|
||||||
<app-nav title="打卡" back="{{true}}" />
|
<app-nav title="打卡" back="{{true}}" />
|
||||||
@@ -25,7 +25,6 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="smoking-preference"><view><text class="j-small-title">抽烟记录</text><text class="j-caption">显示设置保存到账号,重新安装后仍生效</text></view><switch wx:if="{{smokingKnown}}" checked="{{smokingVisible}}" disabled="{{smokingSaving}}" color="#667E75" bindchange="toggleSmoking" /><text wx:elif="{{smokingError}}" bindtap="loadSmokingPreference">重试</text><text wx:else>加载中</text></view>
|
|
||||||
<smoking-card wx:if="{{smokingKnown && smokingVisible}}" id="smoking" bind:updated="onSmokingUpdated" />
|
<smoking-card wx:if="{{smokingKnown && smokingVisible}}" id="smoking" bind:updated="onSmokingUpdated" />
|
||||||
|
|
||||||
<!-- 打卡列表 -->
|
<!-- 打卡列表 -->
|
||||||
|
|||||||
@@ -13,5 +13,3 @@
|
|||||||
@import '../../styles/button-theme.wxss';
|
@import '../../styles/button-theme.wxss';
|
||||||
|
|
||||||
@import '../../styles/typography.wxss';
|
@import '../../styles/typography.wxss';
|
||||||
|
|
||||||
.smoking-preference { display:flex; align-items:center; justify-content:space-between; gap:20rpx; margin:26rpx 0; padding:24rpx; background:#F2F7F4; border-radius:24rpx; }
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const { guardStorageSpace } = require('../../utils/storage-guard');
|
|||||||
Page({
|
Page({
|
||||||
onResize: function() { this.setData(getLayout()); },
|
onResize: function() { this.setData(getLayout()); },
|
||||||
data: {
|
data: {
|
||||||
|
smokingVisible:true, smokingKnown:false, smokingSaving:false, smokingError:false,
|
||||||
showBgModal: false,
|
showBgModal: false,
|
||||||
bgPreview: '',
|
bgPreview: '',
|
||||||
bgFilePath: '',
|
bgFilePath: '',
|
||||||
@@ -41,6 +42,7 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onShow: async function() {
|
onShow: async function() {
|
||||||
|
this.loadSmokingPreference();
|
||||||
this.setData({ bindingKnown: false, bindingError:false });
|
this.setData({ bindingKnown: false, bindingError:false });
|
||||||
try {
|
try {
|
||||||
var info = await api.coupleInfo();
|
var info = await api.coupleInfo();
|
||||||
@@ -59,6 +61,19 @@ Page({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
loadSmokingPreference: async function() {
|
||||||
|
try { const auth=await api.checkAuth(); this.setData({smokingVisible:Number(auth.user.smoking_card_visible)!==0,smokingKnown:true,smokingError:false}); }
|
||||||
|
catch(error) { this.setData({smokingError:true}); }
|
||||||
|
},
|
||||||
|
toggleSmoking: async function(e) {
|
||||||
|
if (this.data.smokingSaving || !this.data.smokingKnown) return;
|
||||||
|
const previous=this.data.smokingVisible;
|
||||||
|
this.setData({smokingSaving:true});
|
||||||
|
try { const result=await api.smokingCardVisible({visible:e.detail.value?1:0}); this.setData({smokingVisible:Number(result.card_visible)===1}); }
|
||||||
|
catch(error) { this.setData({smokingVisible:previous}); wx.showToast({title:error.error||'保存失败,请重试',icon:'none'}); }
|
||||||
|
finally { this.setData({smokingSaving:false}); }
|
||||||
|
},
|
||||||
|
|
||||||
goBind: function() {
|
goBind: function() {
|
||||||
getApp().globalData.openProfileBind = true;
|
getApp().globalData.openProfileBind = true;
|
||||||
wx.switchTab({ url: '/pages/profile/profile' });
|
wx.switchTab({ url: '/pages/profile/profile' });
|
||||||
|
|||||||
@@ -24,6 +24,12 @@
|
|||||||
<text class="j-link">›</text>
|
<text class="j-link">›</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view class="j-profile-setting">
|
||||||
|
<view class="setting-info"><view class="j-small-title">抽烟记录</view><view class="j-caption">默认开启,关闭后不在打卡页显示,已有记录保留</view></view>
|
||||||
|
<switch wx:if="{{smokingKnown}}" checked="{{smokingVisible}}" disabled="{{smokingSaving}}" color="#667E75" bindchange="toggleSmoking" />
|
||||||
|
<text wx:elif="{{smokingError}}" class="j-link" bindtap="loadSmokingPreference">重试</text><text wx:else class="j-caption">加载中</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="section-heading">关系与账号</view>
|
<view class="section-heading">关系与账号</view>
|
||||||
<view class="j-profile-setting" bindtap="deleteAccount"><view class="setting-info"><view class="j-small-title" style="color:#B74768">注销账号</view><view class="j-caption">{{hasPartner ? '请先完成情侣解绑,再注销账号' : '确认后删除账号及全部个人数据'}}</view></view><text class="j-link">›</text></view>
|
<view class="j-profile-setting" bindtap="deleteAccount"><view class="setting-info"><view class="j-small-title" style="color:#B74768">注销账号</view><view class="j-caption">{{hasPartner ? '请先完成情侣解绑,再注销账号' : '确认后删除账号及全部个人数据'}}</view></view><text class="j-link">›</text></view>
|
||||||
<!-- 操作记录 -->
|
<!-- 操作记录 -->
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
const { withRecordAccess } = require('../../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const { getLayout } = require('../../utils/layout');
|
const { getLayout } = require('../../utils/layout');
|
||||||
Page({
|
Page(withRecordAccess({
|
||||||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||||||
data:{},
|
data:{},
|
||||||
onLoad(){this.setData(getLayout());},
|
onLoad(){this.setData(getLayout());},
|
||||||
onResize(){this.setData(getLayout());},
|
onResize(){this.setData(getLayout());},
|
||||||
loadMore(){const card=this.selectComponent('#smoking');if(card)return card.loadMoreLogs();},
|
loadMore(){const card=this.selectComponent('#smoking');if(card)return card.loadMoreLogs();},
|
||||||
async onPullDownRefresh(){try{const card=this.selectComponent('#smoking');if(card)await card.load(true);}finally{wx.stopPullDownRefresh();}}
|
async onPullDownRefresh(){try{const card=this.selectComponent('#smoking');if(card)await card.load(true);}finally{wx.stopPullDownRefresh();}}
|
||||||
});
|
}));
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<view capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}"><launch-transition /><app-nav title="抽烟记录" back="{{true}}" /><scroll-view class="page-bg" scroll-y bindscrolltolower="loadMore" lower-threshold="120" show-scrollbar="{{false}}"><view class="c-body"><smoking-card id="smoking" detailed="{{true}}" /></view></scroll-view></view>
|
<view wx:if="{{!recordAccessBlocked}}" capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}"><launch-transition /><app-nav title="抽烟记录" back="{{true}}" /><scroll-view class="page-bg" scroll-y bindscrolltolower="loadMore" lower-threshold="120" show-scrollbar="{{false}}"><view class="c-body"><smoking-card id="smoking" detailed="{{true}}" /></view></scroll-view></view>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { withRecordAccess } = require('../../utils/record-access');
|
||||||
const { withDataCache } = require('../../utils/page-cache');
|
const { withDataCache } = require('../../utils/page-cache');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const { getLayout } = require('../../utils/layout');
|
const { getLayout } = require('../../utils/layout');
|
||||||
@@ -10,7 +11,7 @@ function formatBytes(bytes) {
|
|||||||
return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB';
|
return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB';
|
||||||
}
|
}
|
||||||
|
|
||||||
Page(withDataCache({
|
Page(withRecordAccess(withDataCache({
|
||||||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||||||
onResize: function () { this.setData(getLayout()); },
|
onResize: function () { this.setData(getLayout()); },
|
||||||
|
|
||||||
@@ -160,4 +161,4 @@ Page(withDataCache({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
}, {loadInfo:['storageInfo']}));
|
}, {loadInfo:['storageInfo']})));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<view capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary j-handwritten-page" style="{{layoutStyle}}">
|
<view wx:if="{{!recordAccessBlocked}}" capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary j-handwritten-page" style="{{layoutStyle}}">
|
||||||
<launch-transition />
|
<launch-transition />
|
||||||
<!-- pages/storage/storage.wxml -->
|
<!-- pages/storage/storage.wxml -->
|
||||||
<app-nav title="存储空间" back="{{true}}" />
|
<app-nav title="存储空间" back="{{true}}" />
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { withRecordAccess } = require('../../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const { withDataCache } = require('../../utils/page-cache');
|
const { withDataCache } = require('../../utils/page-cache');
|
||||||
const { getLayout } = require('../../utils/layout');
|
const { getLayout } = require('../../utils/layout');
|
||||||
@@ -59,7 +60,7 @@ function calcNearestRepeatDate(eventDate, today, interval, unit) {
|
|||||||
return new Date(eventDate);
|
return new Date(eventDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
Page(withDataCache({
|
Page(withRecordAccess(withDataCache({
|
||||||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||||||
onResize: function() { this.setData(getLayout()); },
|
onResize: function() { this.setData(getLayout()); },
|
||||||
data: { loading: true, loadError: false,
|
data: { loading: true, loadError: false,
|
||||||
@@ -207,4 +208,4 @@ Page(withDataCache({
|
|||||||
goBack: function() { wx.navigateBack(); },
|
goBack: function() { wx.navigateBack(); },
|
||||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||||
}, {"loadDetail": ["timelineDetail", "coupleInfo"]}));
|
}, {"loadDetail": ["timelineDetail", "coupleInfo"]})));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<view capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary {{bgImageUrl ? 'detail-immersive' : ''}}" style="{{layoutStyle}}">
|
<view wx:if="{{!recordAccessBlocked}}" capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary {{bgImageUrl ? 'detail-immersive' : ''}}" style="{{layoutStyle}}">
|
||||||
<launch-transition />
|
<launch-transition />
|
||||||
<!-- pages/timeline-detail/timeline-detail.wxml -->
|
<!-- pages/timeline-detail/timeline-detail.wxml -->
|
||||||
<page-meta page-style="{{bgImageUrl ? 'background: transparent;' : ''}}"></page-meta>
|
<page-meta page-style="{{bgImageUrl ? 'background: transparent;' : ''}}"></page-meta>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { withRecordAccess } = require('../../utils/record-access');
|
||||||
const { tryLoginRenewal } = require('../../utils/subscribe');
|
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||||||
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
const { itemsOf, startList, applyList, failList, finishList, withListPagination } = require('../../utils/list-pagination');
|
||||||
const { withDataCache } = require('../../utils/page-cache');
|
const { withDataCache } = require('../../utils/page-cache');
|
||||||
@@ -58,7 +59,7 @@ function calcNearestRepeatDate(eventDate, today, interval, unit) {
|
|||||||
return new Date(eventDate);
|
return new Date(eventDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
Page(withDataCache(withListPagination({
|
Page(withRecordAccess(withDataCache(withListPagination({
|
||||||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||||||
onResize: function() { this.setData(getLayout()); },
|
onResize: function() { this.setData(getLayout()); },
|
||||||
data: { loading: true, loadError: false,
|
data: { loading: true, loadError: false,
|
||||||
@@ -194,4 +195,4 @@ Page(withDataCache(withListPagination({
|
|||||||
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
goHome: function() { wx.switchTab({ url: '/pages/index/index' }); },
|
||||||
goBack: function() { wx.navigateBack(); },
|
goBack: function() { wx.navigateBack(); },
|
||||||
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
goProfile: function() { wx.switchTab({ url: '/pages/profile/profile' }); }
|
||||||
}, 'loadData'), {"loadData": ["timelineList", "coupleInfo"]}));
|
}, 'loadData'), {"loadData": ["timelineList", "coupleInfo"]})));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<view capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
<view wx:if="{{!recordAccessBlocked}}" capture-bind:tap="onLoginSubscribeTap" class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||||
<launch-transition />
|
<launch-transition />
|
||||||
<!-- pages/timeline/timeline.wxml - 左右交替记忆日 -->
|
<!-- pages/timeline/timeline.wxml - 左右交替记忆日 -->
|
||||||
<app-nav title="记忆日" back="{{true}}" />
|
<app-nav title="记忆日" back="{{true}}" />
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ function page(name,api,wxExtra={},app={}){let definition;const storage=new Map([
|
|||||||
const flush=async()=>{for(let i=0;i<30;i++)await Promise.resolve();};
|
const flush=async()=>{for(let i=0;i<30;i++)await Promise.resolve();};
|
||||||
test('smoking preference is restored from server on a newly created page, even without local storage',async()=>{
|
test('smoking preference is restored from server on a newly created page, even without local storage',async()=>{
|
||||||
let visible=0;const api={checkAuth:async()=>({user:{smoking_card_visible:visible}}),smokingCardVisible:async d=>{visible=d.visible;return {card_visible:visible};}};
|
let visible=0;const api={checkAuth:async()=>({user:{smoking_card_visible:visible}}),smokingCardVisible:async d=>{visible=d.visible;return {card_visible:visible};}};
|
||||||
const first=page('punch/punch.js',api);await first.p.loadSmokingPreference();assert.equal(first.p.data.smokingVisible,false);await first.p.toggleSmoking({detail:{value:true}});assert.equal(visible,1);
|
const first=page('settings/settings.js',api);await first.p.loadSmokingPreference();assert.equal(first.p.data.smokingVisible,false);await first.p.toggleSmoking({detail:{value:true}});assert.equal(visible,1);
|
||||||
const installed=page('punch/punch.js',api);await installed.p.loadSmokingPreference();assert.equal(installed.p.data.smokingVisible,true);assert.equal(installed.storage.has('smokingVisible'),false);
|
const installed=page('punch/punch.js',api);await installed.p.loadSmokingPreference();assert.equal(installed.p.data.smokingVisible,true);assert.equal(installed.storage.has('smokingVisible'),false);
|
||||||
});
|
});
|
||||||
test('failed smoking preference save retains the last server state',async()=>{
|
test('failed smoking preference save retains the last server state',async()=>{
|
||||||
const e=page('punch/punch.js',{checkAuth:async()=>({user:{smoking_card_visible:0}}),smokingCardVisible:async()=>{throw {error:'offline'};}});await e.p.loadSmokingPreference();await e.p.toggleSmoking({detail:{value:true}});assert.equal(e.p.data.smokingVisible,false);assert.equal(e.p.data.smokingSaving,false);
|
const e=page('settings/settings.js',{checkAuth:async()=>({user:{smoking_card_visible:0}}),smokingCardVisible:async()=>{throw {error:'offline'};}});await e.p.loadSmokingPreference();await e.p.toggleSmoking({detail:{value:true}});assert.equal(e.p.data.smokingVisible,false);assert.equal(e.p.data.smokingSaving,false);
|
||||||
});
|
});
|
||||||
test('announcement is dismissible per revision and server disable immediately hides it',async()=>{
|
test('announcement is dismissible per revision and server disable immediately hides it',async()=>{
|
||||||
let data={enabled:true,title:'公告',content:'Hello',revision:'v1'};const e=page('index/index.js',{announcement:async()=>data});await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,true);e.p.closeAnnouncement();await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,false);data={...data,content:'Updated',revision:'v2'};await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,true);data={...data,enabled:false};await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,false);
|
let data={enabled:true,title:'公告',content:'Hello',revision:'v1'};const e=page('index/index.js',{announcement:async()=>data});await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,true);e.p.closeAnnouncement();await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,false);data={...data,content:'Updated',revision:'v2'};await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,true);data={...data,enabled:false};await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,false);
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ const test = require('node:test');
|
|||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const loader = require('./load-module.cjs');
|
const loader = require('./load-module.cjs');
|
||||||
test('development requests and file previews use local backend, trial and release use production', () => {
|
test('requests and file previews use the manually configured API base in every runtime', () => {
|
||||||
for (const version of ['develop','trial','release']) {
|
for (const version of ['develop','trial','release']) {
|
||||||
const wx = {getAccountInfoSync:()=>({miniProgram:{envVersion:version}})};
|
const wx = {getAccountInfoSync:()=>({miniProgram:{envVersion:version}})};
|
||||||
const load = loader({wx,console});
|
const load = loader({wx,console});
|
||||||
const env = load(path.join(__dirname,'../config/env.js'));
|
const configured = load(path.join(__dirname,'../config/env.js'));
|
||||||
const {API_BASE,getImageUrl} = load(path.join(__dirname,'../utils/request.js'));
|
const {API_BASE,getImageUrl} = load(path.join(__dirname,'../utils/request.js'));
|
||||||
assert.equal(env.env, version === 'develop' ? 'dev' : version);
|
assert.deepEqual(Object.keys(configured),['API_BASE']);
|
||||||
assert.equal(API_BASE, version === 'develop' ? 'http://127.0.0.1:8080/api' : 'https://we.sundynix.cn/api');
|
assert.equal(API_BASE,require('../config/env').API_BASE);
|
||||||
assert.equal(getImageUrl('/api/files/6083e119-6b26-441b-8b70-44d224366238'),API_BASE+'/files/6083e119-6b26-441b-8b70-44d224366238');
|
assert.equal(getImageUrl('/api/files/6083e119-6b26-441b-8b70-44d224366238'),API_BASE+'/files/6083e119-6b26-441b-8b70-44d224366238');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ test('duplicate and ID-only uploads keep stable display URLs and file metadata',
|
|||||||
assert.equal(uploadReference(result),id);
|
assert.equal(uploadReference(result),id);
|
||||||
assert.equal(result.duplicate,true);
|
assert.equal(result.duplicate,true);
|
||||||
assert.equal(result.file.url,'https://minio/raw-object');
|
assert.equal(result.file.url,'https://minio/raw-object');
|
||||||
assert.equal(c.getImageUrl(result.path),'https://we.sundynix.cn/api/files/'+id);
|
assert.equal(c.getImageUrl(result.path),c.API_BASE+'/files/'+id);
|
||||||
assert.throws(()=>uploadReference({success:true,path:'http://tmp/new.jpg'}),/文件 ID/);
|
assert.throws(()=>uploadReference({success:true,path:'http://tmp/new.jpg'}),/文件 ID/);
|
||||||
});
|
});
|
||||||
test('diary edit uploads HTTP temporary images, retains existing IDs and stops on upload failure', async () => {
|
test('diary edit uploads HTTP temporary images, retains existing IDs and stops on upload failure', async () => {
|
||||||
@@ -56,5 +56,5 @@ test('media display keeps local assets and temporary paths without a backend dir
|
|||||||
assert.equal(c.getImageUrl('/images/default-avatar.png'), '/images/default-avatar.png');
|
assert.equal(c.getImageUrl('/images/default-avatar.png'), '/images/default-avatar.png');
|
||||||
assert.equal(c.getImageUrl('wxfile://tmp/photo.jpg'), 'wxfile://tmp/photo.jpg');
|
assert.equal(c.getImageUrl('wxfile://tmp/photo.jpg'), 'wxfile://tmp/photo.jpg');
|
||||||
assert.equal(c.getImageUrl('http://tmp/photo.jpg'), 'http://tmp/photo.jpg');
|
assert.equal(c.getImageUrl('http://tmp/photo.jpg'), 'http://tmp/photo.jpg');
|
||||||
assert.equal(c.getImageUrl(id), 'https://we.sundynix.cn/api/files/' + id);
|
assert.equal(c.getImageUrl(id), c.API_BASE+'/files/' + id);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ function records(api) {
|
|||||||
Page:p=>definition=p, console:{error(){}}, setTimeout:f=>f(),
|
Page:p=>definition=p, console:{error(){}}, setTimeout:f=>f(),
|
||||||
wx:{getStorageSync:()=> 'token',navigateTo:p=>routes.push(p.url)},
|
wx:{getStorageSync:()=> 'token',navigateTo:p=>routes.push(p.url)},
|
||||||
require(name) {
|
require(name) {
|
||||||
|
if(name.endsWith('/record-access'))return {guardRecordEntries:p=>p,withRecordAccess:p=>p};
|
||||||
if(name.endsWith('/list-pagination'))return require('../utils/list-pagination');
|
if(name.endsWith('/list-pagination'))return require('../utils/list-pagination');
|
||||||
if(name.endsWith('/page-cache'))return {withDataCache:p=>p};
|
if(name.endsWith('/page-cache'))return {withDataCache:p=>p};
|
||||||
if(name.endsWith('/request'))return {api};
|
if(name.endsWith('/request'))return {api};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ test('native custom tabs retain the three main pages; pages never mount a duplic
|
|||||||
test('component selects the main page, hides on child routes and avoids reloading its current tab',()=>{
|
test('component selects the main page, hides on child routes and avoids reloading its current tab',()=>{
|
||||||
let definition,route='pages/profile/profile',pageData={};const navigations=[];
|
let definition,route='pages/profile/profile',pageData={};const navigations=[];
|
||||||
vm.runInNewContext(fs.readFileSync(path.join(root,'custom-tab-bar/index.js'),'utf8'),{
|
vm.runInNewContext(fs.readFileSync(path.join(root,'custom-tab-bar/index.js'),'utf8'),{
|
||||||
Component:c=>definition=c,require:name=>name.includes('subscribe')?{tryLoginRenewal(){}}:({tabForPage,paths}),getCurrentPages:()=>[{route,data:pageData}],wx:{switchTab:o=>{navigations.push(o.url);o.complete();},reLaunch(){assert.fail('normal tab switching must retain pages');}}
|
Component:c=>definition=c,require:name=>name.includes('record-access')?{requireRecordAccess:()=>true}:name.includes('subscribe')?{tryLoginRenewal(){}}:({tabForPage,paths}),getCurrentPages:()=>[{route,data:pageData}],wx:{switchTab:o=>{navigations.push(o.url);o.complete();},reLaunch(){assert.fail('normal tab switching must retain pages');}}
|
||||||
});
|
});
|
||||||
const component={...definition.methods,data:{...definition.data},setData(v){Object.assign(this.data,v);}};
|
const component={...definition.methods,data:{...definition.data},setData(v){Object.assign(this.data,v);}};
|
||||||
definition.lifetimes.attached.call(component);assert.equal(component.data.active,'profile');
|
definition.lifetimes.attached.call(component);assert.equal(component.data.active,'profile');
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ test('server relation changes discard cached data before another read',async()=>
|
|||||||
await api.diaryList();await api.diaryList();assert.equal(reads,1);
|
await api.diaryList();await api.diaryList();assert.equal(reads,1);
|
||||||
await api.checkAuth();assert.equal(storage.get('coupleId'),'22');assert.equal(redirects,1);
|
await api.checkAuth();assert.equal(storage.get('coupleId'),'22');assert.equal(redirects,1);
|
||||||
const fresh=await api.diaryList();assert.equal(reads,2);assert.equal(fresh.items[0].title,'new space');
|
const fresh=await api.diaryList();assert.equal(reads,2);assert.equal(fresh.items[0].title,'new space');
|
||||||
assert.equal(getImageUrl('/api/files/6083e119-6b26-441b-8b70-44d224366238'),'https://we.sundynix.cn/api/files/6083e119-6b26-441b-8b70-44d224366238');
|
assert.equal(getImageUrl('/api/files/6083e119-6b26-441b-8b70-44d224366238'),require('../config/env').API_BASE+'/files/6083e119-6b26-441b-8b70-44d224366238');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('settings and profile consent retain couple cache and lead into cooling',async()=>{
|
test('settings and profile consent retain couple cache and lead into cooling',async()=>{
|
||||||
|
|||||||
@@ -7,13 +7,17 @@ function setup(partner=false){
|
|||||||
}
|
}
|
||||||
const start=(x,y,ignore=false)=>({touches:[{clientX:x,clientY:y}],target:{dataset:{swipeIgnore:ignore}}});
|
const start=(x,y,ignore=false)=>({touches:[{clientX:x,clientY:y}],target:{dataset:{swipeIgnore:ignore}}});
|
||||||
const end=(x,y)=>({changedTouches:[{clientX:x,clientY:y}]});
|
const end=(x,y)=>({changedTouches:[{clientX:x,clientY:y}]});
|
||||||
test('left swipe closes either editor once, restores tabs and never navigates away from profile',()=>{
|
test('leftward swipe keeps either editor open',()=>{
|
||||||
for(const partner of [false,true]){const {p,calls}=setup(partner);p.onEditTouchStart(start(280,300));p.onEditTouchEnd(end(100,310));p.onEditTouchEnd(end(100,310));assert.equal(p.data.showEditProfileModal,false);assert.equal(p.data.showEditPartnerModal,false);assert.deepEqual(calls,[false,'keyboard']);}
|
for(const partner of [false,true]){const {p,calls}=setup(partner);p.onEditTouchStart(start(280,300));p.onEditTouchEnd(end(100,310));assert.equal(partner?p.data.showEditPartnerModal:p.data.showEditProfileModal,true);assert.deepEqual(calls,[]);}
|
||||||
});
|
});
|
||||||
test('vertical scrolling, short or rightward gestures and text selection keep the editor open',()=>{
|
test('vertical scrolling, short gestures and text selection keep the editor open',()=>{
|
||||||
for(const [s,e] of [[start(280,300),end(240,305)],[start(280,300),end(160,500)],[start(100,300),end(280,310)],[start(280,300,true),end(100,310)]]){const {p,calls}=setup();p.onEditTouchStart(s);p.onEditTouchEnd(e);assert.equal(p.data.showEditProfileModal,true);assert.equal(calls.length,0);}
|
for(const [s,e] of [[start(40,300),end(80,305)],[start(40,300),end(160,500)],[start(40,300,true),end(180,310)]]){const {p,calls}=setup();p.onEditTouchStart(s);p.onEditTouchEnd(e);assert.equal(p.data.showEditProfileModal,true);assert.equal(calls.length,0);}
|
||||||
});
|
});
|
||||||
test('cancelled touches and partner saving never dismiss the editor',()=>{
|
test('cancelled touches and partner saving never dismiss the editor',()=>{
|
||||||
const {p}=setup();p.onEditTouchStart(start(280,300));p.onEditTouchCancel();p.onEditTouchEnd(end(100,300));assert.equal(p.data.showEditProfileModal,true);
|
const {p}=setup();p.onEditTouchStart(start(12,300));p.onEditTouchCancel();p.onEditTouchEnd(end(160,300));assert.equal(p.data.showEditProfileModal,true);
|
||||||
const e=setup(true);e.p.data.partnerSaving=true;e.p.onEditTouchStart(start(280,300));e.p.onEditTouchEnd(end(100,300));assert.equal(e.p.data.showEditPartnerModal,true);
|
const e=setup(true);e.p.data.partnerSaving=true;e.p.onEditTouchStart(start(12,300));e.p.onEditTouchEnd(end(160,300));assert.equal(e.p.data.showEditPartnerModal,true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('right swipe closes either editor exactly once without navigating the tab page',()=>{
|
||||||
|
for(const partner of [false,true]){const {p,calls}=setup(partner);p.onEditTouchStart(start(12,300));p.onEditTouchEnd(end(160,310));p.onEditTouchEnd(end(160,310));assert.equal(p.data.showEditProfileModal,false);assert.equal(p.data.showEditPartnerModal,false);assert.deepEqual(calls,[false,'keyboard']);}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
const test=require('node:test'), assert=require('node:assert/strict'), path=require('node:path');
|
||||||
|
const createLoader=require('./load-module.cjs');
|
||||||
|
const root=path.join(__dirname,'..');
|
||||||
|
function env(cid='') {
|
||||||
|
const modals=[],routes=[],storage={coupleId:cid};let page,component;
|
||||||
|
const app={globalData:{},ensureAuth:async()=>{}};
|
||||||
|
const wx={getStorageSync:k=>storage[k],removeStorageSync:k=>delete storage[k],showModal:o=>modals.push(o),switchTab:o=>routes.push(o.url),navigateTo:o=>routes.push(o.url),showToast(){}};
|
||||||
|
const load=createLoader({wx,console,Date,Promise,setTimeout,clearTimeout,setInterval,clearInterval,getApp:()=>app,Page:p=>page=p,Component:c=>component=c,getCurrentPages:()=>[{route:'pages/index/index',data:{}}]}, {'../../utils/request':{api:{},getImageUrl:v=>v},'../../utils/page-cache':{withDataCache:p=>p}});
|
||||||
|
return {wx,modals,routes,storage,app,load,page:()=>({...page,data:{...page.data},setData(v){Object.assign(this.data,v)}}),component:()=>component};
|
||||||
|
}
|
||||||
|
test('unbound entry modal is shared and confirmation opens the existing invitation panel',()=>{
|
||||||
|
const e=env(),guard=e.load(path.join(root,'utils/record-access.js'));
|
||||||
|
assert.equal(guard.requireRecordAccess(),false);guard.requireRecordAccess();assert.equal(e.modals.length,1);
|
||||||
|
const modal=e.modals[0];assert.equal(modal.confirmText,'立即邀请');modal.success({confirm:true});modal.complete();
|
||||||
|
assert.equal(e.app.globalData.openProfileBind,true);assert.deepEqual(e.routes,['/pages/profile/profile']);
|
||||||
|
e.storage.coupleId='couple';assert.equal(guard.requireRecordAccess(),true);
|
||||||
|
});
|
||||||
|
test('homepage record links and profile feature shortcuts reject unbound users before navigation',()=>{
|
||||||
|
for(const [file,methods] of [['index/index',['showMore','writeDiary','goToAlbum','goToCheckin']],['profile/profile',['openStatistic']]]) {
|
||||||
|
for(const method of methods){const e=env();e.load(path.join(root,'pages',file+'.js'));const p=e.page();p[method]({currentTarget:{dataset:{key:'diary'}}});assert.equal(e.routes.length,0);assert.equal(e.modals.length,1,method);}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('record tab rejects unbound navigation and allows bound navigation',()=>{
|
||||||
|
for(const cid of ['', 'couple']){const e=env(cid);e.load(path.join(root,'custom-tab-bar/index.js'));const c=e.component();const p={...c.methods,setData(){}};p.navigate({currentTarget:{dataset:{tab:'records'}}});assert.equal(e.routes.length,cid?1:0);assert.equal(e.modals.length,cid?0:1);}
|
||||||
|
});
|
||||||
|
test('all protected direct links avoid loading records when unbound and cancellation returns home',async()=>{
|
||||||
|
const pages=['discover/discover','timeline/timeline','punch/punch','diary/diary','albums/albums','album/album','lovewords/lovewords','publish/publish','checkin-detail/checkin-detail','timeline-detail/timeline-detail','period/period','diary_detail/diary_detail','diary_edit/diary_edit','pet/pet','pet/achievements','smoking-detail/smoking-detail','storage/storage'];
|
||||||
|
for(const file of pages){const e=env();e.load(path.join(root,'pages',file+'.js'));const p=e.page();await p.onLoad({});await p.onShow();assert.equal(p.data.recordAccessBlocked,true,file);assert.equal(e.modals.length,1,file);e.modals[0].success({cancel:true});e.modals[0].complete();assert.deepEqual(e.routes,['/pages/index/index']);}
|
||||||
|
});
|
||||||
|
test('cold direct entry waits for login before deciding binding and loading the page',async()=>{
|
||||||
|
const e=env();let loaded=0,shown=0,finish;
|
||||||
|
e.app.ensureAuth=()=>new Promise(resolve=>finish=()=>{e.storage.coupleId='bound';resolve();});
|
||||||
|
const guard=e.load(path.join(root,'utils/record-access.js'));
|
||||||
|
const p=guard.withRecordAccess({onLoad(){loaded++;},onShow(){shown++;},setData(v){this.data={...this.data,...v};}});
|
||||||
|
const load=p.onLoad(),show=p.onShow();assert.equal(loaded,0);finish();await Promise.all([load,show]);assert.equal(loaded,1);assert.equal(shown,1);assert.equal(p.data.recordAccessBlocked,false);assert.equal(e.modals.length,0);
|
||||||
|
e.storage.coupleId='';await p.onShow();assert.equal(shown,1);assert.equal(e.modals.length,1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('authoritative backend denial clears stale binding and offers invitation',async()=>{
|
||||||
|
const e=env('stale-couple');
|
||||||
|
e.wx.request=o=>o.success({statusCode:403,data:{error:'请先绑定情侣,再使用记录功能',code:10002}});
|
||||||
|
const {api}=e.load(path.join(root,'utils/request.js'));
|
||||||
|
await assert.rejects(api.diaryAdd({content:'blocked'}),error=>error.code===10002);
|
||||||
|
assert.equal(e.storage.coupleId,undefined);assert.equal(e.modals.length,1);assert.equal(e.routes.length,0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('more and storage entries require binding while bound navigation stays available',()=>{
|
||||||
|
for(const cid of ['', 'couple']) {
|
||||||
|
for(const method of ['openStatistic','goStorage','goToSettings']) {
|
||||||
|
const e=env(cid);e.load(path.join(root,'pages/profile/profile.js'));const p=e.page();
|
||||||
|
p[method]({currentTarget:{dataset:{key:'settings'}}});
|
||||||
|
assert.equal(e.modals.length,cid?0:1,method);assert.equal(e.routes.length,cid?1:0,method);
|
||||||
|
if(cid) assert.equal(e.routes[0],method==='goStorage'?'/pages/storage/storage':'/pages/settings/settings');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -12,9 +12,9 @@ test('GET business parameters and POST bodies use clean paths with auth', async
|
|||||||
const response = await api.diaryList({page:2,page_size:20});
|
const response = await api.diaryList({page:2,page_size:20});
|
||||||
assert.equal(response.page_size,20);
|
assert.equal(response.page_size,20);
|
||||||
await api.periodVisibleSet({period_visible:0});
|
await api.periodVisibleSet({period_visible:0});
|
||||||
assert.equal(calls[0].url,'https://we.sundynix.cn/api/diaryList?page=2&pageSize=20');
|
assert.equal(calls[0].url,require('../config/env').API_BASE+'/diaryList?page=2&pageSize=20');
|
||||||
assert.equal(calls[0].header.Authorization,'Bearer test-token');
|
assert.equal(calls[0].header.Authorization,'Bearer test-token');
|
||||||
assert.equal(calls[1].url,'https://we.sundynix.cn/api/periodVisibleSet');
|
assert.equal(calls[1].url,require('../config/env').API_BASE+'/periodVisibleSet');
|
||||||
assert.equal(calls[1].data.periodVisible,0);
|
assert.equal(calls[1].data.periodVisible,0);
|
||||||
});
|
});
|
||||||
test('multipart upload retains action and token when Android omits headers',async()=>{
|
test('multipart upload retains action and token when Android omits headers',async()=>{
|
||||||
@@ -38,7 +38,7 @@ test('failed homepage load ends the recent-record spinner',async()=>{
|
|||||||
let definition;
|
let definition;
|
||||||
const api={coupleInfo:async()=>{throw { _statusCode:404 };}};
|
const api={coupleInfo:async()=>{throw { _statusCode:404 };}};
|
||||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../pages/index/index.js'),'utf8'),{
|
vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../pages/index/index.js'),'utf8'),{
|
||||||
Page(p){definition=p;},require(name){return name.endsWith('/list-pagination')?require('../utils/list-pagination'):name.endsWith('/page-cache')?{withDataCache:p=>p}:name.endsWith('/request')?{api,getImageUrl:v=>v}:name.endsWith('/layout')?{getLayout:()=>({})}:{saveScrollPosition(){},getScrollPosition(){return 0;}};},
|
Page(p){definition=p;},require(name){return name.endsWith('/record-access')?{guardRecordEntries:p=>p}:name.endsWith('/list-pagination')?require('../utils/list-pagination'):name.endsWith('/page-cache')?{withDataCache:p=>p}:name.endsWith('/request')?{api,getImageUrl:v=>v}:name.endsWith('/layout')?{getLayout:()=>({})}:{saveScrollPosition(){},getScrollPosition(){return 0;}};},
|
||||||
wx:{getStorageSync:()=> 'test-token'}, console:{error(){}}, Date, setTimeout
|
wx:{getStorageSync:()=> 'test-token'}, console:{error(){}}, Date, setTimeout
|
||||||
});
|
});
|
||||||
const page={...definition,data:{...definition.data},setData(data){Object.assign(this.data,data);}};
|
const page={...definition,data:{...definition.data},setData(data){Object.assign(this.data,data);}};
|
||||||
@@ -50,7 +50,7 @@ test('unbound newcomer loads homepage records without requesting couple-only pet
|
|||||||
let definition;let petCalls=0;
|
let definition;let petCalls=0;
|
||||||
const api={coupleInfo:async()=>({not_bound:true,partner1:{nickname:'新用户'}}),timelineList:async()=>[],albumList:async()=>[],diaryList:async()=>({items:[],total:0}),songsList:async()=>[],checkinList:async()=>({items:[]}),petInfo:async()=>{petCalls++;throw Error('请先绑定情侣');}};
|
const api={coupleInfo:async()=>({not_bound:true,partner1:{nickname:'新用户'}}),timelineList:async()=>[],albumList:async()=>[],diaryList:async()=>({items:[],total:0}),songsList:async()=>[],checkinList:async()=>({items:[]}),petInfo:async()=>{petCalls++;throw Error('请先绑定情侣');}};
|
||||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../pages/index/index.js'),'utf8'),{
|
vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../pages/index/index.js'),'utf8'),{
|
||||||
Page(p){definition=p;},require(name){return name.endsWith('/list-pagination')?require('../utils/list-pagination'):name.endsWith('/page-cache')?{withDataCache:p=>p}:name.endsWith('/request')?{api,getImageUrl:v=>v}:name.endsWith('/layout')?{getLayout:()=>({})}:name.endsWith('/journal')?{diaryEntry:v=>v}:{};},
|
Page(p){definition=p;},require(name){return name.endsWith('/record-access')?{guardRecordEntries:p=>p}:name.endsWith('/list-pagination')?require('../utils/list-pagination'):name.endsWith('/page-cache')?{withDataCache:p=>p}:name.endsWith('/request')?{api,getImageUrl:v=>v}:name.endsWith('/layout')?{getLayout:()=>({})}:name.endsWith('/journal')?{diaryEntry:v=>v}:{};},
|
||||||
wx:{getStorageSync:()=>1,removeStorageSync(){}},console,Date,setTimeout
|
wx:{getStorageSync:()=>1,removeStorageSync(){}},console,Date,setTimeout
|
||||||
});
|
});
|
||||||
const page={...definition,data:{...definition.data,coupleId:99},setData(data){Object.assign(this.data,data);}};
|
const page={...definition,data:{...definition.data,coupleId:99},setData(data){Object.assign(this.data,data);}};
|
||||||
@@ -68,7 +68,7 @@ test('smoking visibility page uses the real request adapter and restores the per
|
|||||||
else if(o.url.endsWith('/smokingCardVisible')){visible=o.data.visible;o.success({statusCode:200,data:{success:true,cardVisible:visible}});}
|
else if(o.url.endsWith('/smokingCardVisible')){visible=o.data.visible;o.success({statusCode:200,data:{success:true,cardVisible:visible}});}
|
||||||
else throw new Error('Unexpected request: '+o.url);
|
else throw new Error('Unexpected request: '+o.url);
|
||||||
}};
|
}};
|
||||||
require('./load-module.cjs')({wx,console,Date,Promise,Page:p=>definition=p},{'../../utils/page-cache':{withDataCache:p=>p}})(path.join(__dirname,'../pages/punch/punch.js'));
|
require('./load-module.cjs')({wx,console,Date,Promise,Page:p=>definition=p},{'../../utils/page-cache':{withDataCache:p=>p}})(path.join(__dirname,'../pages/settings/settings.js'));
|
||||||
const makePage=()=>({...definition,data:JSON.parse(JSON.stringify(definition.data)),setData(p){Object.assign(this.data,p);}});
|
const makePage=()=>({...definition,data:JSON.parse(JSON.stringify(definition.data)),setData(p){Object.assign(this.data,p);}});
|
||||||
const page=makePage();await page.loadSmokingPreference();assert.equal(page.data.smokingVisible,true);
|
const page=makePage();await page.loadSmokingPreference();assert.equal(page.data.smokingVisible,true);
|
||||||
await page.toggleSmoking({detail:{value:false}});assert.equal(visible,0);assert.equal(page.data.smokingVisible,false);
|
await page.toggleSmoking({detail:{value:false}});assert.equal(visible,0);assert.equal(page.data.smokingVisible,false);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function setup({preflightQuota=false,uploadQuota=false,networkRetry=false,metada
|
|||||||
const options={url:'https://we.sundynix.cn/api/upload',filePath:'original.jpg'};
|
const options={url:'https://we.sundynix.cn/api/upload',filePath:'original.jpg'};
|
||||||
test('preflight checks the compressed bytes before any upload',async()=>{
|
test('preflight checks the compressed bytes before any upload',async()=>{
|
||||||
const c=setup();await c.uploadFileCompat(options);
|
const c=setup();await c.uploadFileCompat(options);
|
||||||
assert.equal(c.checks[0].url,'https://we.sundynix.cn/api/uploadPreflight');
|
assert.equal(c.checks[0].url,c.API_BASE+'/uploadPreflight');
|
||||||
assert.equal(c.checks[0].data.size,100);assert.equal(c.checks[0].data.md5,'a'.repeat(32));
|
assert.equal(c.checks[0].data.size,100);assert.equal(c.checks[0].data.md5,'a'.repeat(32));
|
||||||
assert.equal(c.uploads[0].filePath,'compressed.jpg');
|
assert.equal(c.uploads[0].filePath,'compressed.jpg');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// 所有记录入口使用同一绑定检查,个人资料、头像与邀请不受影响。
|
||||||
|
let showing = false;
|
||||||
|
function hasRecordAccess() { const id = wx.getStorageSync('coupleId'); return !!id && String(id) !== '0'; }
|
||||||
|
function invitePartner() {
|
||||||
|
getApp().globalData.openProfileBind = true;
|
||||||
|
wx.switchTab({url:'/pages/profile/profile'});
|
||||||
|
}
|
||||||
|
function requireRecordAccess(onCancel) {
|
||||||
|
if (hasRecordAccess()) return true;
|
||||||
|
if (showing) return false;
|
||||||
|
showing = true;
|
||||||
|
wx.showModal({
|
||||||
|
title:'邀请另一半,开启共同记录',
|
||||||
|
content:'绑定情侣后,就可以一起写日记、收藏照片、记录纪念日和使用其他记录功能。邀请TA加入,开始你们的生活手账吧。',
|
||||||
|
confirmText:'立即邀请', cancelText:'暂时不了', confirmColor:'#B74768',
|
||||||
|
success(result) { if (result.confirm) invitePartner(); else if (onCancel) onCancel(); },
|
||||||
|
fail() { if (onCancel) onCancel(); },
|
||||||
|
complete() { showing = false; }
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function guardRecordEntries(definition, names) {
|
||||||
|
names.forEach(name => {
|
||||||
|
const original = definition[name];
|
||||||
|
if (original) definition[name] = function(...args) {
|
||||||
|
if (!requireRecordAccess()) return;
|
||||||
|
return original.apply(this, args);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
// 分享链接、历史页面栈等直接进入二级页面时同样拦截,不加载数据。
|
||||||
|
function withRecordAccess(definition) {
|
||||||
|
function enter(page, original, args) {
|
||||||
|
const allowed = hasRecordAccess();
|
||||||
|
page.setData({recordAccessBlocked:!allowed});
|
||||||
|
if (!allowed) {
|
||||||
|
requireRecordAccess(() => wx.switchTab({url:'/pages/index/index'}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return original && original.apply(page, args);
|
||||||
|
}
|
||||||
|
const load = definition.onLoad, show = definition.onShow, change = definition.onDataChanged;
|
||||||
|
definition.onLoad = function(...args) {
|
||||||
|
this.setData({recordAccessBlocked:true});
|
||||||
|
const app = typeof getApp === 'function' && getApp();
|
||||||
|
this._recordEntryReady = Promise.resolve(app && app.ensureAuth ? app.ensureAuth() : null)
|
||||||
|
.then(() => enter(this, load, args)).catch(error => {
|
||||||
|
console.warn('检查记录权限失败', error);
|
||||||
|
wx.showToast({title:'登录状态获取失败,请重新进入',icon:'none'});
|
||||||
|
});
|
||||||
|
return this._recordEntryReady;
|
||||||
|
};
|
||||||
|
definition.onShow = function(...args) {
|
||||||
|
return Promise.resolve(this._recordEntryReady).then(() => enter(this, show, args));
|
||||||
|
};
|
||||||
|
definition.onDataChanged = function(...args) { return enter(this, change, args); };
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
module.exports = {hasRecordAccess, requireRecordAccess, invitePartner, guardRecordEntries, withRecordAccess};
|
||||||
+19
-7
@@ -64,6 +64,17 @@ function formatUploadError(data) {
|
|||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function maybeRedirectBinding(data) {
|
||||||
|
if (!data || Number(data.code) !== 10002) return false;
|
||||||
|
if (!data._bindingPromptShown) {
|
||||||
|
data._bindingPromptShown = true;
|
||||||
|
wx.removeStorageSync('coupleId');
|
||||||
|
dataCache.clear();
|
||||||
|
require('./record-access').requireRecordAccess();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 存储配额错误码(与后端一致),上传超限时引导用户付费。
|
// 存储配额错误码(与后端一致),上传超限时引导用户付费。
|
||||||
var STORAGE_QUOTA_EXCEEDED = 10001;
|
var STORAGE_QUOTA_EXCEEDED = 10001;
|
||||||
function isStorageQuotaError(data) {
|
function isStorageQuotaError(data) {
|
||||||
@@ -170,6 +181,7 @@ function sendRequest(action, data = null, method = 'POST', options = {}) {
|
|||||||
}
|
}
|
||||||
if (!options.silent) { try { wx.hideLoading(); } catch(e) {} }
|
if (!options.silent) { try { wx.hideLoading(); } catch(e) {} }
|
||||||
if (res.statusCode >= 400 || (res.data && res.data.error)) {
|
if (res.statusCode >= 400 || (res.data && res.data.error)) {
|
||||||
|
maybeRedirectBinding(res.data);
|
||||||
reject({ ...fromWire(res.data), _statusCode: res.statusCode });
|
reject({ ...fromWire(res.data), _statusCode: res.statusCode });
|
||||||
} else {
|
} else {
|
||||||
const result = fromWire(res.data);
|
const result = fromWire(res.data);
|
||||||
@@ -213,11 +225,11 @@ const api = {
|
|||||||
);
|
);
|
||||||
wx.hideLoading();
|
wx.hideLoading();
|
||||||
if (data && !data.error && (data.path || data.success)) { dataCache.mutation('uploadAvatar', data); return data; }
|
if (data && !data.error && (data.path || data.success)) { dataCache.mutation('uploadAvatar', data); return data; }
|
||||||
if (!maybeRedirectStorage(data)) wx.showToast({ title: formatUploadError(data), icon: 'none' });
|
if (!maybeRedirectBinding(data) && !maybeRedirectStorage(data)) wx.showToast({ title: formatUploadError(data), icon: 'none' });
|
||||||
throw data || { error: '上传失败' };
|
throw data || { error: '上传失败' };
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
wx.hideLoading();
|
wx.hideLoading();
|
||||||
if (!maybeRedirectStorage(e)) wx.showToast({ title: formatUploadError(e), icon: 'none' });
|
if (!maybeRedirectBinding(e) && !maybeRedirectStorage(e)) wx.showToast({ title: formatUploadError(e), icon: 'none' });
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -233,11 +245,11 @@ const api = {
|
|||||||
);
|
);
|
||||||
wx.hideLoading();
|
wx.hideLoading();
|
||||||
if (data && !data.error && (data.path || data.success)) { dataCache.mutation('uploadAvatar', data); return data; }
|
if (data && !data.error && (data.path || data.success)) { dataCache.mutation('uploadAvatar', data); return data; }
|
||||||
if (!maybeRedirectStorage(data)) wx.showToast({ title: formatUploadError(data), icon: 'none' });
|
if (!maybeRedirectBinding(data) && !maybeRedirectStorage(data)) wx.showToast({ title: formatUploadError(data), icon: 'none' });
|
||||||
throw data || { error: '上传失败' };
|
throw data || { error: '上传失败' };
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
wx.hideLoading();
|
wx.hideLoading();
|
||||||
if (!maybeRedirectStorage(e)) wx.showToast({ title: formatUploadError(e), icon: 'none' });
|
if (!maybeRedirectBinding(e) && !maybeRedirectStorage(e)) wx.showToast({ title: formatUploadError(e), icon: 'none' });
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -280,11 +292,11 @@ const api = {
|
|||||||
});
|
});
|
||||||
wx.hideLoading();
|
wx.hideLoading();
|
||||||
if (data && !data.error && (data.success || data.path || data.uploaded)) { dataCache.mutation('photoUpload', data); return data; }
|
if (data && !data.error && (data.success || data.path || data.uploaded)) { dataCache.mutation('photoUpload', data); return data; }
|
||||||
if (!maybeRedirectStorage(data)) wx.showToast({ title: formatUploadError(data), icon: 'none' });
|
if (!maybeRedirectBinding(data) && !maybeRedirectStorage(data)) wx.showToast({ title: formatUploadError(data), icon: 'none' });
|
||||||
throw data || { error: '上传失败' };
|
throw data || { error: '上传失败' };
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
wx.hideLoading();
|
wx.hideLoading();
|
||||||
if (!maybeRedirectStorage(e)) wx.showToast({ title: formatUploadError(e), icon: 'none' });
|
if (!maybeRedirectBinding(e) && !maybeRedirectStorage(e)) wx.showToast({ title: formatUploadError(e), icon: 'none' });
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -433,7 +445,7 @@ async function uploadFileCompat(options) {
|
|||||||
success: (res) => {
|
success: (res) => {
|
||||||
var data = parseUploadResponse(res.data);
|
var data = parseUploadResponse(res.data);
|
||||||
if (data) {
|
if (data) {
|
||||||
if (maybeRedirectStorage(data)) { reject(data); return; }
|
if (maybeRedirectBinding(data) || maybeRedirectStorage(data)) { reject(data); return; }
|
||||||
if (res.statusCode < 400 && !data.error && (data.success || data.path || data.uploaded)) {
|
if (res.statusCode < 400 && !data.error && (data.success || data.path || data.uploaded)) {
|
||||||
dataCache.mutation(actionMatch ? decodeURIComponent(actionMatch[1] || actionMatch[2]) : '', data);
|
dataCache.mutation(actionMatch ? decodeURIComponent(actionMatch[1] || actionMatch[2]) : '', data);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user