diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js index a19eb487..ad4d0145 100644 --- a/miniprogram/pages/index/index.js +++ b/miniprogram/pages/index/index.js @@ -120,24 +120,32 @@ Page({ async loadSuperMembers() { try { - // 优先加载VIP会员 + // 优先加载 VIP 会员(购买 1980 fullbook/vip 订单的用户) let members = [] try { const res = await app.request({ url: '/api/miniprogram/vip/members', silent: true }) if (res && res.success && res.data) { - members = res.data.filter(u => u.avatar || u.vip_avatar).slice(0, 4).map(u => ({ - id: u.id, name: u.vip_name || u.nickname || '会员', - avatar: u.vip_avatar || u.avatar, isVip: true + // 不再过滤无头像用户,无头像时用首字母展示 + members = (Array.isArray(res.data) ? res.data : []).slice(0, 4).map(u => ({ + id: u.id, + name: u.vip_name || u.nickname || '会员', + avatar: u.vip_avatar || u.avatar || '', + isVip: true })) + if (members.length > 0) { + console.log('[Index] 超级个体加载成功:', members.length, '人') + } } - } catch (e) {} - // 不足4个则用有头像的普通用户补充 + } catch (e) { + console.log('[Index] vip/members 请求失败:', e) + } + // 不足 4 个则用有头像的普通用户补充 if (members.length < 4) { try { const dbRes = await app.request({ url: '/api/miniprogram/users?limit=20', silent: true }) if (dbRes && dbRes.success && dbRes.data) { const existIds = new Set(members.map(m => m.id)) - const extra = dbRes.data + const extra = (Array.isArray(dbRes.data) ? dbRes.data : []) .filter(u => u.avatar && u.nickname && !existIds.has(u.id)) .slice(0, 4 - members.length) .map(u => ({ id: u.id, name: u.nickname, avatar: u.avatar, isVip: u.is_vip === 1 })) diff --git a/soul-api/.env copy b/soul-api/.env copy new file mode 100644 index 00000000..077a905f --- /dev/null +++ b/soul-api/.env copy @@ -0,0 +1,38 @@ +# 服务(启动端口在 .env 中配置,修改 PORT 后重启生效) +PORT=8080 +GIN_MODE=debug + +# 版本号:打包 zip 前在此填写,上传服务器覆盖 .env 后,访问 /health 会返回此版本 +APP_VERSION=0.0.0 + +# 数据库(与 Next 现网一致:腾讯云 CDB soul_miniprogram) +DB_DSN=cdb_outerroot:Zhiqun1984@tcp(56b4c23f6853c.gz.cdb.myqcloud.com:14413)/soul_miniprogram?charset=utf8mb4&parseTime=True + +# 微信小程序配置 +WECHAT_APPID=wxb8bbb2b10dec74aa +WECHAT_APPSECRET=3c1fb1f63e6e052222bbcead9d07fe0c +WECHAT_MCH_ID=1318592501 +WECHAT_MCH_KEY=wx3e31b068be59ddc131b068be59ddc2 +WECHAT_NOTIFY_URL=https://soul.quwanzhi.com/api/miniprogram/pay/notify +# 小程序码/订阅消息跳转版本:formal=正式版(默认) | trial=体验版 | developer=开发版 +WECHAT_MINI_PROGRAM_STATE=formal + +# 微信转账配置(API v3) +WECHAT_APIV3_KEY=wx3e31b068be59ddc131b068be59ddc2 +# 公钥证书(本地或 OSS):https://karuocert.oss-cn-shenzhen.aliyuncs.com/1318592501/apiclient_cert.pem +WECHAT_CERT_PATH=certs/apiclient_cert.pem +# 私钥(线上用 OSS):https://karuocert.oss-cn-shenzhen.aliyuncs.com/1318592501/apiclient_key.pem +WECHAT_KEY_PATH=certs/apiclient_key.pem +WECHAT_SERIAL_NO=4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5 +WECHAT_TRANSFER_URL=https://souladmin.quwanzhi.com/api/payment/wechat/transfer/notify + +# 管理端登录(与 next-project 一致,默认 admin / admin123) +# ADMIN_USERNAME=admin +# ADMIN_PASSWORD=admin123 +# ADMIN_SESSION_SECRET=soul-admin-secret-change-in-prod + +# 可选:信任代理 IP(逗号分隔),部署在 Nginx 后时填写 +# TRUSTED_PROXIES=127.0.0.1,::1 + +# 跨域 CORS:允许的源,逗号分隔。未设置时使用默认值(含 localhost、soul.quwanzhi.com) +CORS_ORIGINS=http://localhost:5174,http://127.0.0.1:5174,https://soul.quwanzhi.com,http://soul.quwanzhi.com,https://souladmin.quwanzhi.com,http://souladmin.quwanzhi.com diff --git a/soul-api/internal/handler/miniprogram.go b/soul-api/internal/handler/miniprogram.go index a6f21c70..51a55122 100644 --- a/soul-api/internal/handler/miniprogram.go +++ b/soul-api/internal/handler/miniprogram.go @@ -447,6 +447,14 @@ func MiniprogramPayNotify(c *gin.Context) { if attach.ProductType == "fullbook" { db.Model(&model.User{}).Where("id = ?", buyerUserID).Update("has_full_book", true) fmt.Printf("[PayNotify] 用户已购全书: %s\n", buyerUserID) + } else if attach.ProductType == "vip" { + // VIP 支付成功:更新 users.is_vip、vip_expire_date(与 next-project 一致) + expireDate := time.Now().AddDate(0, 0, 365) + db.Model(&model.User{}).Where("id = ?", buyerUserID).Updates(map[string]interface{}{ + "is_vip": true, + "vip_expire_date": expireDate, + }) + fmt.Printf("[PayNotify] 用户开通VIP: %s, 过期日 %s\n", buyerUserID, expireDate.Format("2006-01-02")) } else if attach.ProductType == "match" { fmt.Printf("[PayNotify] 用户购买匹配次数: %s,订单 %s\n", buyerUserID, orderSn) } else if attach.ProductType == "section" && attach.ProductID != "" { diff --git a/soul-api/internal/handler/vip.go b/soul-api/internal/handler/vip.go index a1a8db50..f45d8d1e 100644 --- a/soul-api/internal/handler/vip.go +++ b/soul-api/internal/handler/vip.go @@ -2,15 +2,64 @@ package handler import ( "net/http" + "strconv" "time" "soul-api/internal/database" "soul-api/internal/model" "github.com/gin-gonic/gin" + "gorm.io/gorm" ) +// 默认 VIP 价格与权益(与 next-project 一致) +const defaultVipPrice = 1980 + +var defaultVipRights = []string{ + "智能纪要 - 每天推送派对精华", + "会议纪要库 - 所有场次会议纪要", + "案例库 - 30-100个创业项目案例", + "链接资源 - 进群聊天链接资源", + "解锁全部章节内容(365天)", + "匹配所有创业伙伴", + "创业老板排行榜展示", + "专属VIP标识", +} + +// isVipFromUsers 从 users 表判断是否 VIP(is_vip=1 且 vip_expire_date>NOW) +func isVipFromUsers(db *gorm.DB, userID string) (bool, *time.Time) { + var u struct { + IsVip *bool + VipExpireDate *time.Time + } + err := db.Table("users").Select("is_vip", "vip_expire_date").Where("id = ?", userID).First(&u).Error + if err != nil || u.IsVip == nil || !*u.IsVip || u.VipExpireDate == nil { + return false, nil + } + if u.VipExpireDate.Before(time.Now()) { + return false, nil + } + return true, u.VipExpireDate +} + +// isVipFromOrders 从 orders 表判断是否 VIP(兜底) +func isVipFromOrders(db *gorm.DB, userID string) (bool, *time.Time) { + var order model.Order + err := db.Where("user_id = ? AND (status = ? OR status = ?) AND (product_type = ? OR product_type = ?)", + userID, "paid", "completed", "fullbook", "vip"). + Order("pay_time DESC").First(&order).Error + if err != nil || order.PayTime == nil { + return false, nil + } + exp := order.PayTime.AddDate(0, 0, 365) + if exp.Before(time.Now()) { + return false, nil + } + return true, &exp +} + // VipStatus GET /api/miniprogram/vip/status 小程序-查询用户 VIP 状态 +// 优先 users 表(is_vip、vip_expire_date),无则从 orders 兜底 func VipStatus(c *gin.Context) { userID := c.Query("userId") if userID == "" { @@ -19,35 +68,41 @@ func VipStatus(c *gin.Context) { } db := database.DB() - // 查是否有 fullbook 或 vip 的已支付订单 - var order model.Order - err := db.Where("user_id = ? AND status = ? AND (product_type = ? OR product_type = ?)", - userID, "paid", "fullbook", "vip"). - Order("pay_time DESC").First(&order).Error - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": true, - "data": gin.H{ - "isVip": false, - "daysRemaining": 0, - "expireDate": "", - "price": float64(1980), - }, - }) - return - } - - expireDate := time.Now().AddDate(0, 0, 365) - daysRemaining := 365 - if order.PayTime != nil { - expireDate = order.PayTime.AddDate(0, 0, 365) - if expireDate.After(time.Now()) { - daysRemaining = int(expireDate.Sub(time.Now()).Hours() / 24) - } else { - daysRemaining = 0 + // 1. 优先 users 表 + isVip, expireDate := isVipFromUsers(db, userID) + if !isVip { + // 2. 兜底:从 orders 查 + isVip, expireDate = isVipFromOrders(db, userID) + if !isVip { + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": gin.H{ + "isVip": false, + "daysRemaining": 0, + "expireDate": "", + "profile": gin.H{"name": "", "project": "", "contact": "", "avatar": "", "bio": ""}, + "price": float64(defaultVipPrice), + "rights": defaultVipRights, + }, + }) + return } } - expStr := expireDate.Format("2006-01-02") + + // 查用户 VIP 资料(profile) + var user model.User + _ = db.Where("id = ?", userID).First(&user).Error + profile := buildVipProfile(&user) + + daysRemaining := 0 + expStr := "" + if expireDate != nil { + daysRemaining = int(expireDate.Sub(time.Now()).Hours()/24) + 1 + if daysRemaining < 0 { + daysRemaining = 0 + } + expStr = expireDate.Format("2006-01-02") + } c.JSON(http.StatusOK, gin.H{ "success": true, @@ -55,11 +110,45 @@ func VipStatus(c *gin.Context) { "isVip": true, "daysRemaining": daysRemaining, "expireDate": expStr, - "price": float64(1980), + "profile": profile, + "price": float64(defaultVipPrice), + "rights": defaultVipRights, }, }) } +func buildVipProfile(u *model.User) gin.H { + name, project, contact, avatar, bio := "", "", "", "", "" + if u.VipName != nil { + name = *u.VipName + } + if name == "" && u.Nickname != nil { + name = *u.Nickname + } + if u.VipProject != nil { + project = *u.VipProject + } + if u.VipContact != nil { + contact = *u.VipContact + } + if contact == "" && u.Phone != nil { + contact = *u.Phone + } + if contact == "" && u.WechatID != nil { + contact = *u.WechatID + } + if u.VipAvatar != nil { + avatar = *u.VipAvatar + } + if avatar == "" && u.Avatar != nil { + avatar = *u.Avatar + } + if u.VipBio != nil { + bio = *u.VipBio + } + return gin.H{"name": name, "project": project, "contact": contact, "avatar": avatar, "bio": bio} +} + // VipProfileGet GET /api/miniprogram/vip/profile 小程序-获取 VIP 资料 func VipProfileGet(c *gin.Context) { userID := c.Query("userId") @@ -70,38 +159,24 @@ func VipProfileGet(c *gin.Context) { db := database.DB() var user model.User if err := db.Where("id = ?", userID).First(&user).Error; err != nil { - c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"name": "", "project": "", "contact": "", "bio": ""}}) + c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"name": "", "project": "", "contact": "", "avatar": "", "bio": ""}}) return } - name := "" - if user.Nickname != nil { - name = *user.Nickname - } - contact := "" - if user.Phone != nil { - contact = *user.Phone - } - if user.WechatID != nil && contact == "" { - contact = *user.WechatID - } c.JSON(http.StatusOK, gin.H{ "success": true, - "data": gin.H{ - "name": name, - "project": "", - "contact": contact, - "bio": "", - }, + "data": buildVipProfile(&user), }) } // VipProfilePost POST /api/miniprogram/vip/profile 小程序-更新 VIP 资料 +// 仅 VIP 会员可更新,更新 vip_name/vip_project/vip_contact/vip_avatar/vip_bio func VipProfilePost(c *gin.Context) { var req struct { UserID string `json:"userId" binding:"required"` Name string `json:"name"` Project string `json:"project"` Contact string `json:"contact"` + Avatar string `json:"avatar"` Bio string `json:"bio"` } if err := c.ShouldBindJSON(&req); err != nil { @@ -109,81 +184,156 @@ func VipProfilePost(c *gin.Context) { return } db := database.DB() + + // 校验是否 VIP(users 或 orders) + isVip, _ := isVipFromUsers(db, req.UserID) + if !isVip { + isVip, _ = isVipFromOrders(db, req.UserID) + } + if !isVip { + c.JSON(http.StatusOK, gin.H{"success": false, "error": "仅VIP会员可填写资料"}) + return + } + updates := map[string]interface{}{} if req.Name != "" { - updates["nickname"] = req.Name + updates["vip_name"] = req.Name + } + if req.Project != "" { + updates["vip_project"] = req.Project } if req.Contact != "" { - updates["phone"] = req.Contact + updates["vip_contact"] = req.Contact } - if len(updates) > 0 { - db.Model(&model.User{}).Where("id = ?", req.UserID).Updates(updates) + if req.Avatar != "" { + updates["vip_avatar"] = req.Avatar } - c.JSON(http.StatusOK, gin.H{"success": true}) + if req.Bio != "" { + updates["vip_bio"] = req.Bio + } + if len(updates) == 0 { + c.JSON(http.StatusOK, gin.H{"success": false, "error": "无更新内容"}) + return + } + + if err := db.Model(&model.User{}).Where("id = ?", req.UserID).Updates(updates).Error; err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "error": "更新失败"}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "message": "资料已更新"}) } // VipMembers GET /api/miniprogram/vip/members 小程序-VIP 会员列表(无 id 返回列表;有 id 返回单个) +// 优先 users 表(is_vip=1 且 vip_expire_date>NOW),无则从 orders 兜底 func VipMembers(c *gin.Context) { id := c.Query("id") + limit := 20 + if l := c.Query("limit"); l != "" { + if n, err := parseInt(l); err == nil && n > 0 && n <= 100 { + limit = n + } + } db := database.DB() - // 有 id 时查单个:优先从已购 fullbook/vip 的用户中找 if id != "" { + // 单个:优先 users 表 var user model.User if err := db.Where("id = ?", id).First(&user).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": true, "data": nil}) return } - // 检查是否 VIP(有 fullbook 或 vip 订单) - var cnt int64 - db.Model(&model.Order{}).Where("user_id = ? AND status = ? AND (product_type = ? OR product_type = ?)", - id, "paid", "fullbook", "vip").Count(&cnt) - item := formatVipMember(&user, cnt > 0) + isVip, _ := isVipFromUsers(db, id) + if !isVip { + isVip, _ = isVipFromOrders(db, id) + } + if !isVip { + c.JSON(http.StatusOK, gin.H{"success": false, "error": "会员不存在或已过期"}) + return + } + item := formatVipMember(&user, true) c.JSON(http.StatusOK, gin.H{"success": true, "data": item}) return } - // 无 id:返回 VIP 会员列表(有 fullbook 或 vip 订单的用户) - var userIDs []string - db.Model(&model.Order{}).Select("DISTINCT user_id"). - Where("status = ? AND (product_type = ? OR product_type = ?)", "paid", "fullbook", "vip"). - Pluck("user_id", &userIDs) - if len(userIDs) == 0 { - c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}}) - return - } + // 列表:优先 users 表(is_vip=1 且 vip_expire_date>NOW) var users []model.User - db.Where("id IN ?", userIDs).Find(&users) + err := db.Table("users"). + Select("id", "nickname", "avatar", "vip_name", "vip_project", "vip_avatar", "vip_bio"). + Where("is_vip = 1 AND vip_expire_date > ?", time.Now()). + Order("vip_expire_date DESC"). + Limit(limit). + Find(&users).Error + + if err != nil || len(users) == 0 { + // 兜底:从 orders 查 + var userIDs []string + db.Model(&model.Order{}).Select("DISTINCT user_id"). + Where("(status = ? OR status = ?) AND (product_type = ? OR product_type = ?)", "paid", "completed", "fullbook", "vip"). + Pluck("user_id", &userIDs) + if len(userIDs) == 0 { + c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}, "total": 0}) + return + } + db.Where("id IN ?", userIDs).Find(&users) + } + list := make([]gin.H, 0, len(users)) for i := range users { list = append(list, formatVipMember(&users[i], true)) } - c.JSON(http.StatusOK, gin.H{"success": true, "data": list}) + c.JSON(http.StatusOK, gin.H{"success": true, "data": list, "total": len(list)}) } func formatVipMember(u *model.User, isVip bool) gin.H { name := "" - if u.Nickname != nil { + if u.VipName != nil && *u.VipName != "" { + name = *u.VipName + } + if name == "" && u.Nickname != nil { name = *u.Nickname } + if name == "" { + name = "创业者" + } avatar := "" - if u.Avatar != nil { + if u.VipAvatar != nil && *u.VipAvatar != "" { + avatar = *u.VipAvatar + } + if avatar == "" && u.Avatar != nil { avatar = *u.Avatar } + project := "" + if u.VipProject != nil { + project = *u.VipProject + } + bio := "" + if u.VipBio != nil { + bio = *u.VipBio + } contact := "" - if u.Phone != nil { + if u.VipContact != nil { + contact = *u.VipContact + } + if contact == "" && u.Phone != nil { contact = *u.Phone } - if u.WechatID != nil && contact == "" { + if contact == "" && u.WechatID != nil { contact = *u.WechatID } return gin.H{ - "id": u.ID, - "nickname": name, - "avatar": avatar, - "vip_name": name, - "vip_avatar": avatar, + "id": u.ID, + "name": name, + "nickname": name, + "avatar": avatar, + "vip_name": name, + "vip_avatar": avatar, + "vip_project": project, "vip_contact": contact, - "is_vip": isVip, + "vip_bio": bio, + "is_vip": isVip, } } + +func parseInt(s string) (int, error) { + return strconv.Atoi(s) +} diff --git a/soul-api/internal/model/user.go b/soul-api/internal/model/user.go index 6f85f34d..67f0e062 100644 --- a/soul-api/internal/model/user.go +++ b/soul-api/internal/model/user.go @@ -23,6 +23,15 @@ type User struct { WithdrawnEarnings *float64 `gorm:"column:withdrawn_earnings;type:decimal(10,2)" json:"withdrawnEarnings,omitempty"` Source *string `gorm:"column:source;size:50" json:"source,omitempty"` + // VIP 相关(与 next-project 线上 users 表一致) + IsVip *bool `gorm:"column:is_vip" json:"-"` + VipExpireDate *time.Time `gorm:"column:vip_expire_date" json:"-"` + VipName *string `gorm:"column:vip_name;size:100" json:"-"` + VipAvatar *string `gorm:"column:vip_avatar;size:500" json:"-"` + VipProject *string `gorm:"column:vip_project;size:200" json:"-"` + VipContact *string `gorm:"column:vip_contact;size:100" json:"-"` + VipBio *string `gorm:"column:vip_bio;type:text" json:"-"` + // 以下为接口返回时从订单/绑定表实时计算的字段,不入库 PurchasedSectionCount int `gorm:"-" json:"purchasedSectionCount,omitempty"` } diff --git a/soul-api/scripts/fix-vip-orders.sql b/soul-api/scripts/fix-vip-orders.sql new file mode 100644 index 00000000..1860e29d --- /dev/null +++ b/soul-api/scripts/fix-vip-orders.sql @@ -0,0 +1,83 @@ +-- ============================================================ +-- VIP 订单修复脚本 +-- 场景:甲方开发的 VIP 支付可能未正确设置 product_type +-- 会员价:1980 元 +-- 执行前请先备份数据库! +-- ============================================================ + +-- 1. 诊断:查看当前疑似 VIP 订单的状态(执行后人工确认) +-- -------------------------------------------------------- +SELECT id, order_sn, user_id, product_type, amount, status, pay_time, created_at +FROM orders +WHERE amount = 1980 + AND (status = 'paid' OR status = 'completed') +ORDER BY pay_time DESC; + +-- 2. 统计:有多少条需要修复 +-- -------------------------------------------------------- +SELECT COUNT(*) AS need_fix_count +FROM orders +WHERE amount = 1980 + AND (status = 'paid' OR status = 'completed') + AND (product_type IS NULL OR product_type = '' OR product_type NOT IN ('vip', 'fullbook')); + +-- 3. 修复:将 1980 元已支付订单的 product_type 设为 'vip' +-- -------------------------------------------------------- +-- 条件:金额=1980 且 已支付 且 product_type 不是 vip/fullbook +UPDATE orders +SET product_type = 'vip' +WHERE amount = 1980 + AND (status = 'paid' OR status = 'completed') + AND (product_type IS NULL OR product_type = '' OR product_type NOT IN ('vip', 'fullbook')); + +-- 4. 兼容大小写:若 product_type 为 'VIP'、'Vip' 等,统一为小写 +-- -------------------------------------------------------- +UPDATE orders +SET product_type = 'vip' +WHERE amount = 1980 + AND (status = 'paid' OR status = 'completed') + AND LOWER(TRIM(product_type)) = 'vip' + AND product_type != 'vip'; + +-- 5. 验证:修复后应无遗漏 +-- -------------------------------------------------------- +SELECT id, order_sn, user_id, product_type, amount, status +FROM orders +WHERE amount = 1980 + AND (status = 'paid' OR status = 'completed') + AND product_type NOT IN ('vip', 'fullbook'); +-- 期望结果:0 行 + +-- ============================================================ +-- 可选:若线上 next-project 用 users 表存 is_vip,需确保字段存在 +-- 执行前请确认 users 表是否已有这些列! +-- ============================================================ + +-- 6. 检查 users 表是否有 VIP 相关列(MySQL) +-- SHOW COLUMNS FROM users LIKE 'is_vip'; +-- SHOW COLUMNS FROM users LIKE 'vip_expire_date'; + +-- 7. 若 users 表无 VIP 列,可执行以下 ALTER(按需取消注释) +-- -------------------------------------------------------- +-- ALTER TABLE users ADD COLUMN is_vip TINYINT(1) DEFAULT 0; +-- ALTER TABLE users ADD COLUMN vip_expire_date DATETIME NULL; +-- ALTER TABLE users ADD COLUMN vip_name VARCHAR(100) NULL; +-- ALTER TABLE users ADD COLUMN vip_avatar VARCHAR(500) NULL; +-- ALTER TABLE users ADD COLUMN vip_project VARCHAR(200) NULL; +-- ALTER TABLE users ADD COLUMN vip_contact VARCHAR(100) NULL; +-- ALTER TABLE users ADD COLUMN vip_bio TEXT NULL; + +-- 8. 从 orders 同步到 users(仅当用 users 表存 VIP 时) +-- 将 1980 元已支付订单对应的用户标记为 VIP,过期日 = pay_time + 365 天 +-- -------------------------------------------------------- +-- UPDATE users u +-- INNER JOIN ( +-- SELECT user_id, MAX(pay_time) AS last_pay +-- FROM orders +-- WHERE amount = 1980 +-- AND (status = 'paid' OR status = 'completed') +-- AND product_type IN ('vip', 'fullbook') +-- GROUP BY user_id +-- ) o ON u.id = o.user_id +-- SET u.is_vip = 1, +-- u.vip_expire_date = DATE_ADD(o.last_pay, INTERVAL 365 DAY); diff --git a/归档/miniprogram/.gitignore b/归档/miniprogram/.gitignore new file mode 100644 index 00000000..14ea590c --- /dev/null +++ b/归档/miniprogram/.gitignore @@ -0,0 +1,14 @@ +# Windows +[Dd]esktop.ini +Thumbs.db +$RECYCLE.BIN/ + +# macOS +.DS_Store +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes + +# Node.js +node_modules/ diff --git a/归档/miniprogram/README.md b/归档/miniprogram/README.md new file mode 100644 index 00000000..2cc11dbf --- /dev/null +++ b/归档/miniprogram/README.md @@ -0,0 +1,138 @@ +# Soul创业实验 - 微信小程序 + +> 一场SOUL的创业实验场 - 来自Soul派对房的真实商业故事 + +## 📱 项目简介 + +本项目是《一场SOUL的创业实验场》的微信小程序版本,完整还原了Web端的所有UI界面和功能。 + +## 🎨 设计特点 + +- **主题色**: Soul青色 (#00CED1) +- **设计风格**: 深色主题 + 毛玻璃效果 +- **1:1还原**: 完全复刻Web端的UI设计 + +## 📂 项目结构 + +``` +miniprogram/ +├── app.js # 应用入口 +├── app.json # 应用配置 +├── app.wxss # 全局样式 +├── custom-tab-bar/ # 自定义TabBar组件 +│ ├── index.js +│ ├── index.json +│ ├── index.wxml +│ └── index.wxss +├── pages/ +│ ├── index/ # 首页 +│ ├── chapters/ # 目录页 +│ ├── match/ # 找伙伴页 +│ ├── my/ # 我的页面 +│ ├── read/ # 阅读页 +│ ├── about/ # 关于作者 +│ ├── referral/ # 推广中心 +│ ├── purchases/ # 订单页 +│ └── settings/ # 设置页 +├── utils/ +│ ├── util.js # 工具函数 +│ └── payment.js # 支付工具 +├── assets/ +│ └── icons/ # 图标资源 +├── project.config.json # 项目配置 +└── sitemap.json # 站点地图 +``` + +## 🚀 功能列表 + +### 核心功能 +- ✅ 首页 - 书籍展示、推荐章节、阅读进度 +- ✅ 目录 - 完整章节列表、篇章折叠展开 +- ✅ 找伙伴 - 匹配动画、匹配类型选择 +- ✅ 我的 - 个人信息、订单、推广中心 +- ✅ 阅读 - 付费墙、章节导航、分享功能 + +### 特色功能 +- ✅ 自定义TabBar(中间突出的找伙伴按钮) +- ✅ 阅读进度条 +- ✅ 匹配动画效果 +- ✅ 付费墙与购买流程 +- ✅ 分享海报功能 +- ✅ 推广佣金系统 + +## 🛠 开发指南 + +### 环境要求 +- 微信开发者工具 >= 1.06.2308310 +- 基础库版本 >= 3.3.4 + +### 快速开始 + +1. **下载微信开发者工具** + - 前往 [微信开发者工具下载页面](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html) + +2. **导入项目** + - 打开微信开发者工具 + - 选择"导入项目" + - 项目目录选择 `miniprogram` 文件夹 + - AppID 使用: `wx432c93e275548671` + +3. **编译运行** + - 点击"编译"按钮 + - 在模拟器中预览效果 + +### 真机调试 + +1. 点击工具栏的"预览"按钮 +2. 使用微信扫描二维码 +3. 在真机上测试所有功能 + +## 📝 配置说明 + +### API配置 +在 `app.js` 中修改 `globalData.baseUrl`: + +```javascript +globalData: { + baseUrl: 'https://soul.ckb.fit', // 你的API地址 + // ... +} +``` + +### AppID配置 +在 `project.config.json` 中修改: + +```json +{ + "appid": "你的小程序AppID" +} +``` + +## 🎯 上线发布 + +1. **准备工作** + - 确保所有功能测试通过 + - 检查API接口是否正常 + - 确认支付功能已配置 + +2. **上传代码** + - 在开发者工具中点击"上传" + - 填写版本号和项目备注 + +3. **提交审核** + - 登录[微信公众平台](https://mp.weixin.qq.com) + - 进入"版本管理" + - 提交审核 + +4. **发布上线** + - 审核通过后点击"发布" + +## 🔗 相关链接 + +- **Web版本**: https://soul.ckb.fit +- **作者微信**: 28533368 +- **技术支持**: 存客宝 + +## 📄 版权信息 + +© 2024 卡若. All rights reserved. diff --git a/归档/miniprogram/app.js b/归档/miniprogram/app.js new file mode 100644 index 00000000..8b507f28 --- /dev/null +++ b/归档/miniprogram/app.js @@ -0,0 +1,540 @@ +/** + * Soul创业派对 - 小程序入口 + * 开发: 卡若 + */ + +App({ + globalData: { + // API基础地址 - 连接真实后端 + baseUrl: 'https://soul.quwanzhi.com', + + // 小程序配置 - 真实AppID + appId: 'wxb8bbb2b10dec74aa', + + // 订阅消息:用户点击「申请提现」→「立即提现」时会先弹出订阅授权窗 + withdrawSubscribeTmplId: 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE', + + // 微信支付配置 + mchId: '1318592501', // 商户号 + + // 用户信息 + userInfo: null, + openId: null, // 微信openId,支付必需 + isLoggedIn: false, + + // 书籍数据 + bookData: null, + totalSections: 62, + + // 购买记录 + purchasedSections: [], + hasFullBook: false, + + // 已读章节(仅统计有权限打开过的章节,用于首页「已读/待读」) + readSectionIds: [], + + // 推荐绑定 + pendingReferralCode: null, // 待绑定的推荐码 + + // 主题配置 + theme: { + brandColor: '#00CED1', + brandSecondary: '#20B2AA', + goldColor: '#FFD700', + bgColor: '#000000', + cardBg: '#1c1c1e' + }, + + // 系统信息 + systemInfo: null, + statusBarHeight: 44, + navBarHeight: 88, + + // TabBar相关 + currentTab: 0 + }, + + onLaunch(options) { + this.globalData.readSectionIds = wx.getStorageSync('readSectionIds') || [] + // 获取系统信息 + this.getSystemInfo() + + // 检查登录状态 + this.checkLoginStatus() + + // 加载书籍数据 + this.loadBookData() + + // 检查更新 + this.checkUpdate() + + // 处理分享参数(推荐码绑定) + this.handleReferralCode(options) + }, + + // 小程序显示时也检查分享参数 + onShow(options) { + this.handleReferralCode(options) + }, + + // 处理推荐码绑定 + handleReferralCode(options) { + const query = options?.query || {} + const refCode = query.ref || query.referralCode + + if (refCode) { + console.log('[App] 检测到推荐码:', refCode) + + // 立即记录访问(不需要登录,用于统计"通过链接进的人数") + this.recordReferralVisit(refCode) + + // 保存待绑定的推荐码(不再在前端做"只能绑定一次"的限制,让后端根据30天规则判断续期/抢夺) + this.globalData.pendingReferralCode = refCode + wx.setStorageSync('pendingReferralCode', refCode) + // 同步写入 referral_code,供章节/找伙伴支付时传给后端,订单会记录 referrer_id 与 referral_code + wx.setStorageSync('referral_code', refCode) + + // 如果已登录,立即尝试绑定,由 /api/miniprogram/referral/bind 按 30 天规则决定 new / renew / takeover + if (this.globalData.isLoggedIn && this.globalData.userInfo) { + this.bindReferralCode(refCode) + } + } + }, + + // 记录推荐访问(不需要登录,用于统计) + async recordReferralVisit(refCode) { + try { + // 获取openId(如果有) + const openId = this.globalData.openId || wx.getStorageSync('openId') || '' + const userId = this.globalData.userInfo?.id || '' + + await this.request('/api/miniprogram/referral/visit', { + method: 'POST', + data: { + referralCode: refCode, + visitorOpenId: openId, + visitorId: userId, + source: 'miniprogram', + page: getCurrentPages()[getCurrentPages().length - 1]?.route || '' + }, + silent: true + }) + console.log('[App] 记录推荐访问成功') + } catch (e) { + console.log('[App] 记录推荐访问失败:', e.message) + // 忽略错误,不影响用户体验 + } + }, + + // 绑定推荐码到用户 + async bindReferralCode(refCode) { + try { + const userId = this.globalData.userInfo?.id + if (!userId || !refCode) return + + console.log('[App] 绑定推荐码:', refCode, '到用户:', userId) + + // 调用API绑定推荐关系 + const res = await this.request('/api/miniprogram/referral/bind', { + method: 'POST', + data: { + userId, + referralCode: refCode + }, + silent: true + }) + + if (res.success) { + console.log('[App] 推荐码绑定成功') + // 仅记录当前已绑定的推荐码,用于展示/调试;是否允许更换由后端根据30天规则判断 + wx.setStorageSync('boundReferralCode', refCode) + this.globalData.pendingReferralCode = null + wx.removeStorageSync('pendingReferralCode') + } + } catch (e) { + console.error('[App] 绑定推荐码失败:', e) + } + }, + + // 获取系统信息 + getSystemInfo() { + try { + const systemInfo = wx.getSystemInfoSync() + this.globalData.systemInfo = systemInfo + this.globalData.statusBarHeight = systemInfo.statusBarHeight || 44 + + // 计算导航栏高度 + const menuButton = wx.getMenuButtonBoundingClientRect() + if (menuButton) { + this.globalData.navBarHeight = (menuButton.top - systemInfo.statusBarHeight) * 2 + menuButton.height + systemInfo.statusBarHeight + } + } catch (e) { + console.error('获取系统信息失败:', e) + } + }, + + // 检查登录状态 + checkLoginStatus() { + try { + const userInfo = wx.getStorageSync('userInfo') + const token = wx.getStorageSync('token') + + if (userInfo && token) { + this.globalData.userInfo = userInfo + this.globalData.isLoggedIn = true + this.globalData.purchasedSections = userInfo.purchasedSections || [] + this.globalData.hasFullBook = userInfo.hasFullBook || false + } + } catch (e) { + console.error('检查登录状态失败:', e) + } + }, + + // 加载书籍数据 + async loadBookData() { + try { + // 先从缓存加载 + const cachedData = wx.getStorageSync('bookData') + if (cachedData) { + this.globalData.bookData = cachedData + } + + // 从服务器获取最新数据 + const res = await this.request('/api/book/all-chapters') + if (res && (res.data || res.chapters)) { + const chapters = res.data || res.chapters || [] + this.globalData.bookData = chapters + wx.setStorageSync('bookData', chapters) + } + } catch (e) { + console.error('加载书籍数据失败:', e) + } + }, + + // 检查更新 + checkUpdate() { + if (wx.canIUse('getUpdateManager')) { + const updateManager = wx.getUpdateManager() + + updateManager.onCheckForUpdate((res) => { + if (res.hasUpdate) { + console.log('发现新版本') + } + }) + + updateManager.onUpdateReady(() => { + wx.showModal({ + title: '更新提示', + content: '新版本已准备好,是否重启应用?', + success: (res) => { + if (res.confirm) { + updateManager.applyUpdate() + } + } + }) + }) + + updateManager.onUpdateFailed(() => { + wx.showToast({ + title: '更新失败,请稍后重试', + icon: 'none' + }) + }) + } + }, + + /** + * 从 soul-api 返回体中取错误提示文案(兼容 message / error 字段) + */ + _getApiErrorMsg(data, defaultMsg = '请求失败') { + if (!data || typeof data !== 'object') return defaultMsg + const msg = data.message || data.error + return (msg && String(msg).trim()) ? String(msg).trim() : defaultMsg + }, + + /** + * 统一请求方法。接口失败时会弹窗提示(与 soul-api 返回的 message/error 一致)。 + * @param {string|object} urlOrOptions - 接口路径,或 { url, method, data, header, silent } + * @param {object} options - { method, data, header, silent } + * @param {boolean} options.silent - 为 true 时不弹窗,仅 reject(用于静默请求如访问统计) + */ + request(urlOrOptions, options = {}) { + let url + if (typeof urlOrOptions === 'string') { + url = urlOrOptions + } else if (urlOrOptions && typeof urlOrOptions === 'object' && urlOrOptions.url) { + url = urlOrOptions.url + options = { ...urlOrOptions, url: undefined } + } else { + url = '' + } + const silent = !!options.silent + const showError = (msg) => { + if (!silent && msg) { + wx.showToast({ title: msg, icon: 'none', duration: 2500 }) + } + } + + return new Promise((resolve, reject) => { + const token = wx.getStorageSync('token') + + wx.request({ + url: this.globalData.baseUrl + url, + method: options.method || 'GET', + data: options.data || {}, + header: { + 'Content-Type': 'application/json', + 'Authorization': token ? `Bearer ${token}` : '', + ...options.header + }, + success: (res) => { + const data = res.data + if (res.statusCode === 200) { + // 业务失败:success === false,soul-api 用 message 或 error 返回原因 + if (data && data.success === false) { + const msg = this._getApiErrorMsg(data, '操作失败') + showError(msg) + reject(new Error(msg)) + return + } + resolve(data) + return + } + if (res.statusCode === 401) { + this.logout() + showError('未授权,请重新登录') + reject(new Error('未授权')) + return + } + // 4xx/5xx:优先用返回体的 message/error + const msg = this._getApiErrorMsg(data, res.statusCode >= 500 ? '服务器异常,请稍后重试' : '请求失败') + showError(msg) + reject(new Error(msg)) + }, + fail: (err) => { + const msg = (err && err.errMsg) ? (err.errMsg.indexOf('timeout') !== -1 ? '请求超时,请重试' : '网络异常,请重试') : '网络异常,请重试' + showError(msg) + reject(new Error(msg)) + } + }) + }) + }, + + // 登录方法 - 获取openId用于支付(加固错误处理,避免审核报“登录报错”) + async login() { + try { + const loginRes = await new Promise((resolve, reject) => { + wx.login({ success: resolve, fail: reject }) + }) + if (!loginRes || !loginRes.code) { + console.warn('[App] wx.login 未返回 code') + wx.showToast({ title: '获取登录态失败,请重试', icon: 'none' }) + return null + } + try { + const res = await this.request('/api/miniprogram/login', { + method: 'POST', + data: { code: loginRes.code } + }) + + if (res.success && res.data) { + // 保存openId + if (res.data.openId) { + this.globalData.openId = res.data.openId + wx.setStorageSync('openId', res.data.openId) + console.log('[App] 获取openId成功') + } + + // 保存用户信息 + if (res.data.user) { + this.globalData.userInfo = res.data.user + this.globalData.isLoggedIn = true + this.globalData.purchasedSections = res.data.user.purchasedSections || [] + this.globalData.hasFullBook = res.data.user.hasFullBook || false + + wx.setStorageSync('userInfo', res.data.user) + wx.setStorageSync('token', res.data.token || '') + + // 登录成功后,检查待绑定的推荐码并执行绑定 + const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode + if (pendingRef) { + console.log('[App] 登录后自动绑定推荐码:', pendingRef) + this.bindReferralCode(pendingRef) + } + } + + return res.data + } + } catch (apiError) { + console.log('[App] API登录失败:', apiError.message) + // 不使用模拟登录,提示用户网络问题 + wx.showToast({ title: '网络异常,请重试', icon: 'none' }) + return null + } + + return null + } catch (e) { + console.error('[App] 登录失败:', e) + wx.showToast({ title: '登录失败,请重试', icon: 'none' }) + return null + } + }, + + // 获取openId (支付必需) + async getOpenId() { + // 先检查缓存 + const cachedOpenId = wx.getStorageSync('openId') + if (cachedOpenId) { + this.globalData.openId = cachedOpenId + return cachedOpenId + } + + // 没有缓存则登录获取 + try { + const loginRes = await new Promise((resolve, reject) => { + wx.login({ success: resolve, fail: reject }) + }) + + const res = await this.request('/api/miniprogram/login', { + method: 'POST', + data: { code: loginRes.code } + }) + + if (res.success && res.data?.openId) { + this.globalData.openId = res.data.openId + wx.setStorageSync('openId', res.data.openId) + // 接口同时返回 user 时视为登录,补全登录态并从登录开始绑定推荐码 + if (res.data.user) { + this.globalData.userInfo = res.data.user + this.globalData.isLoggedIn = true + this.globalData.purchasedSections = res.data.user.purchasedSections || [] + this.globalData.hasFullBook = res.data.user.hasFullBook || false + wx.setStorageSync('userInfo', res.data.user) + wx.setStorageSync('token', res.data.token || '') + const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode + if (pendingRef) { + console.log('[App] getOpenId 登录后自动绑定推荐码:', pendingRef) + this.bindReferralCode(pendingRef) + } + } + return res.data.openId + } + } catch (e) { + console.error('[App] 获取openId失败:', e) + } + + return null + }, + + // 模拟登录已废弃 - 不再使用 + // 现在必须使用真实的微信登录获取openId作为唯一标识 + mockLogin() { + console.warn('[App] mockLogin已废弃,请使用真实登录') + return null + }, + + // 手机号登录:需同时传 wx.login 的 code 与 getPhoneNumber 的 phoneCode + async loginWithPhone(phoneCode) { + try { + const loginRes = await new Promise((resolve, reject) => { + wx.login({ success: resolve, fail: reject }) + }) + if (!loginRes.code) { + wx.showToast({ title: '获取登录态失败', icon: 'none' }) + return null + } + const res = await this.request('/api/miniprogram/phone-login', { + method: 'POST', + data: { code: loginRes.code, phoneCode } + }) + + if (res.success && res.data) { + this.globalData.userInfo = res.data.user + this.globalData.isLoggedIn = true + this.globalData.purchasedSections = res.data.user.purchasedSections || [] + this.globalData.hasFullBook = res.data.user.hasFullBook || false + + wx.setStorageSync('userInfo', res.data.user) + wx.setStorageSync('token', res.data.token) + + // 登录成功后绑定推荐码 + const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode + if (pendingRef) { + console.log('[App] 手机号登录后自动绑定推荐码:', pendingRef) + this.bindReferralCode(pendingRef) + } + + return res.data + } + } catch (e) { + console.log('[App] 手机号登录失败:', e) + wx.showToast({ title: '登录失败,请重试', icon: 'none' }) + } + + return null + }, + + // 退出登录 + logout() { + this.globalData.userInfo = null + this.globalData.isLoggedIn = false + this.globalData.purchasedSections = [] + this.globalData.hasFullBook = false + + wx.removeStorageSync('userInfo') + wx.removeStorageSync('token') + }, + + // 检查是否已购买章节 + hasPurchased(sectionId) { + if (this.globalData.hasFullBook) return true + return this.globalData.purchasedSections.includes(sectionId) + }, + + // 标记章节为已读(仅在有权限打开时由阅读页调用,用于首页已读/待读统计) + markSectionAsRead(sectionId) { + if (!sectionId) return + const list = this.globalData.readSectionIds || [] + if (list.includes(sectionId)) return + list.push(sectionId) + this.globalData.readSectionIds = list + wx.setStorageSync('readSectionIds', list) + }, + + // 已读章节数(用于首页展示) + getReadCount() { + return (this.globalData.readSectionIds || []).length + }, + + // 获取章节总数 + getTotalSections() { + return this.globalData.totalSections + }, + + // 切换TabBar + switchTab(index) { + this.globalData.currentTab = index + }, + + // 显示Toast + showToast(title, icon = 'none') { + wx.showToast({ + title, + icon, + duration: 2000 + }) + }, + + // 显示Loading + showLoading(title = '加载中...') { + wx.showLoading({ + title, + mask: true + }) + }, + + // 隐藏Loading + hideLoading() { + wx.hideLoading() + } +}) diff --git a/归档/miniprogram/app.json b/归档/miniprogram/app.json new file mode 100644 index 00000000..4bbe3721 --- /dev/null +++ b/归档/miniprogram/app.json @@ -0,0 +1,67 @@ +{ + "pages": [ + "pages/index/index", + "pages/chapters/chapters", + "pages/match/match", + "pages/my/my", + "pages/read/read", + "pages/about/about", + "pages/agreement/agreement", + "pages/privacy/privacy", + "pages/referral/referral", + "pages/purchases/purchases", + "pages/settings/settings", + "pages/search/search", + "pages/addresses/addresses", + "pages/addresses/edit", + "pages/withdraw-records/withdraw-records", + "pages/vip/vip", + "pages/member-detail/member-detail" + ], + "window": { + "backgroundTextStyle": "light", + "navigationBarBackgroundColor": "#000000", + "navigationBarTitleText": "Soul创业派对", + "navigationBarTextStyle": "white", + "backgroundColor": "#000000", + "navigationStyle": "custom" + }, + "tabBar": { + "custom": true, + "color": "#8e8e93", + "selectedColor": "#00CED1", + "backgroundColor": "#1c1c1e", + "borderStyle": "black", + "list": [ + { + "pagePath": "pages/index/index", + "text": "首页" + }, + { + "pagePath": "pages/chapters/chapters", + "text": "目录" + }, + { + "pagePath": "pages/match/match", + "text": "找伙伴" + }, + { + "pagePath": "pages/my/my", + "text": "我的" + } + ] + }, + "usingComponents": {}, + "__usePrivacyCheck__": true, + "permission": { + "scope.userLocation": { + "desc": "用于匹配附近的书友" + } + }, + "requiredPrivateInfos": [ + "getLocation" + ], + "lazyCodeLoading": "requiredComponents", + "style": "v2", + "sitemapLocation": "sitemap.json" +} \ No newline at end of file diff --git a/归档/miniprogram/app.wxss b/归档/miniprogram/app.wxss new file mode 100644 index 00000000..9ce22a06 --- /dev/null +++ b/归档/miniprogram/app.wxss @@ -0,0 +1,606 @@ +/** + * Soul创业实验 - 全局样式 + * 主题色: #00CED1 (Soul青色) + * 开发: 卡若 + */ + +/* ===== CSS 变量系统 ===== */ +page { + /* 品牌色 */ + --app-brand: #00CED1; + --app-brand-light: rgba(0, 206, 209, 0.1); + --app-brand-dark: #20B2AA; + + /* 背景色 */ + --app-bg-primary: #000000; + --app-bg-secondary: #1c1c1e; + --app-bg-tertiary: #2c2c2e; + + /* 文字色 */ + --app-text-primary: #ffffff; + --app-text-secondary: rgba(255, 255, 255, 0.7); + --app-text-tertiary: rgba(255, 255, 255, 0.4); + + /* 分隔线 */ + --app-separator: rgba(255, 255, 255, 0.05); + + /* iOS 系统色 */ + --ios-indigo: #5856D6; + --ios-green: #30d158; + --ios-red: #FF3B30; + --ios-orange: #FF9500; + --ios-yellow: #FFD700; + + /* 金色 */ + --gold: #FFD700; + --gold-light: #FFA500; + + /* 粉色 */ + --pink: #E91E63; + + /* 紫色 */ + --purple: #7B61FF; +} + +/* ===== 页面基础样式 ===== */ +page { + background-color: var(--app-bg-primary); + color: var(--app-text-primary); + font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', 'PingFang SC', 'Microsoft YaHei', sans-serif; + font-size: 28rpx; + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +/* ===== 全局容器 ===== */ +.container { + min-height: 100vh; + padding: 0; + background: #000000; + padding-bottom: env(safe-area-inset-bottom); +} + +/* ===== 品牌色系 ===== */ +.brand-color { + color: #00CED1; +} + +.brand-bg { + background-color: #00CED1; +} + +.brand-gradient { + background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); +} + +.gold-color { + color: #FFD700; +} + +.gold-bg { + background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); +} + +/* ===== 文字渐变 ===== */ +.gradient-text { + background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.gold-gradient-text { + background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* ===== 按钮样式 ===== */ +.btn-primary { + background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); + color: #ffffff; + border: none; + border-radius: 48rpx; + padding: 28rpx 48rpx; + font-size: 32rpx; + font-weight: 600; + box-shadow: 0 8rpx 24rpx rgba(0, 206, 209, 0.3); + display: flex; + align-items: center; + justify-content: center; +} + +.btn-primary::after { + border: none; +} + +.btn-primary:active { + opacity: 0.85; + transform: scale(0.98); +} + +.btn-secondary { + background: rgba(0, 206, 209, 0.1); + color: #00CED1; + border: 2rpx solid rgba(0, 206, 209, 0.3); + border-radius: 48rpx; + padding: 28rpx 48rpx; + font-size: 32rpx; + font-weight: 500; +} + +.btn-secondary::after { + border: none; +} + +.btn-secondary:active { + background: rgba(0, 206, 209, 0.2); +} + +.btn-ghost { + background: rgba(255, 255, 255, 0.05); + color: #ffffff; + border: 2rpx solid rgba(255, 255, 255, 0.1); + border-radius: 48rpx; + padding: 28rpx 48rpx; + font-size: 32rpx; +} + +.btn-ghost::after { + border: none; +} + +.btn-ghost:active { + background: rgba(255, 255, 255, 0.1); +} + +.btn-gold { + background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); + color: #000000; + border: none; + border-radius: 48rpx; + padding: 28rpx 48rpx; + font-size: 32rpx; + font-weight: 600; + box-shadow: 0 8rpx 24rpx rgba(255, 215, 0, 0.3); +} + +.btn-gold::after { + border: none; +} + +/* ===== 卡片样式 ===== */ +.card { + background: rgba(28, 28, 30, 0.9); + border-radius: 32rpx; + padding: 32rpx; + margin: 24rpx 32rpx; + border: 2rpx solid rgba(255, 255, 255, 0.05); +} + +.card-light { + background: rgba(44, 44, 46, 0.8); + border-radius: 24rpx; + padding: 24rpx; + border: 2rpx solid rgba(255, 255, 255, 0.08); +} + +.card-gradient { + background: linear-gradient(135deg, rgba(28, 28, 30, 1) 0%, rgba(44, 44, 46, 1) 100%); + border-radius: 32rpx; + padding: 32rpx; + border: 2rpx solid rgba(0, 206, 209, 0.2); +} + +.card-brand { + background: linear-gradient(135deg, rgba(0, 206, 209, 0.1) 0%, rgba(32, 178, 170, 0.05) 100%); + border-radius: 32rpx; + padding: 32rpx; + border: 2rpx solid rgba(0, 206, 209, 0.2); +} + +/* ===== 输入框样式 ===== */ +.input-ios { + background: rgba(0, 0, 0, 0.3); + border: 2rpx solid rgba(255, 255, 255, 0.1); + border-radius: 24rpx; + padding: 28rpx 32rpx; + font-size: 32rpx; + color: #ffffff; +} + +.input-ios:focus { + border-color: rgba(0, 206, 209, 0.5); +} + +.input-ios-placeholder { + color: rgba(255, 255, 255, 0.3); +} + +/* ===== 列表项样式 ===== */ +.list-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 28rpx 32rpx; + background: rgba(28, 28, 30, 0.9); + border-bottom: 1rpx solid rgba(255, 255, 255, 0.05); +} + +.list-item:first-child { + border-radius: 24rpx 24rpx 0 0; +} + +.list-item:last-child { + border-radius: 0 0 24rpx 24rpx; + border-bottom: none; +} + +.list-item:only-child { + border-radius: 24rpx; +} + +.list-item:active { + background: rgba(44, 44, 46, 1); +} + +/* ===== 标签样式 ===== */ +.tag { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8rpx 20rpx; + min-width: 80rpx; + border-radius: 8rpx; + font-size: 22rpx; + font-weight: 500; + box-sizing: border-box; + text-align: center; +} + +.tag-brand { + background: rgba(0, 206, 209, 0.1); + color: #00CED1; +} + +.tag-gold { + background: rgba(255, 215, 0, 0.1); + color: #FFD700; +} + +.tag-pink { + background: rgba(233, 30, 99, 0.1); + color: #E91E63; +} + +.tag-purple { + background: rgba(123, 97, 255, 0.1); + color: #7B61FF; +} + +.tag-free { + background: rgba(0, 206, 209, 0.1); + color: #00CED1; +} + +/* ===== 分隔线 ===== */ +.divider { + height: 1rpx; + background: rgba(255, 255, 255, 0.05); + margin: 24rpx 0; +} + +.divider-vertical { + width: 2rpx; + height: 48rpx; + background: rgba(255, 255, 255, 0.1); +} + +/* ===== 骨架屏动画 ===== */ +.skeleton { + background: linear-gradient(90deg, + rgba(28, 28, 30, 1) 25%, + rgba(44, 44, 46, 1) 50%, + rgba(28, 28, 30, 1) 75% + ); + background-size: 200% 100%; + animation: skeleton-loading 1.5s ease-in-out infinite; + border-radius: 8rpx; +} + +@keyframes skeleton-loading { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +/* ===== 页面过渡动画 ===== */ +.page-transition { + animation: fadeIn 0.3s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20rpx); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ===== 弹窗动画 ===== */ +.modal-overlay { + animation: modalOverlayIn 0.25s ease-out; +} + +.modal-content { + animation: modalContentIn 0.3s cubic-bezier(0.32, 0.72, 0, 1); +} + +@keyframes modalOverlayIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes modalContentIn { + from { + opacity: 0; + transform: scale(0.95) translateY(20rpx); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +/* ===== 脉动动画 ===== */ +.pulse { + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.05); + opacity: 0.8; + } +} + +/* ===== 发光效果 ===== */ +.glow { + box-shadow: 0 0 40rpx rgba(0, 206, 209, 0.3); +} + +.glow-gold { + box-shadow: 0 0 40rpx rgba(255, 215, 0, 0.3); +} + +/* ===== 文字样式 ===== */ +.text-xs { + font-size: 22rpx; +} + +.text-sm { + font-size: 26rpx; +} + +.text-base { + font-size: 28rpx; +} + +.text-lg { + font-size: 32rpx; +} + +.text-xl { + font-size: 36rpx; +} + +.text-2xl { + font-size: 44rpx; +} + +.text-3xl { + font-size: 56rpx; +} + +.text-white { + color: #ffffff; +} + +.text-gray { + color: rgba(255, 255, 255, 0.6); +} + +.text-muted { + color: rgba(255, 255, 255, 0.4); +} + +.text-center { + text-align: center; +} + +.font-medium { + font-weight: 500; +} + +.font-semibold { + font-weight: 600; +} + +.font-bold { + font-weight: 700; +} + +/* ===== Flex布局 ===== */ +.flex { + display: flex; +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.justify-around { + justify-content: space-around; +} + +.flex-1 { + flex: 1; +} + +.gap-1 { + gap: 8rpx; +} + +.gap-2 { + gap: 16rpx; +} + +.gap-3 { + gap: 24rpx; +} + +.gap-4 { + gap: 32rpx; +} + +/* ===== 间距 ===== */ +.p-2 { padding: 16rpx; } +.p-3 { padding: 24rpx; } +.p-4 { padding: 32rpx; } +.p-5 { padding: 40rpx; } + +.px-4 { padding-left: 32rpx; padding-right: 32rpx; } +.py-2 { padding-top: 16rpx; padding-bottom: 16rpx; } +.py-3 { padding-top: 24rpx; padding-bottom: 24rpx; } + +.m-4 { margin: 32rpx; } +.mx-4 { margin-left: 32rpx; margin-right: 32rpx; } +.my-3 { margin-top: 24rpx; margin-bottom: 24rpx; } +.mb-2 { margin-bottom: 16rpx; } +.mb-3 { margin-bottom: 24rpx; } +.mb-4 { margin-bottom: 32rpx; } +.mt-4 { margin-top: 32rpx; } + +/* ===== 圆角 ===== */ +.rounded { border-radius: 8rpx; } +.rounded-lg { border-radius: 16rpx; } +.rounded-xl { border-radius: 24rpx; } +.rounded-2xl { border-radius: 32rpx; } +.rounded-full { border-radius: 50%; } + +/* ===== 安全区域 ===== */ +.safe-bottom { + padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx); +} + +.pb-tabbar { + padding-bottom: 200rpx; +} + +/* ===== 头部导航占位 ===== */ +.nav-placeholder { + height: calc(88rpx + env(safe-area-inset-top, 44rpx)); +} + +/* ===== 隐藏滚动条 ===== */ +::-webkit-scrollbar { + display: none; + width: 0; + height: 0; +} + +/* ===== 触摸反馈 ===== */ +.touch-feedback { + transition: all 0.15s ease; +} + +.touch-feedback:active { + opacity: 0.7; + transform: scale(0.98); +} + +/* ===== 进度条 ===== */ +.progress-bar { + height: 8rpx; + background: rgba(44, 44, 46, 1); + border-radius: 4rpx; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #00CED1 0%, #20B2AA 100%); + border-radius: 4rpx; + transition: width 0.3s ease; +} + +/* ===== 头像样式 ===== */ +.avatar { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + background: linear-gradient(135deg, rgba(0, 206, 209, 0.2) 0%, rgba(32, 178, 170, 0.1) 100%); + display: flex; + align-items: center; + justify-content: center; + color: #00CED1; + font-weight: 700; + font-size: 32rpx; + border: 4rpx solid rgba(0, 206, 209, 0.3); +} + +.avatar-lg { + width: 120rpx; + height: 120rpx; + font-size: 48rpx; +} + +/* ===== 图标容器 ===== */ +.icon-box { + width: 64rpx; + height: 64rpx; + border-radius: 16rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.icon-box-brand { + background: linear-gradient(135deg, rgba(0, 206, 209, 0.2) 0%, rgba(32, 178, 170, 0.1) 100%); +} + +.icon-box-gold { + background: linear-gradient(135deg, rgba(255, 215, 0, 0.2) 0%, rgba(255, 165, 0, 0.1) 100%); +} + +/* ===== 渐变背景 ===== */ +.bg-gradient-dark { + background: linear-gradient(180deg, #000000 0%, #1a1a1a 100%); +} + +.bg-gradient-brand { + background: linear-gradient(135deg, rgba(0, 206, 209, 0.1) 0%, transparent 100%); +} diff --git a/归档/miniprogram/assets/icons/alert-circle.svg b/归档/miniprogram/assets/icons/alert-circle.svg new file mode 100644 index 00000000..f5a441f3 --- /dev/null +++ b/归档/miniprogram/assets/icons/alert-circle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/归档/miniprogram/assets/icons/arrow-right.svg b/归档/miniprogram/assets/icons/arrow-right.svg new file mode 100644 index 00000000..1dc64d3f --- /dev/null +++ b/归档/miniprogram/assets/icons/arrow-right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/归档/miniprogram/assets/icons/bell.svg b/归档/miniprogram/assets/icons/bell.svg new file mode 100644 index 00000000..0e7e405b --- /dev/null +++ b/归档/miniprogram/assets/icons/bell.svg @@ -0,0 +1,4 @@ + + + + diff --git a/归档/miniprogram/assets/icons/book-open.svg b/归档/miniprogram/assets/icons/book-open.svg new file mode 100644 index 00000000..d833e86b --- /dev/null +++ b/归档/miniprogram/assets/icons/book-open.svg @@ -0,0 +1,4 @@ + + + + diff --git a/归档/miniprogram/assets/icons/book.svg b/归档/miniprogram/assets/icons/book.svg new file mode 100644 index 00000000..93579576 --- /dev/null +++ b/归档/miniprogram/assets/icons/book.svg @@ -0,0 +1,4 @@ + + + + diff --git a/归档/miniprogram/assets/icons/chevron-left.svg b/归档/miniprogram/assets/icons/chevron-left.svg new file mode 100644 index 00000000..e406b2b9 --- /dev/null +++ b/归档/miniprogram/assets/icons/chevron-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/归档/miniprogram/assets/icons/gift.svg b/归档/miniprogram/assets/icons/gift.svg new file mode 100644 index 00000000..66ac806c --- /dev/null +++ b/归档/miniprogram/assets/icons/gift.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/归档/miniprogram/assets/icons/home-active.png b/归档/miniprogram/assets/icons/home-active.png new file mode 100644 index 00000000..b6090d87 Binary files /dev/null and b/归档/miniprogram/assets/icons/home-active.png differ diff --git a/归档/miniprogram/assets/icons/home.png b/归档/miniprogram/assets/icons/home.png new file mode 100644 index 00000000..0ffba614 Binary files /dev/null and b/归档/miniprogram/assets/icons/home.png differ diff --git a/归档/miniprogram/assets/icons/home.svg b/归档/miniprogram/assets/icons/home.svg new file mode 100644 index 00000000..76244091 --- /dev/null +++ b/归档/miniprogram/assets/icons/home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/归档/miniprogram/assets/icons/image.svg b/归档/miniprogram/assets/icons/image.svg new file mode 100644 index 00000000..50ed9e6d --- /dev/null +++ b/归档/miniprogram/assets/icons/image.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/归档/miniprogram/assets/icons/list.svg b/归档/miniprogram/assets/icons/list.svg new file mode 100644 index 00000000..688326aa --- /dev/null +++ b/归档/miniprogram/assets/icons/list.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/归档/miniprogram/assets/icons/match-active.png b/归档/miniprogram/assets/icons/match-active.png new file mode 100644 index 00000000..da62b436 Binary files /dev/null and b/归档/miniprogram/assets/icons/match-active.png differ diff --git a/归档/miniprogram/assets/icons/match.png b/归档/miniprogram/assets/icons/match.png new file mode 100644 index 00000000..b15582e3 Binary files /dev/null and b/归档/miniprogram/assets/icons/match.png differ diff --git a/归档/miniprogram/assets/icons/message-circle.svg b/归档/miniprogram/assets/icons/message-circle.svg new file mode 100644 index 00000000..037560e9 --- /dev/null +++ b/归档/miniprogram/assets/icons/message-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/归档/miniprogram/assets/icons/my-active.png b/归档/miniprogram/assets/icons/my-active.png new file mode 100644 index 00000000..da62b436 Binary files /dev/null and b/归档/miniprogram/assets/icons/my-active.png differ diff --git a/归档/miniprogram/assets/icons/my.png b/归档/miniprogram/assets/icons/my.png new file mode 100644 index 00000000..b15582e3 Binary files /dev/null and b/归档/miniprogram/assets/icons/my.png differ diff --git a/归档/miniprogram/assets/icons/partners.svg b/归档/miniprogram/assets/icons/partners.svg new file mode 100644 index 00000000..80668312 --- /dev/null +++ b/归档/miniprogram/assets/icons/partners.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/归档/miniprogram/assets/icons/settings.svg b/归档/miniprogram/assets/icons/settings.svg new file mode 100644 index 00000000..c7006ea8 --- /dev/null +++ b/归档/miniprogram/assets/icons/settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/归档/miniprogram/assets/icons/share.svg b/归档/miniprogram/assets/icons/share.svg new file mode 100644 index 00000000..93179fc2 --- /dev/null +++ b/归档/miniprogram/assets/icons/share.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/归档/miniprogram/assets/icons/sparkles.svg b/归档/miniprogram/assets/icons/sparkles.svg new file mode 100644 index 00000000..e2a4461f --- /dev/null +++ b/归档/miniprogram/assets/icons/sparkles.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/归档/miniprogram/assets/icons/user.svg b/归档/miniprogram/assets/icons/user.svg new file mode 100644 index 00000000..8b190427 --- /dev/null +++ b/归档/miniprogram/assets/icons/user.svg @@ -0,0 +1,4 @@ + + + + diff --git a/归档/miniprogram/assets/icons/users.svg b/归档/miniprogram/assets/icons/users.svg new file mode 100644 index 00000000..4816094b --- /dev/null +++ b/归档/miniprogram/assets/icons/users.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/归档/miniprogram/assets/icons/wallet.svg b/归档/miniprogram/assets/icons/wallet.svg new file mode 100644 index 00000000..6d431e54 --- /dev/null +++ b/归档/miniprogram/assets/icons/wallet.svg @@ -0,0 +1,4 @@ + + + + diff --git a/归档/miniprogram/components/icon/README.md b/归档/miniprogram/components/icon/README.md new file mode 100644 index 00000000..34e394c8 --- /dev/null +++ b/归档/miniprogram/components/icon/README.md @@ -0,0 +1,175 @@ +# Icon 图标组件 + +SVG 图标组件,参考 lucide-react 实现,用于在小程序中使用矢量图标。 + +**技术实现**: 使用 Base64 编码的 SVG + image 组件(小程序不支持直接使用 SVG 标签) + +--- + +## 使用方法 + +### 1. 在页面 JSON 中引入组件 + +```json +{ + "usingComponents": { + "icon": "/components/icon/icon" + } +} +``` + +### 2. 在 WXML 中使用 + +```xml + + + + + + + + + + + + + + + + + +``` + +--- + +## 属性说明 + +| 属性 | 类型 | 默认值 | 说明 | +|-----|------|--------|-----| +| name | String | 'share' | 图标名称 | +| size | Number | 48 | 图标大小(rpx) | +| color | String | 'currentColor' | 图标颜色 | +| customClass | String | '' | 自定义类名 | +| customStyle | String | '' | 自定义样式 | + +--- + +## 可用图标 + +| 图标名称 | 说明 | 对应 lucide-react | +|---------|------|-------------------| +| `share` | 分享 | `` | +| `arrow-up-right` | 右上箭头 | `` | +| `chevron-left` | 左箭头 | `` | +| `search` | 搜索 | `` | +| `heart` | 心形 | `` | + +--- + +## 添加新图标 + +在 `icon.js` 的 `getSvgPath` 方法中添加新图标: + +```javascript +getSvgPath(name) { + const svgMap = { + 'new-icon': '', + // ... 其他图标 + } + return svgMap[name] || '' +} +``` + +**获取 SVG 代码**: 访问 [lucide.dev](https://lucide.dev) 搜索图标,复制 SVG 内容。 +**注意**: 颜色使用 `COLOR` 占位符,组件会自动替换。 + +--- + +## 样式定制 + +### 1. 使用 customClass + +```xml + +``` + +```css +.my-icon-class { + opacity: 0.8; +} +``` + +### 2. 使用 customStyle + +```xml + +``` + +--- + +## 技术说明 + +### 为什么使用 Base64 + image? + +1. **矢量图标**:任意缩放不失真 +2. **灵活着色**:通过 `COLOR` 占位符动态改变颜色 +3. **轻量级**:无需加载字体文件或外部图片 +4. **兼容性**:小程序不支持直接使用 SVG 标签,image 组件支持 Base64 SVG + +### 为什么不用字体图标? + +小程序对字体文件有限制,Base64 编码字体文件会增加包体积,SVG 图标更轻量。 + +### 与 lucide-react 的对应关系 + +- **lucide-react**: React 组件库,使用 SVG +- **本组件**: 小程序自定义组件,也使用 SVG +- **SVG path 数据**: 完全相同,从 lucide 官网复制 + +--- + +## 示例 + +### 悬浮分享按钮 + +```xml + +``` + +```css +.fab-share { + position: fixed; + right: 32rpx; + bottom: calc(120rpx + env(safe-area-inset-bottom)); + width: 96rpx; + height: 96rpx; + border-radius: 50%; + background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); + display: flex; + align-items: center; + justify-content: center; +} +``` + +--- + +## 扩展图标库 + +可以继续添加更多 lucide-react 图标: + +- `star` - 星星 +- `wallet` - 钱包 +- `gift` - 礼物 +- `info` - 信息 +- `settings` - 设置 +- `user` - 用户 +- `book-open` - 打开的书 +- `eye` - 眼睛 +- `clock` - 时钟 +- `users` - 用户组 + +--- + +**图标组件创建完成!** 🎉 diff --git a/归档/miniprogram/components/icon/icon.js b/归档/miniprogram/components/icon/icon.js new file mode 100644 index 00000000..b2dec23f --- /dev/null +++ b/归档/miniprogram/components/icon/icon.js @@ -0,0 +1,83 @@ +// components/icon/icon.js +Component({ + properties: { + // 图标名称 + name: { + type: String, + value: 'share', + observer: 'updateIcon' + }, + // 图标大小(rpx) + size: { + type: Number, + value: 48 + }, + // 图标颜色 + color: { + type: String, + value: '#ffffff', + observer: 'updateIcon' + }, + // 自定义类名 + customClass: { + type: String, + value: '' + }, + // 自定义样式 + customStyle: { + type: String, + value: '' + } + }, + + data: { + svgData: '' + }, + + lifetimes: { + attached() { + this.updateIcon() + } + }, + + methods: { + // SVG 图标数据映射 + getSvgPath(name) { + const svgMap = { + 'share': '', + + 'arrow-up-right': '', + + 'chevron-left': '', + + 'search': '', + + 'heart': '' + } + + return svgMap[name] || '' + }, + + // 更新图标 + updateIcon() { + const { name, color } = this.data + let svgString = this.getSvgPath(name) + + if (svgString) { + // 替换颜色占位符 + svgString = svgString.replace(/COLOR/g, color) + + // 转换为 Base64 Data URL + const svgData = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}` + + this.setData({ + svgData: svgData + }) + } else { + this.setData({ + svgData: '' + }) + } + } + } +}) diff --git a/归档/miniprogram/components/icon/icon.json b/归档/miniprogram/components/icon/icon.json new file mode 100644 index 00000000..a89ef4db --- /dev/null +++ b/归档/miniprogram/components/icon/icon.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/归档/miniprogram/components/icon/icon.wxml b/归档/miniprogram/components/icon/icon.wxml new file mode 100644 index 00000000..b1c29a25 --- /dev/null +++ b/归档/miniprogram/components/icon/icon.wxml @@ -0,0 +1,5 @@ + + + + {{name}} + diff --git a/归档/miniprogram/components/icon/icon.wxss b/归档/miniprogram/components/icon/icon.wxss new file mode 100644 index 00000000..d12d2a0a --- /dev/null +++ b/归档/miniprogram/components/icon/icon.wxss @@ -0,0 +1,18 @@ +/* components/icon/icon.wxss */ +.icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.icon-image { + display: block; + width: 100%; + height: 100%; +} + +.icon-text { + font-size: 24rpx; + color: currentColor; +} diff --git a/归档/miniprogram/custom-tab-bar/index.js b/归档/miniprogram/custom-tab-bar/index.js new file mode 100644 index 00000000..4acd9546 --- /dev/null +++ b/归档/miniprogram/custom-tab-bar/index.js @@ -0,0 +1,153 @@ +/** + * Soul创业实验 - 自定义TabBar组件 + * 根据后台配置动态显示/隐藏"找伙伴"按钮 + */ + +console.log('[TabBar] ===== 组件文件开始加载 =====') + +const app = getApp() +console.log('[TabBar] App 对象:', app) + +Component({ + data: { + selected: 0, + color: '#8e8e93', + selectedColor: '#00CED1', + matchEnabled: false, // 找伙伴功能开关,默认关闭 + list: [ + { + pagePath: '/pages/index/index', + text: '首页', + iconType: 'home' + }, + { + pagePath: '/pages/chapters/chapters', + text: '目录', + iconType: 'list' + }, + { + pagePath: '/pages/match/match', + text: '找伙伴', + iconType: 'match', + isSpecial: true + }, + { + pagePath: '/pages/my/my', + text: '我的', + iconType: 'user' + } + ] + }, + + lifetimes: { + attached() { + console.log('[TabBar] Component attached 生命周期触发') + this.loadFeatureConfig() + }, + ready() { + console.log('[TabBar] Component ready 生命周期触发') + // 如果 attached 中没有成功加载,在 ready 中再次尝试 + if (this.data.matchEnabled === undefined || this.data.matchEnabled === null) { + console.log('[TabBar] 在 ready 中重新加载配置') + this.loadFeatureConfig() + } + } + }, + + // 页面加载时也调用(兼容性更好) + attached() { + console.log('[TabBar] attached() 方法触发') + this.loadFeatureConfig() + }, + + methods: { + // 加载功能配置 + async loadFeatureConfig() { + try { + console.log('[TabBar] 开始加载功能配置...') + console.log('[TabBar] API地址:', app.globalData.baseUrl + '/api/miniprogram/config') + + // app.request 的第一个参数是 url 字符串,第二个参数是 options 对象 + const res = await app.request('/api/miniprogram/config', { + method: 'GET' + }) + + + // 兼容两种返回格式 + let matchEnabled = false + + if (res && res.success && res.features) { + console.log('[TabBar] features配置:', JSON.stringify(res.features)) + matchEnabled = res.features.matchEnabled === true + console.log('[TabBar] matchEnabled值:', matchEnabled) + } else if (res && res.configs && res.configs.feature_config) { + // 备用格式:从 configs.feature_config 读取 + console.log('[TabBar] 使用备用格式,从configs读取') + matchEnabled = res.configs.feature_config.matchEnabled === true + console.log('[TabBar] matchEnabled值:', matchEnabled) + } else { + console.log('[TabBar] ⚠️ 未找到features配置,使用默认值false') + console.log('[TabBar] res对象keys:', Object.keys(res || {})) + } + + this.setData({ matchEnabled }, () => { + console.log('[TabBar] ✅ matchEnabled已设置为:', this.data.matchEnabled) + // 配置加载完成后,根据当前路由设置选中状态 + this.updateSelected() + }) + + // 如果当前在找伙伴页面,但功能已关闭,跳转到首页 + if (!matchEnabled) { + const pages = getCurrentPages() + const currentPage = pages[pages.length - 1] + if (currentPage && currentPage.route === 'pages/match/match') { + console.log('[TabBar] 找伙伴功能已关闭,从match页面跳转到首页') + wx.switchTab({ url: '/pages/index/index' }) + } + } + } catch (error) { + console.log('[TabBar] ❌ 加载功能配置失败:', error) + console.log('[TabBar] 错误详情:', error.message || error) + // 默认关闭找伙伴功能 + this.setData({ matchEnabled: false }, () => { + this.updateSelected() + }) + } + }, + + // 根据当前路由更新选中状态 + updateSelected() { + const pages = getCurrentPages() + if (pages.length === 0) return + + const currentPage = pages[pages.length - 1] + const route = currentPage.route + + let selected = 0 + const { matchEnabled } = this.data + + // 根据路由匹配对应的索引 + if (route === 'pages/index/index') { + selected = 0 + } else if (route === 'pages/chapters/chapters') { + selected = 1 + } else if (route === 'pages/match/match') { + selected = 2 + } else if (route === 'pages/my/my') { + selected = matchEnabled ? 3 : 2 + } + + this.setData({ selected }) + }, + + switchTab(e) { + const data = e.currentTarget.dataset + const url = data.path + const index = data.index + + if (this.data.selected === index) return + + wx.switchTab({ url }) + } + } +}) diff --git a/归档/miniprogram/custom-tab-bar/index.json b/归档/miniprogram/custom-tab-bar/index.json new file mode 100644 index 00000000..467ce294 --- /dev/null +++ b/归档/miniprogram/custom-tab-bar/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} diff --git a/归档/miniprogram/custom-tab-bar/index.wxml b/归档/miniprogram/custom-tab-bar/index.wxml new file mode 100644 index 00000000..73369b2a --- /dev/null +++ b/归档/miniprogram/custom-tab-bar/index.wxml @@ -0,0 +1,47 @@ + + + + + + + + + + {{list[0].text}} + + + + + + + + {{list[1].text}} + + + + + + + + {{list[2].text}} + + + + + + + + {{list[3].text}} + + diff --git a/归档/miniprogram/custom-tab-bar/index.wxss b/归档/miniprogram/custom-tab-bar/index.wxss new file mode 100644 index 00000000..98036655 --- /dev/null +++ b/归档/miniprogram/custom-tab-bar/index.wxss @@ -0,0 +1,121 @@ +/** + * Soul创业实验 - 自定义TabBar样式 + * 实现中间突出的"找伙伴"按钮 + */ + +.tab-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 100rpx; + background: rgba(28, 28, 30, 0.95); + backdrop-filter: blur(40rpx); + -webkit-backdrop-filter: blur(40rpx); + display: flex; + align-items: flex-end; + padding-bottom: env(safe-area-inset-bottom); + z-index: 999; +} + +/* 三个tab布局(找伙伴功能关闭时) */ +.tab-bar-three .tab-bar-item { + flex: 1; +} + +/* 四个tab布局(找伙伴功能开启时) */ +.tab-bar-four .tab-bar-item { + flex: 1; +} + +.tab-bar-border { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1rpx; + background: rgba(255, 255, 255, 0.05); +} + +.tab-bar-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 10rpx 0 16rpx; +} + +.icon-wrapper { + width: 48rpx; + height: 48rpx; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 4rpx; +} + +.icon { + width: 44rpx; + height: 44rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.tab-bar-text { + font-size: 22rpx; + line-height: 1; +} + +/* ===== SVG 图标样式 ===== */ +.tab-icon { + width: 48rpx; + height: 48rpx; + display: block; + filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); +} + +.tab-icon.icon-active { + filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); +} + + +/* ===== 找伙伴 - 中间特殊按钮 ===== */ +.special-item { + position: relative; + margin-top: -32rpx; +} + +.special-button { + width: 112rpx; + height: 112rpx; + border-radius: 50%; + background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.4); + margin-bottom: 4rpx; + transition: all 0.2s ease; +} + +.special-button:active { + transform: scale(0.95); +} + +.special-active { + box-shadow: 0 8rpx 40rpx rgba(0, 206, 209, 0.6); +} + +.special-text { + margin-top: 4rpx; +} + +/* ===== 找伙伴特殊按钮图标 ===== */ +.special-icon { + width: 80rpx; + height: 80rpx; + display: block; + filter: brightness(0) saturate(100%) invert(100%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(100%) contrast(100%); +} diff --git a/归档/miniprogram/pages/about/about.js b/归档/miniprogram/pages/about/about.js new file mode 100644 index 00000000..8f19cc60 --- /dev/null +++ b/归档/miniprogram/pages/about/about.js @@ -0,0 +1,81 @@ +/** + * Soul创业派对 - 关于作者页 + * 开发: 卡若 + */ +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + author: { + name: '卡若', + avatar: 'K', + title: 'Soul派对房主理人 · 私域运营专家', + bio: '每天早上6点到9点,在Soul派对房分享真实的创业故事。专注私域运营与项目变现,用"云阿米巴"模式帮助创业者构建可持续的商业体系。本书记录了62个真实商业案例,涵盖电商、内容、传统行业等多个领域。', + stats: [ + { label: '商业案例', value: '62' }, + { label: '连续直播', value: '365天' }, + { label: '派对分享', value: '1000+' } + ], + // 联系方式已移至后台配置 + contact: null, + highlights: [ + '5年私域运营经验', + '帮助100+品牌从0到1增长', + '连续创业者,擅长商业模式设计' + ] + }, + bookInfo: { + title: '一场Soul的创业实验', + totalChapters: 62, + parts: [ + { name: '真实的人', chapters: 10 }, + { name: '真实的行业', chapters: 15 }, + { name: '真实的错误', chapters: 9 }, + { name: '真实的赚钱', chapters: 20 }, + { name: '真实的社会', chapters: 9 } + ], + price: 9.9 + } + }, + + onLoad() { + this.setData({ + statusBarHeight: app.globalData.statusBarHeight + }) + this.loadBookStats() + }, + + // 加载书籍统计 + async loadBookStats() { + try { + const res = await app.request('/api/miniprogram/book/stats') + if (res && res.success) { + this.setData({ + 'bookInfo.totalChapters': res.data?.totalChapters || 62, + 'author.stats': [ + { label: '商业案例', value: String(res.data?.totalChapters || 62) }, + { label: '连续直播', value: '365天' }, + { label: '派对分享', value: '1000+' } + ] + }) + } + } catch (e) { + console.log('[About] 加载书籍统计失败,使用默认值') + } + }, + + // 联系方式功能已禁用 + copyWechat() { + wx.showToast({ title: '请在派对房联系作者', icon: 'none' }) + }, + + callPhone() { + wx.showToast({ title: '请在派对房联系作者', icon: 'none' }) + }, + + // 返回 + goBack() { + wx.navigateBack() + } +}) diff --git a/归档/miniprogram/pages/about/about.json b/归档/miniprogram/pages/about/about.json new file mode 100644 index 00000000..e90e9960 --- /dev/null +++ b/归档/miniprogram/pages/about/about.json @@ -0,0 +1,4 @@ +{ + "usingComponents": {}, + "navigationStyle": "custom" +} diff --git a/归档/miniprogram/pages/about/about.wxml b/归档/miniprogram/pages/about/about.wxml new file mode 100644 index 00000000..598e9464 --- /dev/null +++ b/归档/miniprogram/pages/about/about.wxml @@ -0,0 +1,75 @@ + + + + + 关于作者 + + + + + + + + {{author.avatar}} + {{author.name}} + {{author.title}} + {{author.bio}} + + + + + {{item.value}} + {{item.label}} + + + + + + + + {{item}} + + + + + + + 📚 {{bookInfo.title}} + + + {{bookInfo.totalChapters}} + 篇章节 + + + 5 + 大篇章 + + + ¥{{bookInfo.price}} + 全书价格 + + + + + {{item.name}} + {{item.chapters}}节 + + + + + + + 联系作者 + + 🎉 + + Soul派对房 + 每天早上6-9点开播 + + + + 在Soul App搜索"创业实验"或"卡若",加入派对房直接交流 + + + + diff --git a/归档/miniprogram/pages/about/about.wxss b/归档/miniprogram/pages/about/about.wxss new file mode 100644 index 00000000..337aa041 --- /dev/null +++ b/归档/miniprogram/pages/about/about.wxss @@ -0,0 +1,40 @@ +.page { min-height: 100vh; background: #000; } +.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.9); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; } +.nav-back { width: 72rpx; height: 72rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; color: #fff; } +.nav-title { font-size: 36rpx; font-weight: 600; color: #00CED1; } +.nav-placeholder { width: 72rpx; } +.content { padding: 32rpx; } +.author-card { background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%); border-radius: 32rpx; padding: 48rpx; text-align: center; margin-bottom: 24rpx; border: 2rpx solid rgba(0,206,209,0.2); } +.author-avatar { width: 160rpx; height: 160rpx; border-radius: 50%; background: linear-gradient(135deg, #00CED1, #20B2AA); display: flex; align-items: center; justify-content: center; margin: 0 auto 24rpx; font-size: 64rpx; color: #fff; font-weight: 700; border: 4rpx solid rgba(0,206,209,0.3); } +.author-name { font-size: 40rpx; font-weight: 700; color: #fff; display: block; margin-bottom: 8rpx; } +.author-title { font-size: 26rpx; color: #00CED1; display: block; margin-bottom: 24rpx; } +.author-bio { font-size: 26rpx; color: rgba(255,255,255,0.7); line-height: 1.8; display: block; margin-bottom: 32rpx; } +.stats-row { display: flex; justify-content: space-around; padding-top: 32rpx; border-top: 2rpx solid rgba(255,255,255,0.1); } +.stat-item { text-align: center; } +.stat-value { font-size: 36rpx; font-weight: 700; color: #00CED1; display: block; } +.stat-label { font-size: 22rpx; color: rgba(255,255,255,0.5); } +.contact-card { background: #1c1c1e; border-radius: 32rpx; padding: 32rpx; } +.card-title { font-size: 28rpx; font-weight: 600; color: #fff; display: block; margin-bottom: 24rpx; } +.contact-item { display: flex; align-items: center; gap: 24rpx; padding: 24rpx; background: rgba(255,255,255,0.05); border-radius: 16rpx; margin-bottom: 16rpx; } +.contact-item:last-child { margin-bottom: 0; } +.contact-icon { font-size: 40rpx; } +.contact-info { flex: 1; } +.contact-label { font-size: 22rpx; color: rgba(255,255,255,0.5); display: block; } +.contact-value { font-size: 28rpx; color: #fff; } +.contact-btn { padding: 12rpx 24rpx; background: rgba(0,206,209,0.2); color: #00CED1; font-size: 24rpx; border-radius: 16rpx; } + +/* 亮点标签 */ +.highlights { display: flex; flex-wrap: wrap; gap: 16rpx; margin-top: 32rpx; padding-top: 24rpx; border-top: 2rpx solid rgba(255,255,255,0.1); justify-content: center; } +.highlight-tag { display: flex; align-items: center; gap: 8rpx; padding: 12rpx 24rpx; background: rgba(0,206,209,0.15); border-radius: 24rpx; font-size: 24rpx; color: rgba(255,255,255,0.8); } +.tag-icon { color: #00CED1; font-size: 22rpx; } + +/* 书籍信息卡片 */ +.book-info-card { background: #1c1c1e; border-radius: 32rpx; padding: 32rpx; margin-bottom: 24rpx; } +.book-stats { display: flex; justify-content: space-around; padding: 24rpx 0; margin: 16rpx 0; background: rgba(0,0,0,0.3); border-radius: 16rpx; } +.book-stat { text-align: center; } +.book-stat-value { font-size: 36rpx; font-weight: 700; color: #FFD700; display: block; } +.book-stat-label { font-size: 22rpx; color: rgba(255,255,255,0.5); } +.parts-list { display: flex; flex-wrap: wrap; gap: 12rpx; margin-top: 16rpx; } +.part-item { display: flex; align-items: center; gap: 8rpx; padding: 12rpx 20rpx; background: rgba(255,255,255,0.05); border-radius: 12rpx; } +.part-name { font-size: 24rpx; color: rgba(255,255,255,0.8); } +.part-chapters { font-size: 22rpx; color: #00CED1; } diff --git a/归档/miniprogram/pages/addresses/addresses.js b/归档/miniprogram/pages/addresses/addresses.js new file mode 100644 index 00000000..685528cf --- /dev/null +++ b/归档/miniprogram/pages/addresses/addresses.js @@ -0,0 +1,123 @@ +/** + * 收货地址列表页 + * 参考 Next.js: app/view/my/addresses/page.tsx + */ + +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + isLoggedIn: false, + addressList: [], + loading: true + }, + + onLoad() { + this.setData({ + statusBarHeight: app.globalData.statusBarHeight || 44 + }) + this.checkLogin() + }, + + onShow() { + if (this.data.isLoggedIn) { + this.loadAddresses() + } + }, + + // 检查登录状态 + checkLogin() { + const isLoggedIn = app.globalData.isLoggedIn + const userId = app.globalData.userInfo?.id + + if (!isLoggedIn || !userId) { + wx.showModal({ + title: '需要登录', + content: '请先登录后再管理收货地址', + confirmText: '去登录', + success: (res) => { + if (res.confirm) { + wx.switchTab({ url: '/pages/my/my' }) + } else { + wx.navigateBack() + } + } + }) + return + } + + this.setData({ isLoggedIn: true }) + this.loadAddresses() + }, + + // 加载地址列表 + async loadAddresses() { + const userId = app.globalData.userInfo?.id + if (!userId) return + + this.setData({ loading: true }) + + try { + const res = await app.request(`/api/miniprogram/user/addresses?userId=${userId}`) + if (res.success && res.list) { + this.setData({ + addressList: res.list, + loading: false + }) + } else { + this.setData({ addressList: [], loading: false }) + } + } catch (e) { + console.error('加载地址列表失败:', e) + this.setData({ loading: false }) + wx.showToast({ title: '加载失败', icon: 'none' }) + } + }, + + // 编辑地址 + editAddress(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ url: `/pages/addresses/edit?id=${id}` }) + }, + + // 删除地址 + deleteAddress(e) { + const id = e.currentTarget.dataset.id + + wx.showModal({ + title: '确认删除', + content: '确定要删除该收货地址吗?', + confirmColor: '#FF3B30', + success: async (res) => { + if (res.confirm) { + try { + const result = await app.request(`/api/miniprogram/user/addresses/${id}`, { + method: 'DELETE' + }) + + if (result.success) { + wx.showToast({ title: '删除成功', icon: 'success' }) + this.loadAddresses() + } else { + wx.showToast({ title: result.message || '删除失败', icon: 'none' }) + } + } catch (e) { + console.error('删除地址失败:', e) + wx.showToast({ title: '删除失败', icon: 'none' }) + } + } + } + }) + }, + + // 新增地址 + addAddress() { + wx.navigateTo({ url: '/pages/addresses/edit' }) + }, + + // 返回 + goBack() { + wx.navigateBack() + } +}) diff --git a/归档/miniprogram/pages/addresses/addresses.json b/归档/miniprogram/pages/addresses/addresses.json new file mode 100644 index 00000000..2e45b65e --- /dev/null +++ b/归档/miniprogram/pages/addresses/addresses.json @@ -0,0 +1,5 @@ +{ + "usingComponents": {}, + "navigationStyle": "custom", + "enablePullDownRefresh": false +} diff --git a/归档/miniprogram/pages/addresses/addresses.wxml b/归档/miniprogram/pages/addresses/addresses.wxml new file mode 100644 index 00000000..cec2ef6e --- /dev/null +++ b/归档/miniprogram/pages/addresses/addresses.wxml @@ -0,0 +1,66 @@ + + + + + + + + 收货地址 + + + + + + + + 加载中... + + + + + 📍 + 暂无收货地址 + 点击下方按钮添加 + + + + + + + {{item.name}} + {{item.phone}} + 默认 + + {{item.fullAddress}} + + + ✏️ + 编辑 + + + 🗑️ + 删除 + + + + + + + + + 新增收货地址 + + + diff --git a/归档/miniprogram/pages/addresses/addresses.wxss b/归档/miniprogram/pages/addresses/addresses.wxss new file mode 100644 index 00000000..9ff21637 --- /dev/null +++ b/归档/miniprogram/pages/addresses/addresses.wxss @@ -0,0 +1,217 @@ +/** + * 收货地址列表页样式 + */ + +.page { + min-height: 100vh; + background: #000000; + padding-bottom: 200rpx; +} + +/* ===== 导航栏 ===== */ +.nav-bar { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + background: rgba(0, 0, 0, 0.9); + backdrop-filter: blur(40rpx); + border-bottom: 1rpx solid rgba(255, 255, 255, 0.05); +} + +.nav-bar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 32rpx; + height: 88rpx; +} + +.nav-back { + width: 64rpx; + height: 64rpx; + border-radius: 50%; + background: rgba(255, 255, 255, 0.1); + display: flex; + align-items: center; + justify-content: center; +} + +.nav-back:active { + background: rgba(255, 255, 255, 0.15); +} + +.back-icon { + font-size: 48rpx; + color: #ffffff; + line-height: 1; +} + +.nav-title { + flex: 1; + text-align: center; + font-size: 36rpx; + font-weight: 600; + color: #ffffff; +} + +.nav-placeholder { + width: 64rpx; +} + +/* ===== 内容区 ===== */ +.content { + padding: 32rpx; +} + +/* ===== 加载状态 ===== */ +.loading-state { + padding: 240rpx 0; + text-align: center; +} + +.loading-text { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.4); +} + +/* ===== 空状态 ===== */ +.empty-state { + padding: 240rpx 0; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.empty-icon { + font-size: 96rpx; + margin-bottom: 24rpx; + opacity: 0.3; +} + +.empty-text { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.6); + margin-bottom: 16rpx; +} + +.empty-tip { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.4); +} + +/* ===== 地址列表 ===== */ +.address-list { + margin-bottom: 24rpx; +} + +.address-card { + background: #1c1c1e; + border-radius: 24rpx; + border: 2rpx solid rgba(255, 255, 255, 0.05); + padding: 32rpx; + margin-bottom: 24rpx; +} + +/* 地址头部 */ +.address-header { + display: flex; + align-items: center; + gap: 16rpx; + margin-bottom: 16rpx; +} + +.receiver-name { + font-size: 32rpx; + font-weight: 600; + color: #ffffff; +} + +.receiver-phone { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.5); +} + +.default-tag { + font-size: 22rpx; + color: #00CED1; + background: rgba(0, 206, 209, 0.2); + padding: 6rpx 16rpx; + border-radius: 8rpx; + margin-left: auto; +} + +/* 地址文本 */ +.address-text { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.6); + line-height: 1.6; + display: block; + margin-bottom: 24rpx; + padding-bottom: 24rpx; + border-bottom: 2rpx solid rgba(255, 255, 255, 0.05); +} + +/* 操作按钮 */ +.address-actions { + display: flex; + justify-content: flex-end; + gap: 32rpx; +} + +.action-btn { + display: flex; + align-items: center; + gap: 8rpx; + padding: 8rpx 0; +} + +.action-btn:active { + opacity: 0.6; +} + +.edit-btn { + color: #00CED1; +} + +.delete-btn { + color: #FF3B30; +} + +.action-icon { + font-size: 28rpx; +} + +.action-text { + font-size: 28rpx; +} + +/* ===== 新增按钮 ===== */ +.add-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 16rpx; + padding: 32rpx; + background: #00CED1; + border-radius: 24rpx; + font-weight: 600; + margin-top: 48rpx; +} + +.add-btn:active { + opacity: 0.8; + transform: scale(0.98); +} + +.add-icon { + font-size: 36rpx; + color: #000000; +} + +.add-text { + font-size: 32rpx; + color: #000000; +} diff --git a/归档/miniprogram/pages/addresses/edit.js b/归档/miniprogram/pages/addresses/edit.js new file mode 100644 index 00000000..9542c1dc --- /dev/null +++ b/归档/miniprogram/pages/addresses/edit.js @@ -0,0 +1,201 @@ +/** + * 地址编辑页(新增/编辑) + * 参考 Next.js: app/view/my/addresses/[id]/page.tsx + */ + +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + isEdit: false, // 是否为编辑模式 + addressId: null, + + // 表单数据 + name: '', + phone: '', + province: '', + city: '', + district: '', + detail: '', + isDefault: false, + + // 地区选择器 + region: [], + + saving: false + }, + + onLoad(options) { + this.setData({ + statusBarHeight: app.globalData.statusBarHeight || 44 + }) + + // 如果有 id 参数,则为编辑模式 + if (options.id) { + this.setData({ + isEdit: true, + addressId: options.id + }) + this.loadAddress(options.id) + } + }, + + // 加载地址详情(编辑模式) + async loadAddress(id) { + wx.showLoading({ title: '加载中...', mask: true }) + + try { + const res = await app.request(`/api/miniprogram/user/addresses/${id}`) + if (res.success && res.data) { + const addr = res.data + this.setData({ + name: addr.name || '', + phone: addr.phone || '', + province: addr.province || '', + city: addr.city || '', + district: addr.district || '', + detail: addr.detail || '', + isDefault: addr.isDefault || false, + region: [addr.province, addr.city, addr.district] + }) + } else { + wx.showToast({ title: '加载失败', icon: 'none' }) + } + } catch (e) { + console.error('加载地址详情失败:', e) + wx.showToast({ title: '加载失败', icon: 'none' }) + } finally { + wx.hideLoading() + } + }, + + // 表单输入 + onNameInput(e) { + this.setData({ name: e.detail.value }) + }, + + onPhoneInput(e) { + this.setData({ phone: e.detail.value.replace(/\D/g, '').slice(0, 11) }) + }, + + onDetailInput(e) { + this.setData({ detail: e.detail.value }) + }, + + // 地区选择 + onRegionChange(e) { + const region = e.detail.value + this.setData({ + region, + province: region[0], + city: region[1], + district: region[2] + }) + }, + + // 切换默认地址 + onDefaultChange(e) { + this.setData({ isDefault: e.detail.value }) + }, + + // 表单验证 + validateForm() { + const { name, phone, province, city, district, detail } = this.data + + if (!name || name.trim().length === 0) { + wx.showToast({ title: '请输入收货人姓名', icon: 'none' }) + return false + } + + if (!phone || phone.length !== 11) { + wx.showToast({ title: '请输入正确的手机号', icon: 'none' }) + return false + } + + if (!province || !city || !district) { + wx.showToast({ title: '请选择省市区', icon: 'none' }) + return false + } + + if (!detail || detail.trim().length === 0) { + wx.showToast({ title: '请输入详细地址', icon: 'none' }) + return false + } + + return true + }, + + // 保存地址 + async saveAddress() { + if (!this.validateForm()) return + if (this.data.saving) return + + this.setData({ saving: true }) + wx.showLoading({ title: '保存中...', mask: true }) + + const { isEdit, addressId, name, phone, province, city, district, detail, isDefault } = this.data + const userId = app.globalData.userInfo?.id + + if (!userId) { + wx.hideLoading() + wx.showToast({ title: '请先登录', icon: 'none' }) + this.setData({ saving: false }) + return + } + + const addressData = { + userId, + name, + phone, + province, + city, + district, + detail, + fullAddress: `${province}${city}${district}${detail}`, + isDefault + } + + try { + let res + if (isEdit) { + // 编辑模式 - PUT 请求 + res = await app.request(`/api/miniprogram/user/addresses/${addressId}`, { + method: 'PUT', + data: addressData + }) + } else { + // 新增模式 - POST 请求 + res = await app.request('/api/miniprogram/user/addresses', { + method: 'POST', + data: addressData + }) + } + + if (res.success) { + wx.hideLoading() + wx.showToast({ + title: isEdit ? '保存成功' : '添加成功', + icon: 'success' + }) + setTimeout(() => { + wx.navigateBack() + }, 1500) + } else { + wx.hideLoading() + wx.showToast({ title: res.message || '保存失败', icon: 'none' }) + this.setData({ saving: false }) + } + } catch (e) { + console.error('保存地址失败:', e) + wx.hideLoading() + wx.showToast({ title: '保存失败', icon: 'none' }) + this.setData({ saving: false }) + } + }, + + // 返回 + goBack() { + wx.navigateBack() + } +}) diff --git a/归档/miniprogram/pages/addresses/edit.json b/归档/miniprogram/pages/addresses/edit.json new file mode 100644 index 00000000..2e45b65e --- /dev/null +++ b/归档/miniprogram/pages/addresses/edit.json @@ -0,0 +1,5 @@ +{ + "usingComponents": {}, + "navigationStyle": "custom", + "enablePullDownRefresh": false +} diff --git a/归档/miniprogram/pages/addresses/edit.wxml b/归档/miniprogram/pages/addresses/edit.wxml new file mode 100644 index 00000000..c5429207 --- /dev/null +++ b/归档/miniprogram/pages/addresses/edit.wxml @@ -0,0 +1,101 @@ + + + + + + + + {{isEdit ? '编辑地址' : '新增地址'}} + + + + + + + + + + 👤 + 收货人 + + + + + + + + 📱 + 手机号 + + + + + + + + 📍 + 所在地区 + + + + {{province || city || district ? province + ' ' + city + ' ' + district : '请选择省市区'}} + + + + + + + + 🏠 + 详细地址 + +