feat(home): 实现游客模式与登录流程优化

- 添加游客模式,展示公共内容无需强制登录
- 实现游客模块展示打卡、计划、关于我们等功能介绍
- 添加产品介绍页面显示学习成长工具详情
- 优化登录流程,添加用户协议和隐私政策同意选项
- 更新角色标签将老师改为管理员,学员改为成员
- 添加注销账号功能替代退出登录
- 实现webview页面域名白名单安全验证
- 添加隐私政策和用户协议页面
- 优化页面跳转逻辑和错误提示信息
- 统一项目名称从明缘进阶场改为明缘云道场
This commit is contained in:
王宏豪
2026-09-03 18:57:47 +08:00
parent d7fc8c8982
commit 1dd4fa3f41
20 changed files with 588 additions and 57 deletions

View File

@@ -5,12 +5,13 @@
"pages/plan/plan", "pages/plan/plan",
"pages/plan-submit/plan-submit", "pages/plan-submit/plan-submit",
"pages/profile/profile", "pages/profile/profile",
"pages/webview/webview" "pages/webview/webview",
"pages/privacy/privacy"
], ],
"window": { "window": {
"navigationBarBackgroundColor": "#2C1810", "navigationBarBackgroundColor": "#2C1810",
"navigationBarTextStyle": "white", "navigationBarTextStyle": "white",
"navigationBarTitleText": "明缘进阶场", "navigationBarTitleText": "明缘云道场",
"backgroundColor": "#FDFBF7", "backgroundColor": "#FDFBF7",
"backgroundTextStyle": "dark" "backgroundTextStyle": "dark"
}, },
@@ -20,10 +21,30 @@
"backgroundColor": "#ffffff", "backgroundColor": "#ffffff",
"borderStyle": "black", "borderStyle": "black",
"list": [ "list": [
{ "pagePath": "pages/home/home", "text": "首页", "iconPath": "assets/tabbar/home.png", "selectedIconPath": "assets/tabbar/home-active.png" }, {
{ "pagePath": "pages/checkin/checkin", "text": "打卡", "iconPath": "assets/tabbar/checkin.png", "selectedIconPath": "assets/tabbar/checkin-active.png" }, "pagePath": "pages/home/home",
{ "pagePath": "pages/plan/plan", "text": "计划", "iconPath": "assets/tabbar/plan.png", "selectedIconPath": "assets/tabbar/plan-active.png" }, "text": "首页",
{ "pagePath": "pages/profile/profile", "text": "我的", "iconPath": "assets/tabbar/profile.png", "selectedIconPath": "assets/tabbar/profile-active.png" } "iconPath": "assets/tabbar/home.png",
"selectedIconPath": "assets/tabbar/home-active.png"
},
{
"pagePath": "pages/checkin/checkin",
"text": "打卡",
"iconPath": "assets/tabbar/checkin.png",
"selectedIconPath": "assets/tabbar/checkin-active.png"
},
{
"pagePath": "pages/plan/plan",
"text": "计划",
"iconPath": "assets/tabbar/plan.png",
"selectedIconPath": "assets/tabbar/plan-active.png"
},
{
"pagePath": "pages/profile/profile",
"text": "我的",
"iconPath": "assets/tabbar/profile.png",
"selectedIconPath": "assets/tabbar/profile-active.png"
}
] ]
}, },
"sitemapLocation": "sitemap.json" "sitemapLocation": "sitemap.json"

View File

@@ -72,7 +72,7 @@
</view> </view>
<view class="empty" wx:if="{{!groupName}}"> <view class="empty" wx:if="{{!groupName}}">
<view class="empty-icon">👥</view> <view class="empty-icon">👥</view>
<view class="empty-text">暂无小组,请联系老师分配</view> <view class="empty-text">暂无小组,请联系管理员分配</view>
</view> </view>
<view class="empty" wx:elif="{{group.length === 0}}"> <view class="empty" wx:elif="{{group.length === 0}}">
<view class="empty-icon">📭</view> <view class="empty-icon">📭</view>

View File

