From d7fc8c8982592dad67067404333473b454d2a016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=AE=8F=E8=B1=AA?= <2443753184@qq.com> Date: Thu, 3 Sep 2026 16:49:59 +0800 Subject: [PATCH] init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 项目初始化 --- app.js | 11 + app.json | 30 +++ app.wxss | 16 ++ assets/tabbar/checkin-active.png | Bin 0 -> 3312 bytes assets/tabbar/checkin.png | Bin 0 -> 2938 bytes assets/tabbar/home-active.png | Bin 0 -> 1583 bytes assets/tabbar/home.png | Bin 0 -> 1456 bytes assets/tabbar/plan-active.png | Bin 0 -> 1735 bytes assets/tabbar/plan.png | Bin 0 -> 1549 bytes assets/tabbar/profile-active.png | Bin 0 -> 2455 bytes assets/tabbar/profile.png | Bin 0 -> 2148 bytes pages/checkin/checkin.js | 211 +++++++++++++++ pages/checkin/checkin.json | 1 + pages/checkin/checkin.wxml | 108 ++++++++ pages/checkin/checkin.wxss | 394 +++++++++++++++++++++++++++++ pages/home/home.js | 74 ++++++ pages/home/home.json | 5 + pages/home/home.wxml | 42 +++ pages/home/home.wxss | 156 ++++++++++++ pages/plan-submit/plan-submit.js | 351 +++++++++++++++++++++++++ pages/plan-submit/plan-submit.json | 3 + pages/plan-submit/plan-submit.wxml | 95 +++++++ pages/plan-submit/plan-submit.wxss | 56 ++++ pages/plan/plan.js | 145 +++++++++++ pages/plan/plan.json | 1 + pages/plan/plan.wxml | 116 +++++++++ pages/plan/plan.wxss | 80 ++++++ pages/profile/profile.js | 224 ++++++++++++++++ pages/profile/profile.json | 5 + pages/profile/profile.wxml | 105 ++++++++ pages/profile/profile.wxss | 194 ++++++++++++++ pages/webview/webview.js | 8 + pages/webview/webview.json | 1 + pages/webview/webview.wxml | 1 + pages/webview/webview.wxss | 0 project.config.json | 18 ++ project.private.config.json | 46 ++++ sitemap.json | 6 + utils/api.js | 76 ++++++ utils/auth.js | 109 ++++++++ 40 files changed, 2688 insertions(+) create mode 100644 app.js create mode 100644 app.json create mode 100644 app.wxss create mode 100644 assets/tabbar/checkin-active.png create mode 100644 assets/tabbar/checkin.png create mode 100644 assets/tabbar/home-active.png create mode 100644 assets/tabbar/home.png create mode 100644 assets/tabbar/plan-active.png create mode 100644 assets/tabbar/plan.png create mode 100644 assets/tabbar/profile-active.png create mode 100644 assets/tabbar/profile.png create mode 100644 pages/checkin/checkin.js create mode 100644 pages/checkin/checkin.json create mode 100644 pages/checkin/checkin.wxml create mode 100644 pages/checkin/checkin.wxss create mode 100644 pages/home/home.js create mode 100644 pages/home/home.json create mode 100644 pages/home/home.wxml create mode 100644 pages/home/home.wxss create mode 100644 pages/plan-submit/plan-submit.js create mode 100644 pages/plan-submit/plan-submit.json create mode 100644 pages/plan-submit/plan-submit.wxml create mode 100644 pages/plan-submit/plan-submit.wxss create mode 100644 pages/plan/plan.js create mode 100644 pages/plan/plan.json create mode 100644 pages/plan/plan.wxml create mode 100644 pages/plan/plan.wxss create mode 100644 pages/profile/profile.js create mode 100644 pages/profile/profile.json create mode 100644 pages/profile/profile.wxml create mode 100644 pages/profile/profile.wxss create mode 100644 pages/webview/webview.js create mode 100644 pages/webview/webview.json create mode 100644 pages/webview/webview.wxml create mode 100644 pages/webview/webview.wxss create mode 100644 project.config.json create mode 100644 project.private.config.json create mode 100644 sitemap.json create mode 100644 utils/api.js create mode 100644 utils/auth.js diff --git a/app.js b/app.js new file mode 100644 index 0000000..d89fd2e --- /dev/null +++ b/app.js @@ -0,0 +1,11 @@ +const auth = require('./utils/auth'); + +App({ + onLaunch() { + auth.ensureLogin().catch(() => {}); + }, + globalData: { + token: '', + user: null + } +}); \ No newline at end of file diff --git a/app.json b/app.json new file mode 100644 index 0000000..c95aaf4 --- /dev/null +++ b/app.json @@ -0,0 +1,30 @@ +{ + "pages": [ + "pages/home/home", + "pages/checkin/checkin", + "pages/plan/plan", + "pages/plan-submit/plan-submit", + "pages/profile/profile", + "pages/webview/webview" + ], + "window": { + "navigationBarBackgroundColor": "#2C1810", + "navigationBarTextStyle": "white", + "navigationBarTitleText": "明缘进阶场", + "backgroundColor": "#FDFBF7", + "backgroundTextStyle": "dark" + }, + "tabBar": { + "color": "#5A4A3A", + "selectedColor": "#C67B30", + "backgroundColor": "#ffffff", + "borderStyle": "black", + "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/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" +} \ No newline at end of file diff --git a/app.wxss b/app.wxss new file mode 100644 index 0000000..b401459 --- /dev/null +++ b/app.wxss @@ -0,0 +1,16 @@ +page { + background: #FDFBF7; + color: #2C1810; + font-family: -apple-system, "PingFang SC", "Noto Sans SC", sans-serif; +} +.btn-primary { + background: #C67B30; + color: #fff; + border-radius: 12rpx; +} +.card { + background: #fff; + border-radius: 16rpx; + padding: 24rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04); +} \ No newline at end of file diff --git a/assets/tabbar/checkin-active.png b/assets/tabbar/checkin-active.png new file mode 100644 index 0000000000000000000000000000000000000000..2c7bc70af21c5ab0f956115e1ead74717b664f29 GIT binary patch literal 3312 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 literal 0 HcmV?d00001 diff --git a/assets/tabbar/home-active.png b/assets/tabbar/home-active.png new file mode 100644 index 0000000000000000000000000000000000000000..972688c2c1dea5394eebb1901ba3d26cef42fe65 GIT binary patch literal 1583 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 KMNUMnLSTYaq04Ci literal 0 HcmV?d00001 diff --git a/assets/tabbar/plan-active.png b/assets/tabbar/plan-active.png new file mode 100644 index 0000000000000000000000000000000000000000..0a5173ef0262af123a70abadc44691874e186409 GIT binary patch literal 1735 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 VnJV7T@Y?_Y002ovPDHLkV1gfDqNe}= literal 0 HcmV?d00001 diff --git a/assets/tabbar/profile.png b/assets/tabbar/profile.png new file mode 100644 index 0000000000000000000000000000000000000000..bcd171b29d2758f5d62e890d39c39fbc829ea178 GIT binary patch literal 2148 zcmV-q2%GnbP)!aVO?hLTLj+vx+YM=KPg+1Y!p7Dea&vgbER~Uh`t-d<+0Hj_9WoFVFr-{#^rrF#y_% zactSzQmfUJ5P1BN!4YkPS#8uA2H2G(&c{jOT;e=4Gsy!pJ5MCn<2doe9B8c`v|3*} z@yNkn0ssktvMhXBED^&nRN}%XA3pdLDtuBdI`17K>J9dZftdgi18bD?^yeswAD?_> zen}>0Y;26Wn#5{j4Lg2#@N93nG`g}Hc?O{aGg%QcB;~-^*!d-|+~CC2+;U@F)RiQL zjZI`^=)Tc%DL5M^PS!TtbU&EmQppBU?9L^9UmRIpUdDR8?(>Q(^CVG;b(}bQ;LG6g zZ@{d0?OIFvAQstEwstj&o_z78zdW7iu8nzG;lRKE|6~4-mjY`ZNSt#3C_#0rz$h+^ zkf727FHFt6mzP_sJl*)8^M4q$Mn51saEl2PAOlcfZP`0IBcxWV)m+AxOC!t6%c;dP z;5XA)58Jj@X(#goeB{i~P^qylb_IzjgE>81=>>qJ&WU$r+le;-<-PaDsN4UmuHGwb z=5aL4D>&t(r>74QcFRemt!ps?Yo!z+?_2K$9Zh1jT1_osb;TdF#`H3i?3nEz80kap zZsX&&@@j~QgRB8{ruKkKd9_vKA-DDbqpl&b-Uu_wJ^GErx$B6^##VH znSv`|ntr`d!kJO@=uxmyBJFl5H!EOp7~R4R7=_27xsIC(}XRC+D$$zqXgjWN#q*9qL$g+M8Sdj^9o131Vj&3c_LMM{Q0Ql{712}g0 z{{4Z4VP-xG1cS!tK&}%pc=A$c$^@;{$I1v#B-x_BBf!gm`1|KCT&~M?De5nVH&5iY zCox>7uQ_)3?)_GmzsEq21zPucVky+ij_M`)W=2^Nn^~J1h5Yorw9yKMR3ZXmdKFpy z{P~NQUzHcG+qHQ+lZZFm=h#Dc?JWg8KlDtGS)=&e_|iJvPP)X5jkRv`Y1u$(>pFI7_~4_6_%p5b-Y8B(K`D=JR3g$kFsJGYAybwO zVt)E6uKwi0g@xSWKcB#C4!>iN~QNRyGzdn){KhFdQY3t*R&7m(t%jAMkUTK zvQNVC>G{Q05!rN{SQ7z}oJJ1avv21Y%5PeuN253)@0k$O+VYlbAYpAHRg@$S6%Hs9 z{B>mLz)2ynp~`LU?VG$`$vlr7xwl`FnKfGNi=A`W`oetc!0b^eB^yu_yKrK9_FS`) zlV(UnNj3HN_p`nBuZdD%zO@>;1ThsTZErAprM2wm{~^EwFJ8R7)XY&avpWlcGJE#g z``<5@Z08UtK-5H@jM2M+{Mpl|D<%xX%m#(cxbev&U%idVF9L&YrNS#Oq}s-kwQ(GO zcl@Q<=d!wWCQEDxg;<0`fwkqe<5y=9C=eOAb6(=0A2g%g$wZU%N0RonCBe7nurvWJ{cq?%(Y9tmq9)vKP zrzNK0jO*F+E)l%1mCCg!)GpZxBVW!cJINX+O7c)QBRtiN>?G^PLek(1Q!`7<^y5;=YG&BdkIQZz z>`H;LQ54Vg?TLRaL3Y+mPLaid=Ce3OK5@GFd}6c5HLBHWI59Q%VH_t9dZNX^7_;^< z! z;X+rb!ZxZ;48wt-H~xHYlEileM(<9XHv}+@i1y|niCJqc$%0ux^+75TQRa2!R^svL z`H#9mFKPao(5TiC9U5ej`BXoZ-)vU%7eJwX6qa4s&gHe{3uYMK!E}U3KW3t ajsF2Euou~Pai*;R0000 ({ + label, + color: STATUS_META[label] || '#C67B30' +})); + +function errMessage(err) { + if (err && typeof err === 'object') { + if (err.detail) return String(err.detail); + if (err.error) return String(err.error); + if (err.message) return err.message; + } + return ''; +} + +function pad2(n) { + return n < 10 ? '0' + n : String(n); +} + +function fmtTime(ts) { + if (!ts) return ''; + const d = new Date(String(ts).replace(' ', 'T')); + if (isNaN(d.getTime())) return String(ts); + const now = new Date(); + const startOfDay = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime(); + const diff = Math.round((startOfDay(now) - startOfDay(d)) / 86400000); + const hm = pad2(d.getHours()) + ':' + pad2(d.getMinutes()); + if (diff === 0) return '今天 ' + hm; + if (diff === 1) return '昨天 ' + hm; + return pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()) + ' ' + hm; +} + +Page({ + data: { + statuses: STATUSES, + statusItems: STATUS_ITEMS, + statusIndex: -1, + loading: true, + classDate: '', + weekday: '', + dayNum: '', + checked: false, + record: null, + statusColor: '', + statusBg: '', + history: [], + group: [], + groupName: '', + checkedCount: 0, + total: 0, + tab: 'group', + submitting: false, + refreshing: false, + loadError: '', + }, + + onShow() { + this.loadPage(); + }, + + onPullDownRefresh() { + this.loadPage().then(() => wx.stopPullDownRefresh()); + }, + + loadPage() { + this.setData({ loadError: '' }); + return auth.ensureLogin() + .then((token) => { + if (!token) { + wx.switchTab({ url: '/pages/profile/profile' }); + return; + } + return this.loadWeek(); + }) + .catch((err) => { + const msg = (err && (err.error || err.detail || err.message)) || '加载失败,请重试'; + this.setData({ loading: false, loadError: msg }); + wx.showToast({ title: msg, icon: 'none' }); + }); + }, + + onRetry() { + this.loadPage(); + }, + + loadWeek() { + this.setData({ loading: true }); + return api.request({ url: '/api/v1/checkin/week' }).then((res) => { + const classDate = res.class_date || ''; + const weekday = WEEKDAYS[new Date(classDate + 'T00:00:00').getDay()] || ''; + const dayNum = classDate.split('-')[2] || ''; + const statusIndex = res.record && res.record.status + ? Math.max(0, STATUSES.indexOf(res.record.status)) + : -1; + const statusColor = res.record ? (STATUS_META[res.record.status] || '#C67B30') : ''; + this.setData({ + classDate, + weekday, + dayNum, + checked: !!res.checked, + record: res.record || null, + statusIndex, + statusColor, + statusBg: statusColor ? statusColor + '1A' : '' + }); + return this.loadGroup(); + }).then(() => { + this.setData({ loading: false }); + }).catch(() => { + this.setData({ loading: false }); + }); + }, + + loadGroup() { + const data = this.data.classDate ? { class_date: this.data.classDate } : {}; + return api.request({ url: '/api/v1/checkin/group', data }).then((res) => { + const members = (res.members || []).map((m) => ({ + ...m, + shortName: (m.name || '?').charAt(0), + timeText: m.checked && m.record ? fmtTime(m.record.checked_at) : '' + })); + const checkedCount = members.filter((m) => m.checked).length; + this.setData({ + group: members, + groupName: res.group_name || '', + checkedCount, + total: members.length + }); + }).catch(() => {}); + }, + + loadHistory() { + return api.request({ url: '/api/v1/checkin/list', data: { page: 1, size: 20 } }).then((res) => { + const history = (res.list || []).map((item) => { + const color = STATUS_META[item.status] || '#C67B30'; + return { + ...item, + timeText: fmtTime(item.checked_at), + color, + bg: color + '1A' + }; + }); + this.setData({ history }); + }).catch(() => {}); + }, + + onStatusTap(e) { + if (this.data.submitting) { + return; + } + const statusIndex = Number(e.currentTarget.dataset.index); + const status = STATUSES[statusIndex]; + if (!status) { + return; + } + const prevIndex = this.data.statusIndex; + this.setData({ statusIndex, submitting: true }); + api.request({ + url: '/api/v1/checkin', + method: 'POST', + data: { + status, + class_date: this.data.classDate, + course_type: '会员课程' + } + }).then((res) => { + const record = res.record || null; + const statusColor = record ? (STATUS_META[record.status] || '#C67B30') : ''; + this.setData({ submitting: false, checked: true, record, statusColor, statusBg: statusColor ? statusColor + '1A' : '' }); + wx.showToast({ title: '打卡成功', icon: 'success' }); + this.loadGroup(); + if (this.data.tab === 'personal') { + this.loadHistory(); + } + }).catch((err) => { + this.setData({ submitting: false, statusIndex: prevIndex }); + wx.showToast({ title: errMessage(err) || '打卡失败', icon: 'none' }); + }); + }, + + switchTab(e) { + const tab = e.currentTarget.dataset.tab; + if (tab !== 'group' && tab !== 'personal') { + return; + } + this.setData({ tab }); + if (tab === 'personal') { + this.loadHistory(); + } + }, + + + onRefresh() { + this.setData({ refreshing: true }); + const task = this.data.tab === 'group' ? this.loadWeek() : this.loadHistory(); + Promise.resolve(task).then(() => { + this.setData({ refreshing: false }); + }); + } +}); diff --git a/pages/checkin/checkin.json b/pages/checkin/checkin.json new file mode 100644 index 0000000..0f41438 --- /dev/null +++ b/pages/checkin/checkin.json @@ -0,0 +1 @@ +{ "usingComponents": {} } \ No newline at end of file diff --git a/pages/checkin/checkin.wxml b/pages/checkin/checkin.wxml new file mode 100644 index 0000000..a40fa6d --- /dev/null +++ b/pages/checkin/checkin.wxml @@ -0,0 +1,108 @@ + + + + + ! + {{loadError}} + + + + + + + + + + + + + + + + + + + + + + + + + {{weekday}} + {{dayNum}} + + + {{checked ? '已打卡' : '未打卡'}} + {{checked ? '已完成今日打卡' : '请选择状态完成打卡'}} + + + {{record.status}} + + + + + + + + {{item.label}} + + 提交中… + + + + + + + 小组 + 个人 + + + + + {{groupName}} · {{classDate}} + {{checkedCount}}/{{total}} 已打卡 + + + 👥 + 暂无小组,请联系老师分配 + + + 📭 + 本周暂无打卡记录 + + + {{item.shortName}} + + {{item.name}} + {{item.checked ? ('打卡 ' + item.timeText) : ''}} + + {{item.checked ? item.record.status : '未打卡'}} + + + + + 📒 + 暂无个人打卡记录 + + + + {{item.class_date}} + {{item.status}} + + {{item.timeText}} + + + + + + + + diff --git a/pages/checkin/checkin.wxss b/pages/checkin/checkin.wxss new file mode 100644 index 0000000..649a943 --- /dev/null +++ b/pages/checkin/checkin.wxss @@ -0,0 +1,394 @@ +/* ---- error state ---- */ +.error-card { + margin-top: 160rpx; + text-align: center; + padding: 0 40rpx; +} +.error-icon { + width: 96rpx; + height: 96rpx; + line-height: 96rpx; + margin: 0 auto 24rpx; + border-radius: 50%; + background: #fdecea; + color: #c0392b; + font-size: 48rpx; + font-weight: 700; + text-align: center; +} +.error-title { + color: #5A4A3A; + font-size: 28rpx; + margin-bottom: 48rpx; + line-height: 1.6; +} +.btn-retry { + display: inline-block; + min-width: 240rpx; + background: #C67B30; + color: #fff; + border-radius: 12rpx; +} +.btn-hover { opacity: .85; transform: scale(.98); } + +.page { + padding: 32rpx; + padding-bottom: calc(32rpx + env(safe-area-inset-bottom)); + height: 100vh; + display: flex; + flex-direction: column; + box-sizing: border-box; + overflow: hidden; + background: #FDFBF7; +} + +.fade-in { + animation: fadeIn .3s ease; +} +@keyframes fadeIn { + from { opacity: 0; transform: translateY(8rpx); } + to { opacity: 1; transform: none; } +} + +.card { + background: #fff; + border-radius: 16rpx; + padding: 24rpx; + box-shadow: 0 4rpx 16rpx rgba(44, 24, 16, 0.05); +} +.top-card { + flex-shrink: 0; + border: 1rpx solid transparent; + box-shadow: 0 8rpx 24rpx rgba(44, 24, 16, 0.08); +} +.top-card.done-card { + border-color: rgba(46, 125, 50, 0.25); +} + +/* 打卡状态卡 */ +.status-row { + display: flex; + align-items: center; + gap: 20rpx; +} +.date-chip { + width: 124rpx; + flex-shrink: 0; + border-radius: 14rpx; + padding: 16rpx 8rpx; + text-align: center; + background: #2C1810; + color: #FDFBF7; +} +.date-week { + font-size: 22rpx; + opacity: 0.75; +} +.date-num { + font-size: 40rpx; + font-weight: 800; + margin-top: 6rpx; + line-height: 1; +} +.status-main { + flex: 1; + min-width: 0; +} +.status-title { + font-size: 40rpx; + font-weight: 800; +} +.status-title.done { + color: #2E7D32; +} +.status-title.undone { + color: #C67B30; +} +.status-sub { + margin-top: 10rpx; + font-size: 24rpx; + color: #7A6A5C; +} + +/* 状态按钮:两行布局 */ +.pills-grid { + position: relative; + display: flex; + flex-wrap: wrap; + gap: 16rpx; + margin-top: 28rpx; + padding-top: 24rpx; + border-top: 1rpx solid #F2EDE7; +} +.pills-grid.disabled { + opacity: 0.7; +} +.pill { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 1 1 30%; + min-width: 180rpx; + box-sizing: border-box; + margin: 0; + padding: 18rpx 20rpx; + border-radius: 999rpx; + border: 1rpx solid #D8C7B4; + background: #FFF; + color: #5A4A3A; + font-size: 26rpx; + line-height: 1; +} +.pill.active { + background: #C67B30; + border-color: #C67B30; + color: #fff; + box-shadow: 0 6rpx 14rpx rgba(198, 123, 48, 0.22); +} +.pill-hover { + opacity: 0.85; + transform: scale(0.97); +} +.dot { + width: 14rpx; + height: 14rpx; + border-radius: 50%; + margin-right: 12rpx; + flex-shrink: 0; +} +.submitting { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + background: rgba(253, 251, 247, 0.75); + display: flex; + align-items: center; + justify-content: center; + color: #C67B30; + font-weight: 700; + border-radius: 12rpx; + z-index: 2; +} + +/* 记录区 */ +.record-panel { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + margin-top: 24rpx; +} +.record-scroll { + flex: 1; + min-height: 0; +} +.record-inner { + padding: 4rpx 4rpx 24rpx; +} +.tabs { + display: flex; + border-bottom: 1rpx solid #eee; + margin-bottom: 8rpx; +} +.tab { + flex: 1; + text-align: center; + padding: 22rpx 0; + color: #7A6A5C; + transition: color .15s ease; +} +.tab.active { + color: #C67B30; + font-weight: 700; + border-bottom: 4rpx solid #C67B30; + transform: translateY(-2rpx); +} +.tab-hover { + background: #f6efe6; +} + +/* 小组头部 + 进度 */ +.group-head { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12rpx 8rpx; +} +.group-title { + color: #C67B30; + font-weight: 700; + font-size: 26rpx; +} +.group-progress { + padding: 6rpx 14rpx; + border-radius: 999rpx; + background: #f6efe6; + color: #C67B30; + font-size: 22rpx; +} + +/* 成员列表 */ +.member { + display: flex; + align-items: center; + padding: 18rpx 8rpx; + border-bottom: 1rpx solid #f2ede7; +} +.member-avatar { + width: 60rpx; + height: 60rpx; + border-radius: 50%; + background: #f6efe6; + color: #C67B30; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} +.member-avatar.ok { + background: #e8f4eb; + color: #2E7D32; +} +.member-info { + flex: 1; + min-width: 0; + margin-left: 16rpx; +} +.member-name { + font-size: 28rpx; + color: #2C1810; +} +.member.ok .member-name { + color: #2E7D32; +} +.member-meta { + margin-top: 6rpx; + font-size: 22rpx; + color: #8B7A6A; +} +.member-badge { + flex-shrink: 0; + margin-left: 12rpx; + padding: 8rpx 16rpx; + border-radius: 999rpx; + font-size: 22rpx; +} +.member-badge.ok { + background: #e8f4eb; + color: #2E7D32; +} +.member-badge.idle { + background: #f2ede7; + color: #8B7A6A; +} + +/* 历史 */ +.history-item { + background: #fff; + border-radius: 12rpx; + padding: 20rpx; + margin-bottom: 16rpx; + box-shadow: 0 4rpx 16rpx rgba(44, 24, 16, 0.04); +} +.history-head { + display: flex; + align-items: center; + justify-content: space-between; +} +.history-date { + font-size: 28rpx; + font-weight: 700; + color: #2C1810; +} +.history-badge { + padding: 6rpx 14rpx; + border-radius: 999rpx; + font-size: 22rpx; +} +.history-time { + margin-top: 8rpx; + font-size: 22rpx; + color: #999; +} + +/* 空态 */ +.empty { + padding: 64rpx 24rpx; + text-align: center; + color: #B6A996; + font-size: 26rpx; +} +.empty-icon { + width: 96rpx; + height: 96rpx; + line-height: 96rpx; + margin: 0 auto 20rpx; + border-radius: 50%; + background: #f6efe6; + font-size: 48rpx; + text-align: center; +} +.empty-text { + color: #B6A996; +} + +/* 骨架屏 */ +.skeleton-card { + background: #fff; + border-radius: 16rpx; + padding: 28rpx 24rpx; + box-shadow: 0 4rpx 16rpx rgba(44, 24, 16, 0.04); +} +.sk { + background: #F0EBE3; + border-radius: 12rpx; +} +.sk-title { + width: 60%; + height: 40rpx; + margin-bottom: 24rpx; +} +.sk-line { + height: 24rpx; + margin-top: 16rpx; +} +.w60 { + width: 60%; +} +.w90 { + width: 90%; +} +.skeleton-list { + margin-top: 24rpx; +} +.sk-row { + height: 96rpx; + margin-bottom: 16rpx; +} + +/* 顶部当前状态徽标 */ +.hero-status { + display: inline-flex; + align-items: center; + margin-top: 14rpx; + padding: 8rpx 16rpx; + border-radius: 999rpx; + background: #F6F0E7; + font-size: 24rpx; +} +.hero-status-dot { + width: 12rpx; + height: 12rpx; + border-radius: 50%; + margin-right: 10rpx; + flex-shrink: 0; +} +.hero-status-label { + font-weight: 700; +} + + + diff --git a/pages/home/home.js b/pages/home/home.js new file mode 100644 index 0000000..7dc3dc0 --- /dev/null +++ b/pages/home/home.js @@ -0,0 +1,74 @@ +const api = require('../../utils/api'); +const auth = require('../../utils/auth'); + +function errMessage(err) { + if (err && typeof err === 'object') { + if (err.detail) return String(err.detail); + if (err.error) return String(err.error); + if (err.message) return String(err.message); + } + return '加载失败,请稍后再试'; +} + +Page({ + data: { + greeting: '', + displayName: '', + encouragement: '', + modules: [], + loading: true, + error: '', + skeletonItems: [0, 1, 2, 3, 4, 5, 6, 7, 8] + }, + + onShow() { this.loadHome(); }, + + loadHome(silent) { + if (!silent) { + this.setData({ loading: true, error: '' }); + } + return auth.ensureLogin() + .then((token) => { + if (!token) { + // 新用户未绑定手机号,引导去「我的」完成绑定,避免无 token 请求受保护接口导致 401 + this.setData({ loading: false, error: '请先完成微信绑定后再进入' }); + setTimeout(() => wx.switchTab({ url: '/pages/profile/profile' }), 500); + return; + } + return api.request({ url: '/api/v1/pages/home' }); + }) + .then((res) => { + if (!res) return; + this.setData({ + loading: false, + error: '', + greeting: res.greeting || '', + displayName: res.display_name || '', + encouragement: res.encouragement || '', + modules: (res.modules || []).filter((m) => m.visible) + }); + }) + .catch((err) => { + this.setData({ loading: false, error: errMessage(err) }); + }); + }, + + onPullDownRefresh() { + this.loadHome(true).then(() => { + wx.stopPullDownRefresh(); + }); + }, + + onRetry() { + this.loadHome(); + }, + + onModuleTap(e) { + const url = e.currentTarget.dataset.url; + if (!url) return; + const token = auth.getToken(); + const sep = url.indexOf('?') >= 0 ? '&' : '?'; + const target = url + sep + 'token=' + encodeURIComponent(token); + wx.navigateTo({ url: '/pages/webview/webview?url=' + encodeURIComponent(target) }); + } +}); diff --git a/pages/home/home.json b/pages/home/home.json new file mode 100644 index 0000000..09e623a --- /dev/null +++ b/pages/home/home.json @@ -0,0 +1,5 @@ +{ + "usingComponents": {}, + "enablePullDownRefresh": true, + "backgroundTextStyle": "dark" +} \ No newline at end of file diff --git a/pages/home/home.wxml b/pages/home/home.wxml new file mode 100644 index 0000000..15b9a71 --- /dev/null +++ b/pages/home/home.wxml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + {{greeting}} + {{displayName || '欢迎回来'}} + {{encouragement}} + + + + {{error}} + 重试 + + + + + {{item.icon}} + {{item.label}} + {{item.desc}} + + + + 暂无功能入口 + + \ No newline at end of file diff --git a/pages/home/home.wxss b/pages/home/home.wxss new file mode 100644 index 0000000..df47d66 --- /dev/null +++ b/pages/home/home.wxss @@ -0,0 +1,156 @@ +.page { + padding: 32rpx; + min-height: 100vh; + box-sizing: border-box; + background: #FDFBF7; +} + +/* ---- Hero 品牌头图 ---- */ +.hero { + position: relative; + overflow: hidden; + padding: 48rpx 40rpx 56rpx; + border-radius: 24rpx; + background: linear-gradient(135deg, #2C1810 0%, #4A2B1D 55%, #6B3F24 100%); + box-shadow: 0 12rpx 32rpx rgba(44, 24, 16, 0.18); + color: #FDFBF7; +} +.hero-chip { + display: inline-block; + padding: 10rpx 22rpx; + border-radius: 999rpx; + background: rgba(198, 123, 48, 0.18); + color: #E3B37C; + font-size: 22rpx; + margin-bottom: 22rpx; +} +.hero-name { + font-size: 48rpx; + font-weight: 700; + color: #FFFFFF; + letter-spacing: 2rpx; +} +.hero-enc { + margin-top: 14rpx; + font-size: 26rpx; + color: #E8DCCB; + line-height: 1.6; +} +.hero-skeleton { + min-height: 240rpx; + background: linear-gradient(90deg, #EFE6D9 25%, #FAF4EC 50%, #EFE6D9 75%); + background-size: 200% 100%; + animation: shimmer 1.4s ease-in-out infinite; +} + +/* ---- 模块网格 ---- */ +.grid { + display: flex; + flex-wrap: wrap; + gap: 20rpx; + margin-top: 32rpx; +} +.module { + width: 31%; + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + background: #FFF; + border-radius: 22rpx; + padding: 30rpx 12rpx 26rpx; + box-shadow: 0 6rpx 24rpx rgba(44, 24, 16, 0.06); + transition: transform 0.18s ease, opacity 0.18s ease; +} +.module-hover { + transform: scale(1.03); + opacity: 0.9; +} +.module-icon { + width: 84rpx; + height: 84rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background: #F6EFE6; + font-size: 40rpx; + flex-shrink: 0; +} +.module-label { + margin-top: 16rpx; + max-width: 100%; + font-size: 28rpx; + font-weight: 700; + color: #2C1810; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.module-desc { + margin-top: 8rpx; + max-width: 100%; + font-size: 22rpx; + color: #8B7A6A; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ---- 骨架屏 ---- */ +.module-skeleton { + box-shadow: none; +} +.sk { + background: #EDE3D6; + border-radius: 10rpx; +} +.sk-circle { + width: 84rpx; + height: 84rpx; + border-radius: 50%; + margin-bottom: 16rpx; +} +.sk-line { + height: 24rpx; + margin-top: 12rpx; +} +.w-70 { width: 70%; } +.w-40 { width: 40%; } + +/* ---- 错误 / 空状态 ---- */ +.error-bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20rpx; + margin-top: 32rpx; + padding: 22rpx 24rpx; + border-radius: 14rpx; + background: #FDEAEA; + color: #C0392B; + font-size: 24rpx; +} +.error-text { + flex: 1; + min-width: 0; +} +.error-retry { + flex-shrink: 0; + color: #C67B30; + font-weight: 700; + padding: 4rpx 0 4rpx 16rpx; +} +.link-hover { opacity: 0.7; } +.empty { + padding: 80rpx 0; + text-align: center; + color: #B6A996; + font-size: 26rpx; +} + +@keyframes shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} \ No newline at end of file diff --git a/pages/plan-submit/plan-submit.js b/pages/plan-submit/plan-submit.js new file mode 100644 index 0000000..7880aa8 --- /dev/null +++ b/pages/plan-submit/plan-submit.js @@ -0,0 +1,351 @@ +const api = require('../../utils/api'); +const auth = require('../../utils/auth'); + +const MAX_IMAGES = 4; +const MAX_IMAGE_SIZE = 1024 * 1024; // 超过 1MB 才压缩 +const COMPRESS_LEVELS = [80, 60, 45]; // 逐级降低质量,直到满足大小限制 +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: '年计划' }; + +function errDetail(err) { + if (err && err.detail) return String(err.detail); + if (err && err.error) return String(err.error); + if (err && err.message) return String(err.message); + return ''; +} + +function getFileSize(filePath) { + return new Promise((resolve) => { + wx.getFileInfo({ + filePath, + success: (res) => resolve(Number(res.size) || 0), + fail: () => resolve(0) + }); + }); +} + +function hasImageExt(path) { + if (!path) return false; + const ext = (String(path).split('.').pop() || '').toLowerCase(); + return !!ext && ALLOWED_IMAGE_EXT.indexOf(ext) !== -1; +} + +Page({ + data: { + planType: 'weekly', + typeLabel: '周计划', + periodKey: '', + groupName: '', + loading: true, + loadError: '', + canSubmit: true, + message: '', + content: '', + checked: false, + record: null, + attachments: [], + attachmentThumbs: [], + uploading: false, + submitting: false, + history: [] + }, + + onLoad(options) { + const planType = options.plan_type || 'weekly'; + this.setData({ + planType, + typeLabel: TYPE_LABELS[planType] || '计划', + periodKey: options.period_key ? decodeURIComponent(options.period_key) : '', + groupName: options.group_name ? decodeURIComponent(options.group_name) : '' + }); + auth.ensureLogin().then((token) => { + if (!token) { + this.setData({ loading: false, loadError: '请先在「我的」页面绑定手机号' }); + setTimeout(() => wx.switchTab({ url: '/pages/profile/profile' }), 600); + return; + } + this.loadStatus(); + }).catch(() => { + this.setData({ loading: false, loadError: '登录失败,请重试' }); + }); + }, + + onRetry() { + this.setData({ loading: true, loadError: '' }); + auth.ensureLogin().then((token) => { + if (!token) { + this.setData({ loading: false, loadError: '请先在「我的」页面绑定手机号' }); + setTimeout(() => wx.switchTab({ url: '/pages/profile/profile' }), 600); + return; + } + this.loadStatus(); + }).catch(() => { + this.setData({ loading: false, loadError: '登录失败,请重试' }); + }); + }, + + loadStatus() { + api.request({ + url: '/api/v1/plan/status', + data: { type: this.data.planType, period_key: this.data.periodKey } + }).then((res) => { + this.applyRecord(res); + if (this.data.planType !== 'weekly') { + this.loadHistory(); + } + }).catch((err) => { + this.setData({ loading: false, loadError: errDetail(err) || '加载计划失败' }); + }); + }, + + applyRecord(res) { + const record = res.record; + const attachments = record ? (record.attachments || []) : []; + this.setData({ + loading: false, + loadError: '', + periodKey: res.period_key || this.data.periodKey, + canSubmit: res.can_submit !== false, + message: res.message || '', + checked: !!res.checked, + record, + content: record ? (record.content || '') : '', + attachments, + attachmentThumbs: attachments.map((p) => this.planFileUrl(p)) + }); + }, + + loadHistory() { + api.request({ url: '/api/v1/plan/history', data: { type: this.data.planType, page: 1, size: 20 } }) + .then((res) => { + const history = (res.list || []).map((item) => ({ + ...item, + thumbSrcs: (item.attachments || []).map((p) => this.planFileUrl(p)) + })); + this.setData({ history }); + }).catch(() => {}); + }, + + onContentInput(e) { + this.setData({ content: e.detail.value }); + }, + + planFileUrl(path) { + if (!path) return ''; + if (/^https?:\/\//i.test(path)) return path; + const parts = String(path).replace(/^attachments\//, '').split('/'); + if (parts.length < 4) { + return IMAGE_PLACEHOLDER_URL; + } + return api.STATIC_BASE_URL + '/clock/static/plan/' + parts.slice(0, 4).map(encodeURIComponent).join('/'); + }, + + chooseImages() { + const remain = MAX_IMAGES - this.data.attachments.length; + if (remain <= 0) { + wx.showToast({ title: '最多上传4张图片', icon: 'none' }); + return; + } + if (!this.data.canSubmit || !this.data.periodKey) { + wx.showToast({ title: '当前不可上传', icon: 'none' }); + return; + } + wx.chooseMedia({ + count: remain, + mediaType: ['image'], + sourceType: ['album', 'camera'], + success: (res) => { + const paths = (res.tempFiles || []).map((f) => f.tempFilePath); + this.uploadImages(paths); + } + }); + }, + + uploadImages(paths) { + if (this.data.uploading || !paths.length) { + return; + } + const planType = this.data.planType; + const periodKey = this.data.periodKey; + let attachments = this.data.attachments.slice(); + this.setData({ uploading: true }); + const run = (index) => { + if (index >= paths.length) { + this.setData({ + uploading: false, + attachmentThumbs: attachments.map((p) => this.planFileUrl(p)) + }); + return; + } + const rawPath = paths[index]; + const doUpload = (src) => { + wx.uploadFile({ + url: api.BASE_URL + '/api/v1/plan/upload', + filePath: src, + name: 'files', + formData: { plan_type: planType, period_key: periodKey }, + header: { Authorization: 'Bearer ' + auth.getToken() }, + success: (res) => { + let data = {}; + try { + data = JSON.parse(res.data); + } catch (e) { + data = {}; + } + if (data.success && Array.isArray(data.attachments)) { + attachments = attachments.concat(data.attachments).slice(0, MAX_IMAGES); + this.setData({ + attachments, + attachmentThumbs: attachments.map((p) => this.planFileUrl(p)) + }); + } else { + wx.showToast({ title: data.error || '上传失败', icon: 'none' }); + } + run(index + 1); + }, + fail: () => { + wx.showToast({ title: '上传失败', icon: 'none' }); + run(index + 1); + } + }); + }; + // 按大小限制压缩:小图直接上传,大图逐级降质量直到 ≤ MAX_IMAGE_SIZE + const tryCompress = (src, level) => { + getFileSize(src).then((size) => { + if (!size || size <= MAX_IMAGE_SIZE || level >= COMPRESS_LEVELS.length) { + doUpload(src); + return; + } + wx.compressImage({ + src, + quality: COMPRESS_LEVELS[level], + success: (res) => { + const compressed = res.tempFilePath || src; + if (!hasImageExt(compressed)) { + // 压缩产物扩展名丢失时回退当前图,避免后端按扩展名校验失败 + doUpload(src); + return; + } + tryCompress(compressed, level + 1); + }, + fail: () => doUpload(src) + }); + }); + }; + tryCompress(rawPath, 0); + }; + run(0); + }, + + removeImage(e) { + if (this.data.uploading) { + return; + } + const index = Number(e.currentTarget.dataset.index); + const path = this.data.attachments[index]; + if (!path) { + return; + } + const parts = path.replace(/^attachments\//, '').split('/'); + const dropLocal = () => { + const attachments = this.data.attachments.slice(); + attachments.splice(index, 1); + this.setData({ + attachments, + attachmentThumbs: attachments.map((p) => this.planFileUrl(p)) + }); + }; + if (parts.length < 4) { + dropLocal(); + return; + } + const sid = parts[0]; + const planType = parts[1]; + const period = parts[2]; + const filename = parts[3]; + api.request({ + url: `/api/v1/plan/file/${sid}/${planType}/${period}/${encodeURIComponent(filename)}`, + method: 'DELETE' + }).then(dropLocal).catch(() => { + wx.showToast({ title: '删除失败', icon: 'none' }); + }); + }, + + previewImage(e) { + const index = Number(e.currentTarget.dataset.index); + const urls = this.data.attachmentThumbs; + const src = urls[index]; + if (!src) { + return; + } + wx.previewImage({ current: src, urls }); + }, + + previewHistoryImage(e) { + const historyId = e.currentTarget.dataset.id; + const index = Number(e.currentTarget.dataset.index); + const item = this.data.history.find((h) => String(h.id) === String(historyId)); + if (!item) { + return; + } + const urls = item.thumbSrcs || []; + const src = urls[index]; + if (!src) { + return; + } + wx.previewImage({ current: src, urls }); + }, + + submit() { + if (this.data.submitting) { + return; + } + if (!this.data.canSubmit) { + wx.showToast({ title: this.data.message || '当前不可提交', icon: 'none' }); + return; + } + const hasText = !!(this.data.content || '').trim(); + const hasImages = !!this.data.attachments.length; + if (!hasText && !hasImages) { + wx.showToast({ title: '请填写内容或上传图片', icon: 'none' }); + return; + } + const doPost = () => { + this.setData({ submitting: true }); + api.request({ + url: '/api/v1/plan/checkin', + method: 'POST', + data: { + plan_type: this.data.planType, + period_key: this.data.periodKey, + status: '已完成', + content: this.data.content, + attachments: this.data.attachments + } + }).then(() => { + this.setData({ submitting: false }); + wx.showToast({ title: '已保存', icon: 'success' }); + wx.setStorageSync('plan_overview_dirty', true); + setTimeout(() => wx.navigateBack(), 600); + }).catch((err) => { + this.setData({ submitting: false }); + const message = errDetail(err) || '保存失败'; + wx.showToast({ title: message, icon: 'none' }); + }); + }; + if (this.data.checked || this.data.record) { + wx.showModal({ + title: '覆盖确认', + content: '将覆盖本周期已提交的计划,确定保存?', + success: (r) => { + if (r.confirm) { + doPost(); + } + } + }); + } else { + doPost(); + } + } +}); diff --git a/pages/plan-submit/plan-submit.json b/pages/plan-submit/plan-submit.json new file mode 100644 index 0000000..a97367d --- /dev/null +++ b/pages/plan-submit/plan-submit.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} diff --git a/pages/plan-submit/plan-submit.wxml b/pages/plan-submit/plan-submit.wxml new file mode 100644 index 0000000..049b029 --- /dev/null +++ b/pages/plan-submit/plan-submit.wxml @@ -0,0 +1,95 @@ + + + + + {{typeLabel}} · {{groupName}} · {{periodKey}} + {{message}} + + + + + + + + + + + + + + + + + + + + ! + {{loadError}} + + + + + + {{message || '当前周期不可提交,仅可查看'}} + + + + {{content.length}}/1000 + + + + + + + × + + + + + + 保存将覆盖本周期已提交的计划 + + + + 历史计划 + + {{his.period_key}} · {{his.status}} + {{his.content}} + + + + + 暂无历史计划 + + + + diff --git a/pages/plan-submit/plan-submit.wxss b/pages/plan-submit/plan-submit.wxss new file mode 100644 index 0000000..74dccd9 --- /dev/null +++ b/pages/plan-submit/plan-submit.wxss @@ -0,0 +1,56 @@ +.page { height: 100vh; display: flex; flex-direction: column; box-sizing: border-box; overflow: hidden; background: #FDFBF7; } +.head { flex-shrink: 0; padding: 24rpx 32rpx 8rpx; } +.head-title { font-size: 34rpx; font-weight: 700; color: #2C1810; } +.head-sub { margin-top: 6rpx; font-size: 24rpx; color: #c0392b; } + +.form-scroll { flex: 1; min-height: 0; padding: 8rpx 32rpx 24rpx; box-sizing: border-box; } +.card { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 20rpx; box-shadow: 0 4rpx 16rpx rgba(44, 24, 16, 0.04); } + +/* shared feedback */ +.btn-hover { opacity: .85; transform: scale(.98); } + +/* skeleton */ +.skeleton-card { padding: 32rpx; } +.sk { background: #EDE3D6; border-radius: 10rpx; } +.sk-line { height: 28rpx; margin-bottom: 20rpx; } +.sk-block { height: 120rpx; background: #F2EAE0; border-radius: 12rpx; margin-top: 10rpx; } +.w-90 { width: 90%; } +.w-80 { width: 80%; } +.w-100 { width: 100%; } +.w-60 { width: 60%; } + +/* error state */ +.error-card { padding: 120rpx 40rpx; text-align: center; } +.error-icon { width: 96rpx; height: 96rpx; line-height: 96rpx; margin: 0 auto 24rpx; border-radius: 50%; background: #fdecea; color: #c0392b; font-size: 48rpx; font-weight: 700; } +.error-text { color: #5A4A3A; font-size: 28rpx; margin-bottom: 48rpx; } +.btn-retry { display: inline-block; min-width: 240rpx; background: #C67B30; color: #fff; border-radius: 12rpx; } + +.readonly-tip { margin: 8rpx 0 16rpx; padding: 14rpx 20rpx; background: #fff3e6; border-radius: 10rpx; color: #c0392b; font-size: 26rpx; text-align: center; } + +.content { width: 100%; min-height: 200rpx; box-sizing: border-box; padding: 16rpx; border: 1rpx solid #e4d9cb; border-radius: 12rpx; background: #FDFBF7; color: #2C1810; font-size: 28rpx; } +.content[disabled], .content.readonly { background: #f6f1ea; } +.char-count { margin-top: 8rpx; text-align: right; font-size: 22rpx; color: #B6A996; } + +.upload-section { margin-top: 20rpx; } +.upload-btn { background: #FDFBF7; color: #C67B30; border: 2rpx dashed #C67B30; border-radius: 12rpx; font-size: 26rpx; } +.upload-btn[disabled] { color: #d8c7b4; border-color: #d8c7b4; background: #FDFBF7; } +.thumb-grid { display: flex; flex-wrap: wrap; gap: 14rpx; margin-top: 18rpx; } +.thumb-item { position: relative; width: 156rpx; height: 156rpx; } +.thumb { width: 100%; height: 100%; border-radius: 12rpx; background: #f6efe6; } +.thumb-del { position: absolute; top: -12rpx; right: -12rpx; width: 44rpx; height: 44rpx; line-height: 40rpx; text-align: center; border-radius: 50%; background: #c0392b; color: #fff; font-size: 30rpx; } + +.btn-primary { background: #C67B30; color: #fff; border-radius: 12rpx; } +.btn-save { margin-top: 24rpx; } +.btn-primary[disabled] { background: #d8c7b4; color: #fff; } +.save-hint { margin-top: 12rpx; text-align: center; font-size: 22rpx; color: #B6A996; } + +.section-title { margin: 4rpx 8rpx 14rpx; font-size: 28rpx; font-weight: 700; color: #2C1810; } +.history-card { display: flex; flex-direction: column; gap: 10rpx; } +.history-head { font-size: 28rpx; font-weight: 700; color: #2C1810; } +.history-head .pending { color: #c0392b; } +.content-text { color: #5A4A3A; font-size: 26rpx; } +.history-thumbs { display: flex; flex-wrap: wrap; gap: 12rpx; margin-top: 8rpx; } +.history-thumb { width: 148rpx; height: 148rpx; border-radius: 12rpx; background: #f6efe6; } + +.state-block { padding: 48rpx 0; text-align: center; color: #B6A996; font-size: 26rpx; } +.empty { padding: 48rpx 0; text-align: center; color: #B6A996; font-size: 26rpx; } diff --git a/pages/plan/plan.js b/pages/plan/plan.js new file mode 100644 index 0000000..606b28a --- /dev/null +++ b/pages/plan/plan.js @@ -0,0 +1,145 @@ +const api = require('../../utils/api'); +const auth = require('../../utils/auth'); + +const TYPES = ['weekly', 'monthly', 'yearly']; +const TYPE_LABELS = ['周计划', '月计划', '年计划']; + +Page({ + data: { + types: TYPE_LABELS, + typeIndex: 0, + view: 'overview', // overview | detail + loading: false, + refreshing: false, + periodKey: '', + canSubmit: false, + message: '', + myChecked: false, + myStatus: '', + myRecord: null, + myThumbs: [], + unassignedCount: 0, + groups: [], + selectedGroupName: '', + members: [] + }, + + onShow() { + auth.ensureLogin().then((token) => { + if (!token) { + wx.switchTab({ url: '/pages/profile/profile' }); + return; + } + wx.removeStorageSync('plan_overview_dirty'); + this.setData({ view: 'overview' }); + this.loadOverview(); + }).catch(() => {}); + }, + + onTypeChange(e) { + const typeIndex = Number(e.currentTarget.dataset.index); + if (typeIndex === this.data.typeIndex) { + return; + } + this.setData({ typeIndex, view: 'overview', selectedGroupName: '', members: [] }); + this.loadOverview(); + }, + + onRefresh() { + this.setData({ refreshing: true }); + this.loadOverview(); + }, + + loadOverview() { + const type = TYPES[this.data.typeIndex]; + this.setData({ loading: true }); + api.request({ url: '/api/v1/plan/groups', data: { type } }) + .then((res) => { + const groups = (res.groups || []).map((g) => ({ + ...g, + percent: g.total ? Math.round(((g.checked_count || 0) / g.total) * 100) : 0 + })); + this.setData({ + loading: false, + refreshing: false, + periodKey: res.period_key || '', + canSubmit: !!res.can_submit, + message: res.message || '', + myChecked: !!res.my_checked, + myStatus: res.my_status || '', + myRecord: res.my_record || null, + myThumbs: ((res.my_record && res.my_record.attachments) || []).map((p) => this.planFileUrl(p)), + unassignedCount: res.unassigned_count || 0, + groups + }); + }) + .catch(() => { + this.setData({ loading: false, refreshing: false }); + wx.showToast({ title: '加载小组失败', icon: 'none' }); + }); + }, + + openGroup(e) { + const groupName = e.currentTarget.dataset.group; + if (!groupName) { + return; + } + this.setData({ selectedGroupName: groupName, view: 'detail', members: [] }); + this.loadDetail(); + }, + + loadDetail() { + const type = TYPES[this.data.typeIndex]; + this.setData({ loading: true }); + api.request({ + url: '/api/v1/plan/group', + data: { type, group_name: this.data.selectedGroupName, period_key: this.data.periodKey } + }).then((res) => { + this.setData({ + loading: false, + periodKey: res.period_key || this.data.periodKey, + canSubmit: !!res.can_submit, + message: res.message || '', + members: res.members || [] + }); + }).catch(() => { + this.setData({ loading: false }); + wx.showToast({ title: '加载成员失败', icon: 'none' }); + }); + }, + + backToOverview() { + this.setData({ view: 'overview', selectedGroupName: '', members: [] }); + this.loadOverview(); + }, + + planFileUrl(path) { + if (!path) return ''; + if (/^https?:\/\//i.test(path)) return path; + const parts = String(path).replace(/^attachments\//, '').split('/'); + if (parts.length < 4) return ''; + return api.STATIC_BASE_URL + '/clock/static/plan/' + parts.slice(0, 4).map(encodeURIComponent).join('/'); + }, + + previewMyImage(e) { + const index = Number(e.currentTarget.dataset.index); + const urls = this.data.myThumbs; + const src = urls[index]; + if (!src) return; + wx.previewImage({ current: src, urls }); + }, + + goSubmit() { + const type = TYPES[this.data.typeIndex]; + if (type === 'weekly' && !this.data.canSubmit) { + wx.showToast({ title: this.data.message || '当前不可打卡', icon: 'none' }); + return; + } + const params = [ + 'plan_type=' + type, + 'period_key=' + encodeURIComponent(this.data.periodKey), + 'group_name=' + ].join('&'); + wx.navigateTo({ url: '/pages/plan-submit/plan-submit?' + params }); + } +}); diff --git a/pages/plan/plan.json b/pages/plan/plan.json new file mode 100644 index 0000000..0f41438 --- /dev/null +++ b/pages/plan/plan.json @@ -0,0 +1 @@ +{ "usingComponents": {} } \ No newline at end of file diff --git a/pages/plan/plan.wxml b/pages/plan/plan.wxml new file mode 100644 index 0000000..694a1f9 --- /dev/null +++ b/pages/plan/plan.wxml @@ -0,0 +1,116 @@ + + + + + {{item}} + + + + + + {{types[typeIndex]}} + {{periodKey}} + + {{myChecked ? '已提交' : '待提交'}} + + + + {{myChecked ? '✓' : '●'}} + + {{myChecked ? '已完成' : '未完成'}} + 我的{{types[typeIndex]}} + + + + + + + 我上传的计划 + {{myRecord.status}} + + {{myRecord.checked_at}} 提交 + {{myRecord.content}} + + + + + + {{message}} + + + ◌ + 加载中... + + + + + + {{index + 1}} + {{group.group_name}} + + 已打卡 {{group.checked_count}} + {{group.total}}人 + + › + + + + + + + + 另有未分组 {{unassignedCount}} 人 + 暂无小组 + + + + + + + + ‹ 返回 + + {{selectedGroupName}} + {{periodKey}} + + + + + ◌ + 加载中... + + + + + {{index + 1}} + + {{member.name}} + {{member.record.checked_at}} 提交 + + + {{member.filled ? '已完成' : '未完成'}} + + + 暂无成员 + + + + \ No newline at end of file diff --git a/pages/plan/plan.wxss b/pages/plan/plan.wxss new file mode 100644 index 0000000..0bebaf0 --- /dev/null +++ b/pages/plan/plan.wxss @@ -0,0 +1,80 @@ +.page { height: 100vh; display: flex; flex-direction: column; box-sizing: border-box; overflow: hidden; background: #F6F2EC; } +.view-root { flex: 1; min-height: 0; display: flex; flex-direction: column; } + +.type-tabs { flex-shrink: 0; display: flex; align-items: center; gap: 8rpx; margin: 24rpx 32rpx 0; padding: 8rpx; background: #EFE7DC; border-radius: 999rpx; } +.type-tab { flex: 1; text-align: center; padding: 16rpx 0; color: #8B7A6A; background: transparent; border-radius: 999rpx; font-size: 28rpx; font-weight: 600; transition: all .18s ease; } +.type-tab.active { background: #fff; color: #C67B30; font-weight: 700; box-shadow: 0 4rpx 12rpx rgba(198,123,48,0.18); } +.type-tab-hover { opacity: .85; } + +.view-scroll { flex: 1; min-height: 0; padding: 8rpx 32rpx 32rpx; box-sizing: border-box; } +.overview-head { display: flex; align-items: flex-end; justify-content: space-between; padding: 24rpx 4rpx 16rpx; } +.head-title-wrap { min-width: 0; } +.head-title { font-size: 38rpx; font-weight: 800; color: #2C1810; letter-spacing: 1rpx; } +.head-sub { margin-top: 6rpx; font-size: 24rpx; color: #A99A89; } +.head-pill { flex-shrink: 0; padding: 8rpx 20rpx; border-radius: 999rpx; font-size: 22rpx; background: #fff; color: #C67B30; box-shadow: 0 2rpx 8rpx rgba(44,24,16,0.06); } + +.detail-head { display: flex; align-items: center; gap: 16rpx; padding: 28rpx 4rpx 8rpx; } +.detail-title-wrap { min-width: 0; } +.back { color: #C67B30; font-size: 30rpx; flex-shrink: 0; padding: 10rpx 16rpx 10rpx 0; border-radius: 12rpx; font-weight: 600; } +.back-hover { opacity: .7; } + +.my-status { display: flex; align-items: center; gap: 18rpx; padding: 24rpx; margin-bottom: 20rpx; border-radius: 20rpx; background: #fff; box-shadow: 0 6rpx 20rpx rgba(44,24,16,0.05); } +.my-status.checked { border-left: 8rpx solid #2E7D32; } +.my-status.unchecked { border-left: 8rpx solid #C0392B; } +.my-state-icon { width: 64rpx; height: 64rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 30rpx; font-weight: 800; flex-shrink: 0; } +.my-state-icon.done { background: linear-gradient(135deg, #2E7D32, #43A047); color: #fff; box-shadow: 0 4rpx 12rpx rgba(46,125,50,0.25); } +.my-state-icon.todo { background: linear-gradient(135deg, #C0392B, #E5736C); color: #fff; box-shadow: 0 4rpx 12rpx rgba(192,57,43,0.22); } +.my-status-left { flex: 1; min-width: 0; } +.my-status-label { font-size: 24rpx; color: #8B7A6A; } +.my-status-text { font-size: 32rpx; font-weight: 800; color: #2C1810; margin-top: 6rpx; } +.btn-mystatus { width: 208rpx; margin: 0; flex-shrink: 0; background: linear-gradient(135deg, #C67B30, #E4A05E); color: #fff; border-radius: 999rpx; font-size: 28rpx; font-weight: 700; box-shadow: 0 6rpx 14rpx rgba(198,123,48,0.3); } +.btn-mystatus.outline { background: #fff; color: #C67B30; border: 2rpx solid #C67B30; box-shadow: none; } +.btn-mystatus[disabled] { background: #D8C7B4; color: #fff; border: none; box-shadow: none; } +.btn-hover { opacity: .9; transform: scale(.98); } + +.window-msg { margin: 0 8rpx 20rpx; padding: 14rpx 22rpx; background: #FFF3E6; border-radius: 14rpx; text-align: center; color: #C0392B; font-size: 24rpx; border: 1rpx solid #F7D9B9; } + +.my-plan { margin-bottom: 20rpx; padding: 24rpx; border-radius: 20rpx; background: #fff; box-shadow: 0 6rpx 20rpx rgba(44,24,16,0.05); } +.my-plan-head { display: flex; align-items: center; justify-content: space-between; } +.my-plan-title { font-size: 26rpx; font-weight: 700; color: #6B5D50; } +.my-plan-status { padding: 6rpx 16rpx; border-radius: 999rpx; font-size: 22rpx; font-weight: 700; background: #E8F4EB; color: #2E7D32; } +.my-plan-status.pending { background: #FFF3E6; color: #C67B30; } +.my-plan-meta { margin-top: 8rpx; font-size: 22rpx; color: #A99A89; } +.my-plan-content { margin-top: 14rpx; padding: 16rpx 20rpx; background: #FBF7F1; border-radius: 14rpx; font-size: 28rpx; line-height: 1.7; color: #2C1810; white-space: pre-wrap; word-break: break-all; } +.my-plan-thumbs { display: flex; flex-wrap: wrap; gap: 14rpx; margin-top: 16rpx; } +.my-plan-thumb { width: 148rpx; height: 148rpx; border-radius: 14rpx; background: #F2EDE7; box-shadow: 0 2rpx 6rpx rgba(44,24,16,0.08); } + +.group-card { background: #fff; border-radius: 20rpx; padding: 22rpx 24rpx; margin-bottom: 18rpx; box-shadow: 0 6rpx 20rpx rgba(44,24,16,0.05); } +.group-card-hover { transform: scale(.99); box-shadow: 0 10rpx 24rpx rgba(44,24,16,0.10); } +.group-row { display: flex; align-items: center; gap: 16rpx; } +.group-avatar { width: 52rpx; height: 52rpx; border-radius: 14rpx; background: linear-gradient(135deg, #F3E3CF, #FBE7D1); color: #C67B30; font-size: 26rpx; font-weight: 800; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } +.group-name { font-size: 30rpx; font-weight: 800; color: #2C1810; flex-shrink: 0; } +.chevron { color: #CDBFAE; font-size: 32rpx; line-height: 1; flex-shrink: 0; transition: transform .15s ease; } +.group-card-hover .chevron { transform: translateX(4rpx); } +.group-counts { display: flex; align-items: center; justify-content: flex-end; gap: 10rpx; flex: 1; min-width: 0; } +.count { font-size: 22rpx; white-space: nowrap; } +.count.done { color: #2E7D32; background: #E8F4EB; padding: 6rpx 12rpx; border-radius: 999rpx; font-weight: 700; } +.count.total { color: #C67B30; background: #FFF3E6; padding: 6rpx 12rpx; border-radius: 999rpx; font-weight: 700; } +.progress-track { height: 10rpx; border-radius: 999rpx; background: #F2EDE7; margin-top: 18rpx; overflow: hidden; } +.progress-bar { height: 100%; border-radius: 999rpx; background: linear-gradient(90deg, #C67B30, #EFA45D); transition: width .35s ease; box-shadow: 0 2rpx 6rpx rgba(198,123,48,0.35); } + +.unassigned { margin: 4rpx 8rpx 20rpx; font-size: 24rpx; color: #8B7A6A; } + +.member-list { background: #fff; border-radius: 20rpx; padding: 8rpx 24rpx; box-shadow: 0 6rpx 20rpx rgba(44,24,16,0.05); } +.member-left { display: flex; align-items: center; min-width: 0; } +.member { display: flex; align-items: center; justify-content: space-between; padding: 22rpx 0; border-bottom: 1rpx solid #F2EDE7; } +.member:last-child { border-bottom: none; } +.member-avatar { width: 64rpx; height: 64rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 26rpx; font-weight: 800; flex-shrink: 0; } +.member-avatar.done { background: linear-gradient(135deg, #E8F4EB, #CFEAD5); color: #2E7D32; } +.member-avatar.unchecked { background: linear-gradient(135deg, #FDECEA, #F9D9D5); color: #C0392B; } +.member-info { margin-left: 18rpx; min-width: 0; } +.member-name { font-size: 28rpx; font-weight: 700; color: #2C1810; } +.member-meta { margin-top: 6rpx; font-size: 22rpx; color: #A99A89; } +.badge { flex-shrink: 0; padding: 8rpx 18rpx; border-radius: 999rpx; font-size: 22rpx; font-weight: 700; } +.badge.done { background: #E8F4EB; color: #2E7D32; } +.badge.unchecked { background: #FDECEA; color: #C0392B; } + +.state-block { padding: 64rpx 0; text-align: center; color: #A99A89; font-size: 26rpx; } +.state-icon { font-size: 40rpx; margin-bottom: 12rpx; color: #D8C7B4; animation: pulse 1.2s ease-in-out infinite; } +@keyframes pulse { 0%,100%{opacity:.4} 50%{opacity:1} } +.empty { padding: 64rpx 0; text-align: center; color: #A99A89; font-size: 26rpx; } \ No newline at end of file diff --git a/pages/profile/profile.js b/pages/profile/profile.js new file mode 100644 index 0000000..83b9d5c --- /dev/null +++ b/pages/profile/profile.js @@ -0,0 +1,224 @@ +const api = require('../../utils/api'); +const auth = require('../../utils/auth'); + +const ROLE_LABELS = { + student: '学员', + member: '成员', + teacher: '老师', + leader: '组长', + 'group_leader': '组长', + admin: '管理员' +}; +const LOGIN_TYPE_LABELS = { + wechat: '微信', + phone: '手机号', + admin: '管理员' +}; + +Page({ + data: { + view: 'loading', + profile: null, + phoneLoading: false, + phoneError: '', + loadError: '', + avatarSrc: '', + avatarFallback: '明', + roleLabel: '', + loginTypeLabel: '', + classGroups: [], + avatarUploading: false + }, + + onShow() { + this.setData({ phoneError: '' }); + this.loadPage(); + }, + + onPullDownRefresh() { + this.refresh().then(() => wx.stopPullDownRefresh()); + }, + + refresh() { + if (this.data.view !== 'profile') { + return this.loadPage(); + } + return new Promise((resolve) => { + api.request({ url: '/api/v1/profile' }) + .then((res) => { + this.applyProfile(res); + resolve(); + }) + .catch(() => { + wx.showToast({ title: '刷新失败', icon: 'none' }); + resolve(); + }); + }); + }, + + loadPage() { + this.setData({ loadError: '' }); + return auth.ensureLogin() + .then((token) => { + if (!token) { + this.showPhoneLogin(); + return null; + } + this.setData({ view: 'loading' }); + return this.loadProfile(); + }) + .catch(() => { + this.showPhoneLogin(); + }); + }, + + loadProfile() { + return api.request({ url: '/api/v1/profile' }) + .then((res) => { + this.applyProfile(res); + }) + .catch((err) => { + if (!auth.getToken()) { + this.showPhoneLogin(); + } else { + this.setData({ + view: 'loadError', + loadError: (err && err.error) || '加载失败' + }); + } + }); + }, + + applyProfile(res) { + const displayName = res.display_name || res.name || ''; + const classGroups = (res.class_groups || []).map((g) => ({ + class_id: g.class_id, + class_code: g.class_code, + period: g.period, + group: g.group, + role: this.roleLabel(g.role) + })); + this.setData({ + view: 'profile', + profile: res, + phoneError: '', + loadError: '', + avatarSrc: this.fullUrl(res.wechat_avatar), + avatarFallback: this.firstChar(displayName), + roleLabel: this.roleLabel(res.role), + loginTypeLabel: this.loginTypeLabel(res.login_type), + classGroups + }); + }, + + showPhoneLogin() { + this.setData({ + view: 'phoneLogin', + phoneLoading: false + }); + }, + + roleLabel(role) { + return ROLE_LABELS[role] || role || '学员'; + }, + + loginTypeLabel(type) { + return LOGIN_TYPE_LABELS[type] || type || '微信'; + }, + + fullUrl(rel) { + if (!rel) return ''; + return /^https?:\/\//i.test(rel) ? rel : api.STATIC_BASE_URL + rel; + }, + + firstChar(str) { + const t = (str && typeof str === 'string') ? str.trim() : ''; + return t ? t.charAt(0) : '明'; + }, + + onChooseAvatar(e) { + const path = e.detail && e.detail.avatarUrl; + console.log('[avatar] chooseAvatar ->', path); + if (!path) { + wx.showToast({ title: '未选择头像', icon: 'none' }); + return; + } + this.setData({ avatarUploading: true }); + wx.uploadFile({ + url: api.BASE_URL + '/api/v1/profile/avatar', + filePath: path, + name: 'file', + header: { Authorization: 'Bearer ' + auth.getToken() }, + success: (res) => { + console.log('[avatar] upload success', res.statusCode, res.data); + let data = {}; + try { + data = JSON.parse(res.data); + } catch (e) { + data = {}; + } + if (data.success && data.wechat_avatar) { + const newAvatar = data.wechat_avatar; + const newUrl = this.fullUrl(newAvatar); + this.setData({ + 'profile.wechat_avatar': newAvatar, + avatarSrc: newUrl + }); + wx.showToast({ title: '头像已更新', icon: 'success' }); + } else { + wx.showToast({ title: data.error || '上传失败', icon: 'none' }); + } + }, + fail: (err) => { + console.error('[avatar] upload fail', err); + console.error('[avatar] filePath', path); + wx.showToast({ title: '上传失败', icon: 'none' }); + }, + complete: () => { + this.setData({ avatarUploading: false }); + } + }); + }, + + onGetPhoneNumber(e) { + if (this.data.phoneLoading) { + return; + } + const phoneCode = e.detail && e.detail.code; + if (!phoneCode) { + const message = '未授权手机号,请点击上方按钮重试'; + this.setData({ phoneError: message }); + wx.showToast({ title: message, icon: 'none' }); + return; + } + this.setData({ phoneLoading: true, phoneError: '' }); + auth.loginWithPhone(phoneCode) + .then(() => this.loadProfile()) + .catch((err) => { + const message = (err && err.error) || '手机号登录失败'; + this.setData({ phoneLoading: false, phoneError: message }); + wx.showToast({ title: message, icon: 'none', duration: 2500 }); + }); + }, + + onRetry() { + this.loadPage(); + }, + + logout() { + wx.showModal({ + title: '退出登录', + content: '确定要退出当前账号吗?', + confirmText: '退出', + confirmColor: '#c0392b', + success: (res) => { + if (!res.confirm) { + return; + } + api.request({ url: '/api/v1/auth/logout', method: 'POST' }).catch(() => {}); + auth.clearToken(); + wx.reLaunch({ url: '/pages/home/home' }); + } + }); + } +}); \ No newline at end of file diff --git a/pages/profile/profile.json b/pages/profile/profile.json new file mode 100644 index 0000000..287657f --- /dev/null +++ b/pages/profile/profile.json @@ -0,0 +1,5 @@ +{ + "usingComponents": {}, + "enablePullDownRefresh": true, + "backgroundTextStyle": "dark" +} diff --git a/pages/profile/profile.wxml b/pages/profile/profile.wxml new file mode 100644 index 0000000..e2c0798 --- /dev/null +++ b/pages/profile/profile.wxml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + ! + {{loadError || '加载失败,请稍后再试'}} + + + + + + + + + + + + + + + 📷 + + + + {{profile.display_name}} + {{roleLabel}} + + {{profile.student_id}} + + + + + + 个人信息 + + 姓名 + {{profile.name}} + + + 学号 + {{profile.student_id}} + + + 手机号 + {{profile.phone || '未绑定'}} + + + 角色 + {{roleLabel}} + + + 小组 + {{profile.group || '未分组'}} + + + 登录方式 + {{loginTypeLabel}} + + + + + 班级分组 + + {{item.period || item.class_code}} + {{item.group}} · {{item.role}} + + + + + + + + \ No newline at end of file diff --git a/pages/profile/profile.wxss b/pages/profile/profile.wxss new file mode 100644 index 0000000..d7ccdfa --- /dev/null +++ b/pages/profile/profile.wxss @@ -0,0 +1,194 @@ +button::after { border: none; } + +.page { + padding: 32rpx; + min-height: 100vh; + box-sizing: border-box; + background: #FDFBF7; +} + +.page.has-bottom { + padding-bottom: calc(32rpx + env(safe-area-inset-bottom)); +} + +/* ---- shared feedback ---- */ +.btn-hover { opacity: .85; transform: scale(.98); } +.link-hover { opacity: .7; } + +/* ---- skeleton ---- */ +.skeleton-card { padding: 40rpx 32rpx; } +.skeleton { + background: #eee; + border-radius: 8rpx; +} +.skeleton-avatar { + width: 112rpx; + height: 112rpx; + border-radius: 50%; + margin-bottom: 28rpx; +} +.skeleton-line { + height: 28rpx; + border-radius: 8rpx; + background: #eee; + margin-top: 20rpx; +} +.w-60 { width: 60%; } +.w-40 { width: 40%; } +.w-80 { width: 80%; } +.w-70 { width: 70%; } + +/* ---- error state ---- */ +.error-card { + margin-top: 120rpx; + text-align: center; +} +.error-icon { + width: 96rpx; + height: 96rpx; + line-height: 96rpx; + margin: 0 auto 24rpx; + border-radius: 50%; + background: #fdecea; + color: #c0392b; + font-size: 48rpx; + font-weight: 700; + text-align: center; +} +.error-title { + color: #5A4A3A; + font-size: 28rpx; + margin-bottom: 48rpx; +} +.btn-retry { + display: inline-block; + min-width: 240rpx; + background: #C67B30; + color: #fff; + border-radius: 12rpx; +} + +/* ---- phone login ---- */ +.login-wrap { + min-height: calc(100vh - 200rpx); + display: flex; + align-items: center; + justify-content: center; +} +.login-card { width: 100%; text-align: center; } +.login-icon { font-size: 96rpx; line-height: 1; margin-bottom: 24rpx; } +.login-title { font-size: 40rpx; font-weight: 700; color: #C67B30; margin-bottom: 20rpx; } +.login-desc { font-size: 26rpx; color: #8B7A6A; line-height: 1.6; margin-bottom: 48rpx; } +.btn-phone { width: 100%; background: #C67B30; color: #fff; border-radius: 12rpx; } +.btn-phone[disabled] { background: #d8c7b4; color: #fff; } +.error { margin-top: 24rpx; font-size: 26rpx; color: #c0392b; } + +/* ---- profile header ---- */ +.profile-card { padding: 32rpx; } +.profile-head { display: flex; align-items: center; } +.avatar, +.avatar-fallback { + width: 112rpx; + height: 112rpx; + border-radius: 50%; + overflow: hidden; +} +.avatar { background: #f6efe6; } +.avatar-fallback { + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #C67B30, #e3b37c); + color: #fff; + font-size: 48rpx; + font-weight: 700; +} +.avatar-click { + position: relative; + flex-shrink: 0; +} +.avatar-btn { + width: 112rpx; + height: 112rpx; + margin: 0; + padding: 0; + line-height: 1; + border: none; + background: transparent; + border-radius: 50%; + overflow: hidden; + display: block; +} +.avatar-btn .avatar { display: block; } +.avatar-btn[disabled] { opacity: .7; } +.avatar-camera { + position: absolute; + right: -2rpx; + bottom: -2rpx; + width: 40rpx; + height: 40rpx; + line-height: 36rpx; + text-align: center; + font-size: 20rpx; + color: #fff; + background: #C67B30; + border-radius: 50%; + border: 3rpx solid #FDFBF7; +} +.avatar-hover { opacity: .85; transform: scale(.98); } +.profile-meta { flex: 1; min-width: 0; margin-left: 24rpx; } +.profile-row { display: flex; align-items: center; gap: 12rpx; } +.profile-name { + font-size: 36rpx; + font-weight: 700; + color: #2C1810; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.profile-sub { margin-top: 8rpx; font-size: 24rpx; color: #8B7A6A; } +.badge { + padding: 4rpx 14rpx; + border-radius: 999rpx; + font-size: 20rpx; + flex-shrink: 0; +} +.badge.role { background: #f6efe6; color: #C67B30; } + +/* ---- info / class list ---- */ +.info-card, +.classlist-card { padding: 28rpx; } +.info-title { font-size: 30rpx; font-weight: 700; color: #2C1810; margin-bottom: 8rpx; } +.info-row, +.class-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 18rpx 0; + border-bottom: 1rpx solid #f2ede7; +} +.info-row:last-child, +.class-row:last-child { border-bottom: none; } +.info-label { color: #8B7A6A; font-size: 26rpx; flex-shrink: 0; } +.info-value { + color: #2C1810; + font-size: 28rpx; + max-width: 60%; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; +} +.classlist-card { margin-top: 24rpx; } +.classlist-card .info-title { margin-bottom: 8rpx; } + +/* ---- bottom ---- */ +.bottom-actions { + margin-top: 48rpx; + padding-top: 24rpx; +} +.btn-logout { + background: #fff; + color: #c0392b; + border: 1rpx solid #f0b4ae; + border-radius: 12rpx; +} \ No newline at end of file diff --git a/pages/webview/webview.js b/pages/webview/webview.js new file mode 100644 index 0000000..2cc3ba1 --- /dev/null +++ b/pages/webview/webview.js @@ -0,0 +1,8 @@ +Page({ + data: { url: '' }, + onLoad(options) { + if (options.url) { + this.setData({ url: decodeURIComponent(options.url) }); + } + } +}); \ No newline at end of file diff --git a/pages/webview/webview.json b/pages/webview/webview.json new file mode 100644 index 0000000..0f41438 --- /dev/null +++ b/pages/webview/webview.json @@ -0,0 +1 @@ +{ "usingComponents": {} } \ No newline at end of file diff --git a/pages/webview/webview.wxml b/pages/webview/webview.wxml new file mode 100644 index 0000000..4839609 --- /dev/null +++ b/pages/webview/webview.wxml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/webview/webview.wxss b/pages/webview/webview.wxss new file mode 100644 index 0000000..e69de29 diff --git a/project.config.json b/project.config.json new file mode 100644 index 0000000..ac3e7f3 --- /dev/null +++ b/project.config.json @@ -0,0 +1,18 @@ +{ + "description": "明缘进阶场", + "packOptions": { + "ignore": [] + }, + "setting": { + "urlCheck": true, + "es6": true, + "enhance": true, + "postcss": true, + "minified": true + }, + "compileType": "miniprogram", + "libVersion": "3.0.0", + "appid": "wx0f576e94afa52e1a", + "projectname": "mingyuan-clock", + "simulatorPluginLibVersion": {} +} \ No newline at end of file diff --git a/project.private.config.json b/project.private.config.json new file mode 100644 index 0000000..0c24ae1 --- /dev/null +++ b/project.private.config.json @@ -0,0 +1,46 @@ +{ + "condition": { + "miniprogram": { + "list": [ + { + "name": "pages/webview/webview", + "pathName": "pages/webview/webview", + "query": "", + "scene": null, + "launchMode": "default" + }, + { + "name": "pages/profile/profile", + "pathName": "pages/profile/profile", + "query": "", + "launchMode": "default", + "scene": null + }, + { + "name": "pages/plan/plan", + "pathName": "pages/plan/plan", + "query": "", + "launchMode": "default", + "scene": null + }, + { + "name": "pages/checkin/checkin", + "pathName": "pages/checkin/checkin", + "query": "", + "launchMode": "default", + "scene": null + }, + { + "name": "pages/home/home", + "pathName": "pages/home/home", + "query": "", + "launchMode": "default", + "scene": null + } + ] + } + }, + "setting": { + "urlCheck": false + } +} \ No newline at end of file diff --git a/sitemap.json b/sitemap.json new file mode 100644 index 0000000..726488b --- /dev/null +++ b/sitemap.json @@ -0,0 +1,6 @@ +{ + "desc": "明缘进阶场小程序", + "rules": [ + { "action": "allow", "page": "*" } + ] +} \ No newline at end of file diff --git a/utils/api.js b/utils/api.js new file mode 100644 index 0000000..af6b66c --- /dev/null +++ b/utils/api.js @@ -0,0 +1,76 @@ +//const BASE_URL = 'https://muguaclub.cn'; +//const BASE_URL = "http://127.0.0.1:8000"; +const BASE_URL = 'https://m.muguaclub.cn/clock'; +const STATIC_BASE_URL = 'https://m.muguaclub.cn'; + +let redirectingProfile = false; + +function goProfile() { + if (redirectingProfile) { + return; + } + const pages = getCurrentPages(); + const current = pages.length ? pages[pages.length - 1] : null; + const route = current && current.route; + if (route === 'pages/profile/profile') { + return; + } + redirectingProfile = true; + wx.switchTab({ + url: '/pages/profile/profile', + complete() { + setTimeout(() => { + redirectingProfile = false; + }, 1200); + } + }); +} + +function request(options) { + const { url, method = 'GET', data = {}, auth = true } = options; + const isIdempotent = method === 'GET' || method === 'DELETE'; + let retried = false; + + return new Promise((resolve, reject) => { + const doRequest = () => { + const header = { 'Content-Type': 'application/json' }; + const token = wx.getStorageSync('token'); + if (auth && token) { + header['Authorization'] = 'Bearer ' + token; + } + wx.request({ + url: BASE_URL + url, + method, + data, + header, + success(res) { + if (res.statusCode >= 200 && res.statusCode < 300) { + resolve(res.data); + } else if (res.statusCode === 401) { + wx.removeStorageSync('token'); + if (auth) { + goProfile(); + } + reject(res.data || { error: '未登录' }); + } else { + reject(res.data || { error: '请求失败' }); + } + }, + fail(err) { + // 冷启动时首个请求可能因网络栈未就绪而失败,空闲时对幂等请求自动重试一次 + const msg = (err && err.errMsg) || ''; + const domainError = /url not in domain list|domain not/i.test(msg); + if (!retried && isIdempotent && !domainError) { + retried = true; + setTimeout(doRequest, 800); + return; + } + reject(err); + } + }); + }; + doRequest(); + }); +} + +module.exports = { request, BASE_URL, STATIC_BASE_URL }; diff --git a/utils/auth.js b/utils/auth.js new file mode 100644 index 0000000..ff2c615 --- /dev/null +++ b/utils/auth.js @@ -0,0 +1,109 @@ +const api = require('./api'); + +let loginPromise = null; + +function getToken() { + return wx.getStorageSync('token') || ''; +} + +function setToken(token) { + wx.setStorageSync('token', token); +} + +function clearToken() { + wx.removeStorageSync('token'); +} + +function wxLogin(retries) { + const remain = typeof retries === 'number' ? retries : 3; + return new Promise((resolve, reject) => { + wx.login({ + success(res) { + if (res && res.code) { + resolve(res.code); + } else { + reject(new Error((res && res.errMsg) || '微信登录失败')); + } + }, + fail: reject + }); + }).catch((err) => { + // 冷启动时微信登录服务可能尚未就绪,指数退避重试 + if (remain > 0) { + const delay = 400 + (4 - remain) * 200; // 首次 600ms,依次 800ms、1000ms + return new Promise((resolve) => setTimeout(resolve, delay)) + .then(() => wxLogin(remain - 1)); + } + throw err; + }); +} + +// 整个微信登录流程失败时重新取 code 再试一次,覆盖冷启动首个网络请求偶发失败 +function wechatLogin(retries) { + const remain = typeof retries === 'number' ? retries : 1; + return wxLogin() + .then((code) => api.request({ + url: '/api/v1/auth/wechat-login', + method: 'POST', + data: { code }, + auth: false + })) + .catch((err) => { + if (remain > 0) { + return new Promise((resolve) => setTimeout(resolve, 500)).then(() => wechatLogin(remain - 1)); + } + throw err; + }); +} + +// 单例登录:App.onLaunch 与首屏页面 onShow 可能几乎同时调用, +// 共享同一个进行中的登录 Promise,避免重复 wx.login / wechat-login 造成竞态。 +// 关键:登录失败时必须清空单例,让下一次调用能重新发起, +// 否则冷启动 wx.login 偶发失败后,后续页面永远拿不到 token。 +function ensureLogin(refresh) { + const existing = getToken(); + if (existing && !refresh) { + return Promise.resolve(existing); + } + if (loginPromise) { + return loginPromise; + } + const p = wechatLogin() + .then((res) => { + if (res && res.success) { + setToken(res.token); + return res.token; + } + // need_phone:未绑定手机号时 resolve(null),由页面引导去「我的」绑定 + return null; + }) + .finally(() => { + // 无论成功失败,完成后都清掉单例,让下一次调用可以重新发起 + if (loginPromise === p) { + loginPromise = null; + } + }); + loginPromise = p; + return p; +} + +function loginWithPhone(phoneCode) { + return wxLogin() + .then((code) => api.request({ + url: '/api/v1/auth/bind-phone', + method: 'POST', + data: { code, phone_code: phoneCode }, + auth: false + })) + .then((res) => { + if (res && res.success) { + setToken(res.token); + return res; + } + const error = new Error((res && res.error) || '手机号登录失败'); + error.error = (res && res.error) || '手机号登录失败'; + throw error; + }); +} + +module.exports = { getToken, setToken, clearToken, wxLogin, ensureLogin, loginWithPhone }; \ No newline at end of file