@@ -16,6 +16,7 @@ Page({
displayName: '', displayName: '',
encouragement: '', encouragement: '',
modules: [], modules: [],
introData: null,
loading: true, loading: true,
error: '', error: '',
skeletonItems: [0, 1, 2, 3, 4, 5, 6, 7, 8] skeletonItems: [0, 1, 2, 3, 4, 5, 6, 7, 8]
@@ -27,12 +28,32 @@ Page({
if (!silent) { if (!silent) {
this.setData({ loading: true, error: '' }); this.setData({ loading: true, error: '' });
} }
const token = auth.getToken();
if (!token) {
// 游客模式:展示公开内容,不强制登录
this.setData({
loading: false,
error: '',
greeting: '欢迎来到明缘云道场',
displayName: '游客',
encouragement: '登录后即可打卡、提交计划',
modules: this.getGuestModules(),
introData: this.getIntroData()
});
return Promise.resolve();
}
return auth.ensureLogin() return auth.ensureLogin()
.then((token) => { .then((token) => {
if (!token) { if (!token) {
// 新用户未绑定手机号,引导去「我的」完成绑定,避免无 token 请求受保护接口导致 401 this.setData({
this.setData({ loading: false, error: '请先完成微信绑定后再进入' }); loading: false,
setTimeout(() => wx.switchTab({ url: '/pages/profile/profile' }), 500); error: '',
greeting: '欢迎来到明缘云道场',
displayName: '游客',
encouragement: '登录后即可打卡、提交计划',
modules: this.getGuestModules(),
introData: this.getIntroData()
});
return; return;
} }
return api.request({ url: '/api/v1/pages/home' }); return api.request({ url: '/api/v1/pages/home' });
@@ -45,7 +66,8 @@ Page({
greeting: res.greeting || '', greeting: res.greeting || '',
displayName: res.display_name || '', displayName: res.display_name || '',
encouragement: res.encouragement || '', encouragement: res.encouragement || '',
modules: (res.modules || []).filter((m) => m.visible) modules: (res.modules || []).filter((m) => m.visible),
introData: null
}); });
}) })
.catch((err) => { .catch((err) => {
@@ -53,6 +75,42 @@ Page({
}); });
}, },
getGuestModules() {
return [
{ id: 'checkin', icon: '📋', label: '打卡介绍', desc: '每日打卡记录', visible: true, url: '', guest: true },
{ id: 'plan', icon: '📝', label: '计划介绍', desc: '周/月/年计划', visible: true, url: '', guest: true },
{ id: 'about', icon: 'ℹ️', label: '关于我们', desc: '了解明缘云道场', visible: true, url: '', guest: true }
];
},
getIntroData() {
return {
title: '产品介绍',
sections: [
{
icon: '📚',
heading: '学习成长工具',
text: '明缘云道场是一款专注于个人成长的效率工具,通过每周打卡与计划管理,帮助你养成良好的学习习惯,实现持续进步。好好学习,天天向上。'
},
{
icon: '✅',
heading: '周打卡',
text: '每周打卡,支持多种状态记录(正常、迟到、请假、早退、不参加讨论等)。查看小组打卡情况,与同学们互相监督、共同进步。'
},
{
icon: '📝',
heading: '计划管理',
text: '提供周计划、月计划、年计划三级管理,支持文字与图片上传。清晰追踪每个周期的目标完成情况,让成长有迹可循。'
},
{
icon: '👥',
heading: '小组共学',
text: '加入学习小组,查看组员的打卡与计划完成情况。在集体的氛围中保持学习动力,与志同道合的伙伴一起成长。'
}
]
};
},
onPullDownRefresh() { onPullDownRefresh() {
this.loadHome(true).then(() => { this.loadHome(true).then(() => {
wx.stopPullDownRefresh(); wx.stopPullDownRefresh();
@@ -63,12 +121,37 @@ Page({
this.loadHome(); this.loadHome();
}, },
goLogin() {
wx.switchTab({ url: '/pages/profile/profile' });
},
onModuleTap(e) { onModuleTap(e) {
const url = e.currentTarget.dataset.url; const url = e.currentTarget.dataset.url;
if (!url) return;
const token = auth.getToken(); const token = auth.getToken();
const sep = url.indexOf('?') >= 0 ? '&' : '?'; // 游客模式:引导登录
const target = url + sep + 'token=' + encodeURIComponent(token); if (!token) {
wx.navigateTo({ url: '/pages/webview/webview?url=' + encodeURIComponent(target) }); wx.showModal({
title: '登录提示',
content: '登录后使用此功能,是否前往登录?',
confirmText: '去登录',
confirmColor: '#C67B30',
success: (res) => {
if (res.confirm) {
wx.switchTab({ url: '/pages/profile/profile' });
}
} }
}); });
return;
}
if (!url) return;
// 通过 webview redirect 接口:后端验证 token 后设置 HttpOnly cookie,再 302 到目标页
// H5 页面内的同域请求会自动携带 cookie,H5 端零改动
const redirectUrl = api.BASE_URL + '/api/v1/webview/redirect'
+ '?token=' + encodeURIComponent(token)
+ '&target=' + encodeURIComponent(url);
wx.navigateTo({ url: '/pages/webview/webview?url=' + encodeURIComponent(redirectUrl) });
}
});

View File

@@ -15,6 +15,9 @@
<view class="hero-chip" wx:if="{{greeting}}">{{greeting}}</view> <view class="hero-chip" wx:if="{{greeting}}">{{greeting}}</view>
<view class="hero-name">{{displayName || '欢迎回来'}}</view> <view class="hero-name">{{displayName || '欢迎回来'}}</view>
<view class="hero-enc" wx:if="{{encouragement}}">{{encouragement}}</view> <view class="hero-enc" wx:if="{{encouragement}}">{{encouragement}}</view>
<view class="hero-login-tip" wx:if="{{!displayName || displayName === '游客'}}" hover-class="btn-hover" bindtap="goLogin">
登录账号解锁完整功能 ›
</view>
</view> </view>
<view class="error-bar" wx:if="{{error}}"> <view class="error-bar" wx:if="{{error}}">
@@ -37,6 +40,18 @@
</view> </view>
</view> </view>
<!-- 游客模式:明缘介绍 -->
<view class="intro-section" wx:if="{{introData && introData.sections}}">
<view class="intro-title">{{introData.title}}</view>
<view class="intro-card" wx:for="{{introData.sections}}" wx:key="heading">
<view class="intro-icon">{{item.icon}}</view>
<view class="intro-body">
<view class="intro-heading">{{item.heading}}</view>
<view class="intro-text">{{item.text}}</view>
</view>
</view>
</view>
<view class="empty" wx:if="{{modules.length === 0 && !error}}">暂无功能入口</view> <view class="empty" wx:if="{{modules.length === 0 && !error}}">暂无功能入口</view>
</block> </block>
</view> </view>

View File

@@ -154,3 +154,55 @@
0% { background-position: 200% 0; } 0% { background-position: 200% 0; }
100% { background-position: -200% 0; } 100% { background-position: -200% 0; }
} }
.hero-login-tip {
margin-top: 24rpx;
display: inline-block;
padding: 12rpx 28rpx;
background: rgba(198, 123, 48, 0.25);
border: 1rpx solid rgba(227, 179, 124, 0.4);
border-radius: 999rpx;
color: #E3B37C;
font-size: 24rpx;
}
/* ---- intro section ---- */
.intro-section {
margin-top: 40rpx;
}
.intro-title {
font-size: 32rpx;
font-weight: 700;
color: #2C1810;
margin-bottom: 20rpx;
padding-left: 4rpx;
}
.intro-card {
display: flex;
align-items: flex-start;
background: #fff;
border-radius: 16rpx;
padding: 28rpx 24rpx;
margin-bottom: 16rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
}
.intro-icon {
font-size: 40rpx;
margin-right: 20rpx;
flex-shrink: 0;
line-height: 1;
padding-top: 2rpx;
}
.intro-body {
flex: 1;
min-width: 0;
}
.intro-heading {
font-size: 28rpx;
font-weight: 700;
color: #C67B30;
margin-bottom: 10rpx;
}
.intro-text {
font-size: 25rpx;
color: #5A4A3A;
line-height: 1.8;
}

View File

@@ -5,7 +5,6 @@ const MAX_IMAGES = 4;
const MAX_IMAGE_SIZE = 1024 * 1024; // 超过 1MB 才压缩 const MAX_IMAGE_SIZE = 1024 * 1024; // 超过 1MB 才压缩
const COMPRESS_LEVELS = [80, 60, 45]; // 逐级降低质量,直到满足大小限制 const COMPRESS_LEVELS = [80, 60, 45]; // 逐级降低质量,直到满足大小限制
const ALLOWED_IMAGE_EXT = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']; const ALLOWED_IMAGE_EXT = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'];
const IMAGE_PLACEHOLDER_URL = 'https://picsum.photos/200/200';
const TYPE_LABELS = { weekly: '周计划', monthly: '月计划', yearly: '年计划' }; const TYPE_LABELS = { weekly: '周计划', monthly: '月计划', yearly: '年计划' };
function errDetail(err) { function errDetail(err) {
@@ -136,7 +135,7 @@ Page({
if (/^https?:\/\//i.test(path)) return path; if (/^https?:\/\//i.test(path)) return path;
const parts = String(path).replace(/^attachments\//, '').split('/'); const parts = String(path).replace(/^attachments\//, '').split('/');
if (parts.length < 4) { if (parts.length < 4) {
return IMAGE_PLACEHOLDER_URL; return '';
} }
return api.STATIC_BASE_URL + '/clock/static/plan/' + parts.slice(0, 4).map(encodeURIComponent).join('/'); return api.STATIC_BASE_URL + '/clock/static/plan/' + parts.slice(0, 4).map(encodeURIComponent).join('/');
}, },

View File

@@ -27,7 +27,8 @@ Page({
onShow() { onShow() {
auth.ensureLogin().then((token) => { auth.ensureLogin().then((token) => {
if (!token) { if (!token) {
wx.switchTab({ url: '/pages/profile/profile' }); wx.showToast({ title: '请先登录', icon: 'none' });
setTimeout(() => wx.switchTab({ url: '/pages/profile/profile' }), 500);
return; return;
} }
wx.removeStorageSync('plan_overview_dirty'); wx.removeStorageSync('plan_overview_dirty');

18
pages/privacy/privacy.js Normal file
View File

@@ -0,0 +1,18 @@
const TITLES = {
privacy: '隐私政策',
agreement: '用户协议'
};
Page({
data: {
type: 'privacy',
title: '隐私政策'
},
onLoad(options) {
const type = options.type || 'privacy';
const title = TITLES[type] || '隐私政策';
this.setData({ type, title });
wx.setNavigationBarTitle({ title });
}
});

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "隐私政策",
"usingComponents": {}
}

133
pages/privacy/privacy.wxml Normal file
View File

@@ -0,0 +1,133 @@
<view class="page">
<scroll-view class="content" scroll-y>
<!-- 隐私政策 -->
<block wx:if="{{type === 'privacy'}}">
<view class="section">
<view class="section-title">一、信息收集范围</view>
<view class="section-text">
为了向你提供打卡、计划管理等服务,我们会收集以下信息:
</view>
<view class="section-text">1. 手机号:用于账号注册与登录,验证你的身份;</view>
<view class="section-text">2. 头像与昵称:用于个人资料展示;</view>
<view class="section-text">3. 你主动提交的打卡记录、计划内容及附件图片;</view>
<view class="section-text">4. 设备信息与日志数据:用于排查故障、优化服务。</view>
</view>
<view class="section">
<view class="section-title">二、信息使用目的</view>
<view class="section-text">我们收集的信息仅用于以下目的:</view>
<view class="section-text">1. 提供打卡、计划管理、小组统计等核心功能;</view>
<view class="section-text">2. 账号安全验证与异常登录保护;</view>
<view class="section-text">3. 改进产品体验与服务质量。</view>
</view>
<view class="section">
<view class="section-title">三、信息存储与安全</view>
<view class="section-text">
你的个人信息存储于境内服务器,我们采取合理的技术措施保护你的信息安全,包括加密传输、访问控制等。
</view>
</view>
<view class="section">
<view class="section-title">四、第三方共享</view>
<view class="section-text">
我们不会向第三方出售或出租你的个人信息。仅在以下情况下可能共享:
</view>
<view class="section-text">1. 获得你的明确同意;</view>
<view class="section-text">2. 法律法规要求或司法机关依法定程序要求;</view>
<view class="section-text">3. 为完成服务所必需的合作方(如云存储服务),且签订了保密协议。</view>
</view>
<view class="section">
<view class="section-title">五、你的权利</view>
<view class="section-text">你有权访问、更正、删除你的个人信息,以及注销账号。你可以通过以下方式行使权利:</view>
<view class="section-text">1. 在「我的」页面中查看和修改个人资料;</view>
<view class="section-text">2. 在「我的」页面底部申请注销账号;</view>
<view class="section-text">3. 联系客服协助处理。</view>
</view>
<view class="section">
<view class="section-title">六、未成年人保护</view>
<view class="section-text">
若你是未满 14 周岁的未成年人,请在监护人陪同下阅读本政策,并在监护人同意的情况下使用我们的服务。
</view>
</view>
<view class="section">
<view class="section-title">七、联系方式</view>
<view class="section-text">如有任何疑问或投诉,请通过以下方式联系我们:</view>
<view class="section-text">客服邮箱:[此处填写客服邮箱]</view>
</view>
<view class="section">
<view class="section-title">八、政策更新</view>
<view class="section-text">
我们可能会适时更新本隐私政策,更新后的政策将在本页面公布。重大变更会通过小程序内通知等方式告知你。
</view>
</view>
</block>
<!-- 用户协议 -->
<block wx:if="{{type === 'agreement'}}">
<view class="section">
<view class="section-title">一、服务说明</view>
<view class="section-text">
明缘云道场小程序(以下简称"本小程序")为用户提供打卡记录、计划管理、小组统计等学习辅助服务。使用本小程序即表示你同意本协议的全部条款。
</view>
</view>
<view class="section">
<view class="section-title">二、账号注册与使用</view>
<view class="section-text">1. 你需通过手机号验证注册账号,并确保所提供信息真实准确;</view>
<view class="section-text">2. 你应妥善保管账号信息,对账号下的所有行为负责;</view>
<view class="section-text">3. 如发现账号异常,请及时联系客服处理。</view>
</view>
<view class="section">
<view class="section-title">三、用户行为规范</view>
<view class="section-text">你在使用本服务时,不得从事以下行为:</view>
<view class="section-text">1. 发布违法、违规、侵犯他人权益的内容;</view>
<view class="section-text">2. 恶意刷量、作弊、伪造打卡记录;</view>
<view class="section-text">3. 干扰或破坏本服务的正常运行;</view>
<view class="section-text">4. 未经授权访问他人账号或数据。</view>
</view>
<view class="section">
<view class="section-title">四、内容与知识产权</view>
<view class="section-text">
你上传的内容(包括文字、图片等)的知识产权归你所有。你授予本小程序为提供服务所必需的使用权限。
</view>
</view>
<view class="section">
<view class="section-title">五、服务变更与终止</view>
<view class="section-text">
我们可能根据业务需要调整或终止部分服务,并会提前通知用户。如因你违反本协议,我们有权暂停或终止对你的服务。
</view>
</view>
<view class="section">
<view class="section-title">六、免责声明</view>
<view class="section-text">
因不可抗力、系统故障、网络原因等不可归责于我方的事由导致服务中断或数据丢失的,我们不承担责任,但会尽力协助恢复。
</view>
</view>
<view class="section">
<view class="section-title">七、协议修改</view>
<view class="section-text">
我们可能根据需要修改本协议,修改后的协议将在本页面公布。继续使用服务即视为同意修改后的协议。
</view>
</view>
<view class="section">
<view class="section-title">八、联系方式</view>
<view class="section-text">如有疑问,请联系客服:</view>
<view class="section-text">客服邮箱:[此处填写客服邮箱]</view>
</view>
</block>
<view class="footer">最后更新日期:2026年9月</view>
</scroll-view>
</view>

View File

@@ -0,0 +1,40 @@
.page {
min-height: 100vh;
background: #FDFBF7;
box-sizing: border-box;
}
.content {
height: 100vh;
padding: 32rpx;
box-sizing: border-box;
}
.section {
background: #fff;
border-radius: 16rpx;
padding: 28rpx 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
}
.section-title {
font-size: 30rpx;
font-weight: 700;
color: #2C1810;
margin-bottom: 16rpx;
}
.section-text {
font-size: 26rpx;
color: #5A4A3A;
line-height: 1.8;
margin-bottom: 8rpx;
}
.footer {
text-align: center;
color: #B6A996;
font-size: 22rpx;
padding: 40rpx 0 60rpx;
}

View File

@@ -2,15 +2,15 @@ const api = require('../../utils/api');
const auth = require('../../utils/auth'); const auth = require('../../utils/auth');
const ROLE_LABELS = { const ROLE_LABELS = {
student: '学员', student: '成员',
member: '成员', member: '成员',
teacher: '老师', teacher: '管理员',
leader: '组长', leader: '组长',
'group_leader': '组长', 'group_leader': '组长',
admin: '管理员' admin: '管理员'
}; };
const LOGIN_TYPE_LABELS = { const LOGIN_TYPE_LABELS = {
wechat: '微信', wechat: '微信授权',
phone: '手机号', phone: '手机号',
admin: '管理员' admin: '管理员'
}; };
@@ -27,11 +27,15 @@ Page({
roleLabel: '', roleLabel: '',
loginTypeLabel: '', loginTypeLabel: '',
classGroups: [], classGroups: [],
avatarUploading: false avatarUploading: false,
privacyAgreed: false
}, },
onShow() { onShow() {
this.setData({ phoneError: '' }); this.setData({
phoneError: '',
privacyAgreed: !!wx.getStorageSync('privacy_agreed')
});
this.loadPage(); this.loadPage();
}, },
@@ -114,16 +118,17 @@ Page({
showPhoneLogin() { showPhoneLogin() {
this.setData({ this.setData({
view: 'phoneLogin', view: 'phoneLogin',
phoneLoading: false phoneLoading: false,
privacyAgreed: !!wx.getStorageSync('privacy_agreed')
}); });
}, },
roleLabel(role) { roleLabel(role) {
return ROLE_LABELS[role] || role || '学员'; return ROLE_LABELS[role] || role || '成员';
}, },
loginTypeLabel(type) { loginTypeLabel(type) {
return LOGIN_TYPE_LABELS[type] || type || '微信'; return LOGIN_TYPE_LABELS[type] || type || '手机号';
}, },
fullUrl(rel) { fullUrl(rel) {
@@ -138,7 +143,6 @@ Page({
onChooseAvatar(e) { onChooseAvatar(e) {
const path = e.detail && e.detail.avatarUrl; const path = e.detail && e.detail.avatarUrl;
console.log('[avatar] chooseAvatar ->', path);
if (!path) { if (!path) {
wx.showToast({ title: '未选择头像', icon: 'none' }); wx.showToast({ title: '未选择头像', icon: 'none' });
return; return;
@@ -150,7 +154,6 @@ Page({
name: 'file', name: 'file',
header: { Authorization: 'Bearer ' + auth.getToken() }, header: { Authorization: 'Bearer ' + auth.getToken() },
success: (res) => { success: (res) => {
console.log('[avatar] upload success', res.statusCode, res.data);
let data = {}; let data = {};
try { try {
data = JSON.parse(res.data); data = JSON.parse(res.data);
@@ -170,8 +173,6 @@ Page({
} }
}, },
fail: (err) => { fail: (err) => {
console.error('[avatar] upload fail', err);
console.error('[avatar] filePath', path);
wx.showToast({ title: '上传失败', icon: 'none' }); wx.showToast({ title: '上传失败', icon: 'none' });
}, },
complete: () => { complete: () => {
@@ -184,6 +185,10 @@ Page({
if (this.data.phoneLoading) { if (this.data.phoneLoading) {
return; return;
} }
if (!this.data.privacyAgreed) {
wx.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none', duration: 2000 });
return;
}
const phoneCode = e.detail && e.detail.code; const phoneCode = e.detail && e.detail.code;
if (!phoneCode) { if (!phoneCode) {
const message = '未授权手机号,请点击上方按钮重试'; const message = '未授权手机号,请点击上方按钮重试';
@@ -205,19 +210,48 @@ Page({
this.loadPage(); this.loadPage();
}, },
logout() { togglePrivacy() {
const agreed = !this.data.privacyAgreed;
this.setData({ privacyAgreed: agreed });
if (agreed) {
wx.setStorageSync('privacy_agreed', true);
} else {
wx.removeStorageSync('privacy_agreed');
}
},
openPrivacy(e) {
const type = e.currentTarget.dataset.type || 'privacy';
wx.navigateTo({ url: '/pages/privacy/privacy?type=' + type });
},
deleteAccount() {
wx.showModal({ wx.showModal({
title: '退出登录', title: '注销账号',
content: '确定要退出当前账号吗?', content: '确定要注销账号吗?注销后将清除本机登录信息,需要重新登录。',
confirmText: '退出', confirmText: '注销',
confirmColor: '#c0392b', confirmColor: '#c0392b',
success: (res) => { success: (res) => {
if (!res.confirm) { if (!res.confirm) return;
return; wx.showModal({
} title: '再次确认',
content: '注销后本机登录状态将被清除,你可以使用手机号重新登录。确定继续吗?',
confirmText: '确认注销',
confirmColor: '#c0392b',
success: (res2) => {
if (!res2.confirm) return;
// 仅清除前端登录信息
try {
api.request({ url: '/api/v1/auth/logout', method: 'POST' }).catch(() => {}); api.request({ url: '/api/v1/auth/logout', method: 'POST' }).catch(() => {});
} catch (e) {}
auth.clearToken(); auth.clearToken();
wx.removeStorageSync('privacy_agreed');
wx.showToast({ title: '已注销', icon: 'success' });
setTimeout(() => {
wx.reLaunch({ url: '/pages/home/home' }); wx.reLaunch({ url: '/pages/home/home' });
}, 1000);
}
});
} }
}); });
} }

View File

@@ -20,8 +20,8 @@
<view class="login-wrap"> <view class="login-wrap">
<view class="login-card"> <view class="login-card">
<view class="login-icon">👤</view> <view class="login-icon">👤</view>
<view class="login-title">微信手机号一键登录</view> <view class="login-title">手机号快捷登录</view>
<view class="login-desc">未找到你的账号,请使用微信手机号登录。手机号仅用于账号绑定,不会公开展示。</view> <view class="login-desc">未找到你的账号,请使用手机号登录。手机号仅用于账号绑定,不会公开展示。</view>
<button <button
class="btn-phone btn-primary" class="btn-phone btn-primary"
open-type="getPhoneNumber" open-type="getPhoneNumber"
@@ -29,7 +29,18 @@
loading="{{phoneLoading}}" loading="{{phoneLoading}}"
disabled="{{phoneLoading}}" disabled="{{phoneLoading}}"
hover-class="btn-hover" hover-class="btn-hover"
>{{phoneLoading ? '登录中...' : '微信手机号一键登录'}}</button> >{{phoneLoading ? '登录中...' : '手机号快捷登录'}}</button>
<view class="privacy-row">
<view class="checkbox {{privacyAgreed ? 'checked' : ''}}" bindtap="togglePrivacy">
<text wx:if="{{privacyAgreed}}" class="check-icon">✓</text>
</view>
<view class="privacy-text">
我已阅读并同意
<text class="link" data-type="agreement" catchtap="openPrivacy">《用户协议》</text>
和
<text class="link" data-type="privacy" catchtap="openPrivacy">《隐私政策》</text>
</view>
</view>
<view class="error" wx:if="{{phoneError}}">{{phoneError}}</view> <view class="error" wx:if="{{phoneError}}">{{phoneError}}</view>
</view> </view>
</view> </view>
@@ -91,7 +102,7 @@
</view> </view>
<view class="card classlist-card" wx:if="{{classGroups.length}}"> <view class="card classlist-card" wx:if="{{classGroups.length}}">
<view class="info-title">班级分组</view> <view class="info-title">小组分组</view>
<view class="class-row" wx:for="{{classGroups}}" wx:key="class_id"> <view class="class-row" wx:for="{{classGroups}}" wx:key="class_id">
<text class="info-label">{{item.period || item.class_code}}</text> <text class="info-label">{{item.period || item.class_code}}</text>
<text class="info-value">{{item.group}} · {{item.role}}</text> <text class="info-value">{{item.group}} · {{item.role}}</text>
@@ -99,7 +110,8 @@
</view> </view>
<view class="bottom-actions"> <view class="bottom-actions">
<button class="btn-logout" hover-class="btn-hover" bindtap="logout">退出登录</button> <button class="btn-delete" hover-class="btn-hover" bindtap="deleteAccount">注销账号</button>
</view> </view>
</view> </view>
</block> </block>

View File

@@ -186,9 +186,52 @@ button::after { border: none; }
margin-top: 48rpx; margin-top: 48rpx;
padding-top: 24rpx; padding-top: 24rpx;
} }
.btn-logout { /* ---- privacy row ---- */
.privacy-row {
display: flex;
align-items: flex-start;
margin-top: 28rpx;
text-align: left;
}
.checkbox {
width: 32rpx;
height: 32rpx;
border: 2rpx solid #D8C7B4;
border-radius: 6rpx;
margin-right: 14rpx;
margin-top: 2rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
}
.checkbox.checked {
background: #C67B30;
border-color: #C67B30;
}
.check-icon {
color: #fff;
font-size: 22rpx;
line-height: 1;
font-weight: 700;
}
.privacy-text {
flex: 1;
font-size: 24rpx;
color: #8B7A6A;
line-height: 1.6;
}
.privacy-text .link {
color: #C67B30;
}
/* ---- delete account ---- */
.btn-delete {
background: #fff; background: #fff;
color: #c0392b; color: #c0392b;
border: 1rpx solid #f0b4ae; border: 1rpx solid #f0b4ae;
border-radius: 12rpx; border-radius: 12rpx;
margin-bottom: 20rpx;
} }
/* ---- profile name edit ---- */

View File

@@ -1,8 +1,47 @@
const api = require('../../utils/api');
// 允许加载的域名白名单
const ALLOWED_HOSTS = ['m.muguaclub.cn', 'muguaclub.cn'];
function isAllowedUrl(url) {
if (!url) return false;
try {
// 简单解析 hostname
const match = url.match(/^https?:\/\/([^/:?#]+)/i);
if (!match) return false;
const host = match[1].toLowerCase();
return ALLOWED_HOSTS.some((h) => host === h || host.endsWith('.' + h));
} catch (e) {
return false;
}
}
Page({ Page({
data: { url: '' }, data: {
url: '',
invalid: false
},
onLoad(options) { onLoad(options) {
if (options.url) { if (!options.url) {
this.setData({ url: decodeURIComponent(options.url) }); this.setData({ invalid: true });
return;
}
const url = decodeURIComponent(options.url);
if (!isAllowedUrl(url)) {
console.warn('[webview] blocked url:', url);
this.setData({ invalid: true, url: '' });
return;
}
this.setData({ url, invalid: false });
},
onRetry() {
if (this.data.url) {
// 触发重新加载
this.setData({ url: '' });
setTimeout(() => {
this.setData({ url: this.data._url || '' });
}, 50);
} }
} }
}); });

View File

@@ -1 +1,8 @@
<web-view src="{{url}}" wx:if="{{url}}"></web-view> <view class="page">
<web-view src="{{url}}" wx:if="{{url && !invalid}}"></web-view>
<view class="error-page" wx:if="{{invalid}}">
<view class="error-icon">!</view>
<view class="error-title">无法加载该页面</view>
<view class="error-desc">该页面不在允许访问的域名范围内</view>
</view>
</view>

View File

@@ -0,0 +1,30 @@
.page {
min-height: 100vh;
background: #FDFBF7;
}
.error-page {
padding-top: 200rpx;
text-align: center;
}
.error-icon {
width: 120rpx;
height: 120rpx;
line-height: 120rpx;
margin: 0 auto 32rpx;
border-radius: 50%;
background: #fdecea;
color: #c0392b;
font-size: 64rpx;
font-weight: 700;
}
.error-title {
color: #2C1810;
font-size: 32rpx;
font-weight: 700;
margin-bottom: 16rpx;
}
.error-desc {
color: #8B7A6A;
font-size: 26rpx;
}

View File

@@ -1,5 +1,5 @@
{ {
"description": "明缘进阶场", "description": "明缘云道场",
"packOptions": { "packOptions": {
"ignore": [] "ignore": []
}, },

View File

@@ -1,5 +1,5 @@
{ {
"desc": "明缘进阶场小程序", "desc": "明缘云道场小程序",
"rules": [ "rules": [
{ "action": "allow", "page": "*" } { "action": "allow", "page": "*" }
] ]

View File

@@ -22,7 +22,7 @@ function wxLogin(retries) {
if (res && res.code) { if (res && res.code) {
resolve(res.code); resolve(res.code);
} else { } else {
reject(new Error((res && res.errMsg) || '微信登录失败')); reject(new Error((res && res.errMsg) || '登录失败'));
} }
}, },
fail: reject fail: reject