From 6c25d242cf994ce4a2ef2077569912b5f88b72a0 Mon Sep 17 00:00:00 2001 From: honghefly Date: Fri, 8 May 2026 17:00:10 +0800 Subject: [PATCH] first commit --- app.js | 100 +++ app.json | 47 ++ app.wxss | 96 +++ images/tabbar/home.png | Bin 0 -> 880 bytes images/tabbar/home.svg | 3 + images/tabbar/home@2x.png | Bin 0 -> 1676 bytes images/tabbar/home_active.png | Bin 0 -> 1009 bytes images/tabbar/home_active.svg | 3 + images/tabbar/home_active@2x.png | Bin 0 -> 1889 bytes images/tabbar/practice.png | Bin 0 -> 606 bytes images/tabbar/practice.svg | 6 + images/tabbar/practice@2x.png | Bin 0 -> 1275 bytes images/tabbar/practice_active.png | Bin 0 -> 761 bytes images/tabbar/practice_active.svg | 6 + images/tabbar/practice_active@2x.png | Bin 0 -> 1516 bytes images/tabbar/profile.png | Bin 0 -> 1247 bytes images/tabbar/profile.svg | 4 + images/tabbar/profile@2x.png | Bin 0 -> 2419 bytes images/tabbar/profile_active.png | Bin 0 -> 1449 bytes images/tabbar/profile_active.svg | 4 + images/tabbar/profile_active@2x.png | Bin 0 -> 2847 bytes miniprogram_npm/@jdmini/api/index.d.ts | 295 ++++++++ miniprogram_npm/@jdmini/api/index.js | 4 + .../@jdmini/components/icons/home-active.png | Bin 0 -> 1575 bytes .../@jdmini/components/icons/home.png | Bin 0 -> 1070 bytes .../@jdmini/components/icons/link-active.png | Bin 0 -> 2796 bytes .../@jdmini/components/icons/link.png | Bin 0 -> 1798 bytes .../@jdmini/components/jdwx-ad/index.js | 22 + .../@jdmini/components/jdwx-ad/index.json | 3 + .../@jdmini/components/jdwx-ad/index.wxml | 5 + .../@jdmini/components/jdwx-ad/index.wxss | 7 + .../@jdmini/components/jdwx-link/index.js | 37 + .../@jdmini/components/jdwx-link/index.json | 3 + .../@jdmini/components/jdwx-link/index.wxml | 11 + .../@jdmini/components/jdwx-link/index.wxss | 63 ++ package-lock.json | 26 + package.json | 6 + pages/category/category.js | 43 ++ pages/category/category.json | 4 + pages/category/category.wxml | 51 ++ pages/category/category.wxss | 99 +++ pages/course-detail/course-detail.js | 60 ++ pages/course-detail/course-detail.json | 4 + pages/course-detail/course-detail.wxml | 74 ++ pages/course-detail/course-detail.wxss | 165 +++++ pages/home/home.js | 68 ++ pages/home/home.json | 6 + pages/home/home.wxml | 126 ++++ pages/home/home.wxss | 241 +++++++ pages/practice/practice.js | 60 ++ pages/practice/practice.json | 4 + pages/practice/practice.wxml | 62 ++ pages/practice/practice.wxss | 151 ++++ pages/profile/profile.js | 86 +++ pages/profile/profile.json | 4 + pages/profile/profile.wxml | 111 +++ pages/profile/profile.wxss | 214 ++++++ pages/study-step/study-step.js | 83 +++ pages/study-step/study-step.json | 4 + pages/study-step/study-step.wxml | 55 ++ pages/study-step/study-step.wxss | 154 +++++ pages/work-submit/work-submit.js | 65 ++ pages/work-submit/work-submit.json | 4 + pages/work-submit/work-submit.wxml | 91 +++ pages/work-submit/work-submit.wxss | 206 ++++++ project.config.json | 41 ++ project.private.config.json | 24 + sitemap.json | 7 + utils/api.js | 10 + utils/auth.js | 269 ++++++++ utils/config.js | 36 + utils/data.js | 644 ++++++++++++++++++ utils/httpClient.js | 88 +++ utils/index.js | 27 + utils/request.js | 278 ++++++++ utils/storage.js | 148 ++++ 76 files changed, 4618 insertions(+) create mode 100644 app.js create mode 100644 app.json create mode 100644 app.wxss create mode 100644 images/tabbar/home.png create mode 100644 images/tabbar/home.svg create mode 100644 images/tabbar/home@2x.png create mode 100644 images/tabbar/home_active.png create mode 100644 images/tabbar/home_active.svg create mode 100644 images/tabbar/home_active@2x.png create mode 100644 images/tabbar/practice.png create mode 100644 images/tabbar/practice.svg create mode 100644 images/tabbar/practice@2x.png create mode 100644 images/tabbar/practice_active.png create mode 100644 images/tabbar/practice_active.svg create mode 100644 images/tabbar/practice_active@2x.png create mode 100644 images/tabbar/profile.png create mode 100644 images/tabbar/profile.svg create mode 100644 images/tabbar/profile@2x.png create mode 100644 images/tabbar/profile_active.png create mode 100644 images/tabbar/profile_active.svg create mode 100644 images/tabbar/profile_active@2x.png create mode 100644 miniprogram_npm/@jdmini/api/index.d.ts create mode 100644 miniprogram_npm/@jdmini/api/index.js create mode 100644 miniprogram_npm/@jdmini/components/icons/home-active.png create mode 100644 miniprogram_npm/@jdmini/components/icons/home.png create mode 100644 miniprogram_npm/@jdmini/components/icons/link-active.png create mode 100644 miniprogram_npm/@jdmini/components/icons/link.png create mode 100644 miniprogram_npm/@jdmini/components/jdwx-ad/index.js create mode 100644 miniprogram_npm/@jdmini/components/jdwx-ad/index.json create mode 100644 miniprogram_npm/@jdmini/components/jdwx-ad/index.wxml create mode 100644 miniprogram_npm/@jdmini/components/jdwx-ad/index.wxss create mode 100644 miniprogram_npm/@jdmini/components/jdwx-link/index.js create mode 100644 miniprogram_npm/@jdmini/components/jdwx-link/index.json create mode 100644 miniprogram_npm/@jdmini/components/jdwx-link/index.wxml create mode 100644 miniprogram_npm/@jdmini/components/jdwx-link/index.wxss create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pages/category/category.js create mode 100644 pages/category/category.json create mode 100644 pages/category/category.wxml create mode 100644 pages/category/category.wxss create mode 100644 pages/course-detail/course-detail.js create mode 100644 pages/course-detail/course-detail.json create mode 100644 pages/course-detail/course-detail.wxml create mode 100644 pages/course-detail/course-detail.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/practice/practice.js create mode 100644 pages/practice/practice.json create mode 100644 pages/practice/practice.wxml create mode 100644 pages/practice/practice.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/study-step/study-step.js create mode 100644 pages/study-step/study-step.json create mode 100644 pages/study-step/study-step.wxml create mode 100644 pages/study-step/study-step.wxss create mode 100644 pages/work-submit/work-submit.js create mode 100644 pages/work-submit/work-submit.json create mode 100644 pages/work-submit/work-submit.wxml create mode 100644 pages/work-submit/work-submit.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 create mode 100644 utils/config.js create mode 100644 utils/data.js create mode 100644 utils/httpClient.js create mode 100644 utils/index.js create mode 100644 utils/request.js create mode 100644 utils/storage.js diff --git a/app.js b/app.js new file mode 100644 index 0000000..8377930 --- /dev/null +++ b/app.js @@ -0,0 +1,100 @@ +import { injectApp, waitLogin, gatewayHttpClient } from '@jdmini/api' +const storage = require('./utils/storage.js') + +App(injectApp()({ + globalData: { + userSettings: null + }, + async onLaunch() { + if (wx.canIUse('getUpdateManager')) { + const updateManager = wx.getUpdateManager(); + updateManager.onCheckForUpdate(function (res) { + if (res.hasUpdate) { + updateManager.onUpdateReady(function () { + wx.showModal({ + title: '更新提示', + content: '新版本已经准备好,是否重启应用?', + success(res) { + if (res.confirm) { + updateManager.applyUpdate(); + } + } + }); + }); + } + }); + } + // 等待登录完成 + await waitLogin() + // 初始化用户设置(计算学习天数) + this.globalData.userSettings = storage.initSettings() + }, + + // 内容安全 + async checkdata(txt = '', checkType, mediaUrl = '') { + try { + if (!checkType || (checkType !== 2 && checkType !== 3)) { + throw new Error('checkType必须为2(图片检测)或3(文本检测)') + } + if (checkType === 3 && !txt) { + throw new Error('文本检测时content不能为空') + } + if (checkType === 2 && !mediaUrl) { + throw new Error('图片检测时mediaUrl不能为空') + } + const postdata = { content: txt, checkType: checkType, mediaUrl: mediaUrl } + const data = await gatewayHttpClient.request('/wx/v1/api/app/content/check', 'post', postdata) + if (data.code === 200) { + if (checkType == 3) return data.data.suggest === 'pass' ? 1 : 2 + if (checkType == 2) return data.data.id || null + } else { + return 2 + } + } catch (error) { + console.error('checkdata error:', error) + return 2 + } + }, + + async checkimage(imgurl) { + wx.showLoading({ title: '正在检查图片...', mask: true }); + try { + const upfileData = await gatewayHttpClient.uploadFile(imgurl, 'image'); + const checkid = await this.checkdata('', 2, upfileData.data.url) + let retryCount = 0; + const maxRetries = 5; + while (retryCount < maxRetries) { + await new Promise(resolve => setTimeout(resolve, 1000)); + const passcode = await this.checkSafetyResults(checkid); + switch (passcode) { + case 100: + wx.hideLoading(); + return upfileData.data; + case 20001: + case 20002: + case 20006: + case 21000: + wx.hideLoading(); + await gatewayHttpClient.deleteFile(upfileData.data.id); + this.showwarning('图片含有违规内容,请重新选择'); + return null; + default: + break; + } + retryCount++; + } + wx.hideLoading(); + await gatewayHttpClient.deleteFile(upfileData.data.id); + wx.showToast({ title: '图片检查超时,请重试', icon: 'none' }); + return null; + } catch (error) { + wx.hideLoading(); + wx.showToast({ title: '检查失败,请重试', icon: 'none' }); + return null; + } + }, + + showwarning(txt) { + wx.showModal({ title: '提示', content: txt, showCancel: false }); + } +})) diff --git a/app.json b/app.json new file mode 100644 index 0000000..0c9da20 --- /dev/null +++ b/app.json @@ -0,0 +1,47 @@ +{ + "pages": [ + "pages/home/home", + "pages/category/category", + "pages/course-detail/course-detail", + "pages/study-step/study-step", + "pages/practice/practice", + "pages/work-submit/work-submit", + "pages/profile/profile" + ], + "window": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "画画怎么画", + "navigationBarBackgroundColor": "#ffffff", + "backgroundColor": "#f7f8fc" + }, + "tabBar": { + "color": "#999999", + "selectedColor": "#6C8CFF", + "backgroundColor": "#ffffff", + "borderStyle": "white", + "list": [ + { + "pagePath": "pages/home/home", + "text": "首页", + "iconPath": "images/tabbar/home.png", + "selectedIconPath": "images/tabbar/home_active.png" + }, + { + "pagePath": "pages/practice/practice", + "text": "练习", + "iconPath": "images/tabbar/practice.png", + "selectedIconPath": "images/tabbar/practice_active.png" + }, + { + "pagePath": "pages/profile/profile", + "text": "我的", + "iconPath": "images/tabbar/profile.png", + "selectedIconPath": "images/tabbar/profile_active.png" + } + ] + }, + "style": "v2", + "componentFramework": "glass-easel", + "sitemapLocation": "sitemap.json", + "lazyCodeLoading": "requiredComponents" +} diff --git a/app.wxss b/app.wxss new file mode 100644 index 0000000..7f208bd --- /dev/null +++ b/app.wxss @@ -0,0 +1,96 @@ +/* 全局样式 */ +page { + background-color: #f7f8fc; + font-family: -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif; + color: #333333; + font-size: 28rpx; + box-sizing: border-box; +} + + + +/* 公共容器 */ +.page-container { + min-height: 100vh; + padding-bottom: 40rpx; +} + +/* 卡片通用样式 */ +.card { + background: #ffffff; + border-radius: 24rpx; + overflow: hidden; +} + +/* 主色 */ +.text-primary { color: #6C8CFF; } +.text-orange { color: #FFB84D; } +.text-gray { color: #999999; } +.text-dark { color: #333333; } + +/* 徽章/标签 */ +.badge { + display: inline-block; + padding: 4rpx 16rpx; + border-radius: 20rpx; + font-size: 22rpx; + line-height: 1.4; +} +.badge-blue { background: #EEF1FF; color: #6C8CFF; } +.badge-orange { background: #FFF4E5; color: #FFB84D; } +.badge-green { background: #E8FAF0; color: #3CB371; } +.badge-gray { background: #F2F2F2; color: #999999; } + +/* 通用按钮 */ +.btn-primary { + background: #6C8CFF; + color: #ffffff; + border-radius: 50rpx; + text-align: center; + font-size: 32rpx; + font-weight: 500; + border: none; + padding: 24rpx 0; +} +.btn-primary::after { border: none; } + +.btn-outline { + background: #ffffff; + color: #6C8CFF; + border: 2rpx solid #6C8CFF; + border-radius: 50rpx; + text-align: center; + font-size: 32rpx; + font-weight: 500; + padding: 24rpx 0; +} +.btn-outline::after { border: none; } + +/* 分割线 */ +.divider { + height: 1rpx; + background: #F0F0F0; + margin: 0 32rpx; +} + +/* 空状态 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 80rpx 40rpx; + color: #BBBBBB; +} +.empty-state .empty-icon { + font-size: 80rpx; + margin-bottom: 20rpx; +} +.empty-state .empty-text { + font-size: 28rpx; +} + +/* 安全区底部 */ +.safe-bottom { + height: env(safe-area-inset-bottom); +} diff --git a/images/tabbar/home.png b/images/tabbar/home.png new file mode 100644 index 0000000000000000000000000000000000000000..ef044db8c773c799b99c3efa7db2df2f425b7dc4 GIT binary patch literal 880 zcmeAS@N?(olHy`uVBq!ia0vp^fgsGm1|(PYdzmpXFq?b2IEGZrc{^Ki@?{5!w)h7u zeO@W58V-v%_>OzZ?2Kpl!=%F(a3Wg7f{(px-eE>Q_d@k<=S}x>j8Z1?UiZCsw(d;L z`7`Ew)pl;F-L*S^lP7<|nP&{ga(t8+Cwm-F5i;mxL1OATnfKS!X31SGVyv?K7Fs^f zI^I2?!>v=ha=Rn#>I&~Z`eBux-MMWl{~-0`eW(x?Q5MM z9?;Y*)NzPQ*4Wzp`Rj)5Ycd{8d37$CQBil5-Shfvp6IxX2Q(Eoy4g4^QMG*2p1mzx z&|>-q9e=^5M@8u~SvqPyocBn6{knPm?S1RaPN(eWpS9nAqky;Pl#lPHu0Ou_=_kkc ziX1Z!&0N2oL+Se5r=g!?zWtY;8L1SpjLk!fua!yt`wWdobqP@oOM;Z;EH++WJ}Ee< z_X5*y_6sW+Cv_b*D7ZB*Kv{U_u_GH9RtoHj@Y~kG?l=3^mjj93GTalsbI3p9IlUp{ zfkAUB&<{x(rJhUNzntH=vhnaFjYq#8&sSjH>s*=DQv5|vDZu5NT%5!~pe+j97dS6? zAaKur+JTwYYIfhh@_c0Y$rroj@scfeSr2@sYTSP$&9ndLQf3v=mxq)-?OLBb_o?Wi z310+-nbJ?q__01)VfM4Ydv63wV!0=NukG1?*iZCkTF?WZi7yOn_I6#|=A}F5$FBXy z%T6v|Y^i^5{eteC_vu|nzRk#&jZ}~Q!}Ud3Q`UOR#Jo8*KGv>__5|2&%4>dTD$v_; zfJ@~~oD7rN#0F251R>5DE*Q)OzDf1x^xJmr^vV^I{WkmaH|Br7?eH>Y=~82CiOff-agI>F!X4XbzwDQOpX>8ZdY0A`#pnAbCwixx z`}p=;rCqyj$AlwN_rW)9Ctp(*N N22WQ%mvv4FO#t|`h&2EJ literal 0 HcmV?d00001 diff --git a/images/tabbar/home.svg b/images/tabbar/home.svg new file mode 100644 index 0000000..bda778e --- /dev/null +++ b/images/tabbar/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/images/tabbar/home@2x.png b/images/tabbar/home@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fb25b5539b3cc2b7690029c65974e526ad19bfdc GIT binary patch literal 1676 zcmb7_e>Bs37{}Eu7IDpQliwy%;yM#0$~9${{QeaRow}?z7V2W$*5Yf^6vIwze$LM} zMvE0+aoDQsN1@c2D642AVQqe1SNGiiI``i0AD`!Q&htE<=Xw9}Jg-l-x7RUcMVO+T zoSd@9aW~)1yW;DC$Zy*21uZ=}Ie9-1x1;`7N@icP3k7aEz4J;{Wu{JBo~Sp$^((F^ zPM4|c`xC6g%PN)XEL!}Z-lF&eD)ofjz?EyS$!fS;X|Bx}43GW((9p)^fs&;Q=Q<*m zQ_eXvoj~J<50$sbLrnInx&<2nO-EECuNrl4JxbV)hbV1>YW-)Rv05bH3ulkv#DtD^ z^BHm61c>{HXJ2 z`y!X$j zL@Gqs*{!KzHR`z!)$JneoHLU!fLD0LWPsy!-b8d~%e>ThE;Rr$NrEx7;Vv9C#<6slN8L!^acx_9$4vBiqsVIIOzU@%r9q9kOShbz^qZSjD}q}WPow{fU;`J zgxc0vWDi?Fe~Zp_oez%cNp&fo+e4%>a)BEQ#Jtc&^mT3FU4i}lHCc(#>9Rl~Ps zR+t5$yO>)3J#CH9QD4eC;zWWEGUt-&fs7}io|}2Yc%sf|)O(DwxR|2}=W6-&IIy6W zDCuzR8xx)-P|S4jt)Ap)z`0nWPJ_(%!5yej&N*;}cdP*NHVI3lmPDr~svPa(f& zbhT+ijc@8G>FXbSX!nC3TcW_>rWkX*a4%A{hPG$VPy>BhkN=0A^8Qbqi6%8WqxRm} zoKn)4H$kvukt;p$=Yn0Bt~%{i50~3Qjb26MxYSherfO$TDqcEE?W)?UOm$(8LY9W7}#NN5{mIPlnxDccY8fh*ROyM8oi{ep^J=CFRC?^Yv47+9R)5DL;ISYACR0$D{cPqUzzVji)KC zhDj1Qko8<-lZrPSD>=Gtb5H+2NxpqCc^4HV)9vd5$vYsEaW_<22G7iF^n>I>l$9OQ zOSOq@{nm)_Ab1$bD2@-3?V@7G$vFM@y@v_Js!^qUV3vZCAPsz&&@M20-u;8aX(!E#$JxlZ>hD;*T(T|sN;D>GY*A&i1{t y|4M#HT^}E(2Hys?I`VwycVYM+B>x-HKO6crS|AHVG_TEHM$QB2<;HS7gZ~q)H4P#F literal 0 HcmV?d00001 diff --git a/images/tabbar/home_active.png b/images/tabbar/home_active.png new file mode 100644 index 0000000000000000000000000000000000000000..c4964ab4446afdda0bcaafa37ab32a792dd8817e GIT binary patch literal 1009 zcmeAS@N?(olHy`uVBq!ia0vp^fgsGm1|(PYdzmpXFz@hmaSW-L^LCbDu&|@Z@&B_g zEUIDg^3u}1`jJ`U&BY5^JqFFj3CZneIGFn)m~Tn%Ygjz(lE-q@UR$s1P5a8@W;`hp z-*oc!?(jYL&zwDY?_S!^jl2J+6bBoqrEwiQGQ)+@XAy&1Aj4#h22YU#DqIOdtOlJ- zE2iz(^LYNnm5#?x9^6&UywpM?HTAfB)qLDZ8udsQ5}{-MgPN_vh9ulYd^U zH{bVaVSfGDSO-By4J%TH?MY~;oGO173uKe^5 ztLu+{1vGJWW;SejG2^A?zn57NZ!Ba!&+Pg4YUi=9Q7>z!2LGHjgLeW;d$85Ks($-$ zS3dI$iG7P7ht*$w#k}08vz77D1*MhJuHOUOw|hQuTDIfTft4;vQVMJDebrCBY+bYG zUETAK6a3cyI@R#GN|syY6OU@}uUR`by`Ejk=(OKn%2na;*B$$&9ncJY^l4qa=j%I) zMNBO|bwcp)1Cj_*y^7+2sYIN=73!&q@69QK8 z%vm91=@8UtJkxvj;&t_kPgwa1CwBFSSzNq!B(GtCdqPG-%Or7&KfmtqzvEXDbWKWZ zu(09an-IXo)c#voyTE06abUxR?wU`gIX*%P3#Uk0Bnh?WB+6bkx$We7!Rlv|U%&Cu zMUBcz8{aT+s+K+Q*`p@L>an)1jjfUAdo#!cwMibbH?+AXY&*`c(e!&VOLWTt$!x3T zujNl?r$}krIygN%P$-)d{NwKV7^{UZ_nMrulGOP)>%GIS{CK_0p8TvyGnbUAM~W}M zyy&*&)ni<99<2BAd|=nLpLypiiND)r*Sh*IXlc4QJ=N~-?^zrQx75XiEO=dyyz&3w zvE<=Cum8ID4$PL_@nVu-mGG~hzJKK_vYqFb=)E#sqa<+AU+47tzv5XR{ce5I^qudj z(fIEIfB5`3n|*RN<>iVkw%y9e2@WG2F7Qe^H!oi9)iQGrgg+a_A)6+2#dths(6r@Y{aH8cMK_K9|LulC$1 t`!G4)`F4w;;k0v17;c5+8C2m1@_ZWg`&Zap&j)5@22WQ%mvv4FO#pA9%O?N; literal 0 HcmV?d00001 diff --git a/images/tabbar/home_active.svg b/images/tabbar/home_active.svg new file mode 100644 index 0000000..3f58585 --- /dev/null +++ b/images/tabbar/home_active.svg @@ -0,0 +1,3 @@ + + + diff --git a/images/tabbar/home_active@2x.png b/images/tabbar/home_active@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..7cf48b92969fdda0fc09f68a25d008c656ed8fd1 GIT binary patch literal 1889 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 literal 0 HcmV?d00001 diff --git a/images/tabbar/practice.svg b/images/tabbar/practice.svg new file mode 100644 index 0000000..9796126 --- /dev/null +++ b/images/tabbar/practice.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/images/tabbar/practice@2x.png b/images/tabbar/practice@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..cffebd0abb07f9afe916faac52fe8ae1431f6b66 GIT binary patch literal 1275 zcmeAS@N?(olHy`uVBq!ia0vp^i$Iuz4M-mPBqq(kz;ejb#WAE}&fB{iCklH?I6R!s z%>4eugEo!^4gtYCX&Z_*DzvCFDKNRPG_<#OFmy1iZD)63_+2P!3nqS9_PRDS zHfQRSocF=!GpXsh)sXpUbVoUProo6iPvR^p+VCM9Xt9ouP-1v0P zv&yo4Gac9VY;oS){`m0$*=3%JX*Kc56)p0QUlp~k(@VFN`SEAY_T7v#?Jqr@t@ys@ z^8_RPWXV-CUF%;yyML;-k3OLpCSP4m~VdS9I=PC{V_ZpW#&3jMWdf`TOWy@0Wad|KyFZjeia1KEKKT z=m>{)lK9vDw?H@lzOkm~_LDp1g-KHcvZpPq)h)NVceL}O^B)N*?_53qy1Uclls*+- z>|Av=_3f1x8ZT}iurZh6uhP4_P?t~~IQ(t;yRRtv@@J!c(?71B__6k{wdu;Mq92uh zRX^L3@_FrU>DTqMFTLujkb9f;sXV*Xb;S!gT!?!vqUpEIUgZ&5!YdrSB+ z-`kgk?cZ8M}I!ANFx#LN-jdG9XUWm9*?8))5`02*q3pZYQw&|O>1(#EYy;i?1 zPw!H*>09?73>5gLyspf~j{9x)w#Oes=SZY}FOZXZms_b5zqXwD!>YL}Y|Q^i-99h4 zpXGr_$0q0BxlcFFIS{^Z|J35}>cv5`e=lCxvZJHU{j*tm>K@MfuKzpdte&%ZGcZM+ z6K8bbEEL_n|Mo=TS?x0-Gp3)7nftaP?%Yo;29Ns&r^7#JwbdU_k^H-J>GQt5bN1U@ zpUqoSx2`SL;aUH;(BpGXq;B~f9~6+q{>~0bo%`34uPeI#u8cYMn_*$=j8CCe zk;fJr<+UH1qMhirgG1s|EQgIxKXq!sXzh&{RIlJc^)>|OBY~`Kl zOS}KAN_+mo!OywK(HWmtfu& zmnNNOebM!WB|=HOL3m5_6|X;D1xyouOubcW$r!_+Qp_|-bf?32(d-W&dS7qpi(WbH z`Pn1W?xhDFz3h75?w{H}wV9I6F2-#gZbFS-5>|@Ed>+a?9MZ3N;(wg*XUWD&M z?lC~M)2O6}s^KXk*yl!Ma{ZQ+>o|2^Eq~>xv=eM>796B;`-`x^iPY4&a z)@*&~In}-^qth*HKZj_CT(R#yo{28rC5xx3e_WI=dSl6{?GbV2pURb@7U^fzCdkVv z<*s-dbMn66+6WGWryYViuW{LI5qr#ct!J&DANK=kp@}|=K3Al8uQT^(xVxfg>2ukA zN{qA4u}t#h2~E5z;g$0C@=d#~mcF5_jZ3D^cq6~K?n;d@P#zY5S=%^Q+*Die(#PSf z8KdcK7L&Q~FlM`&vpjFnZKD-w)}_LC#eCD54rgr>HourF8Mb-uj?TR9WyY|`U`Ss6 zCi3kkv9^P*=iL7J8I+xhlS}+?gLC(ni^->IrWIH2owQ%Mr}zEOQ=t=|X*ZmUIXhih z$m@COhkNb2E7zNSxtn+XwOwwLzV?Th2V2Z+?CTybGT#?0r}J3%jj3OVukME{P5<5G zUFLD$$>eLlqp)CeEsMnaPF2MK3+;6M=P%4ZPnfJ~uwmNOjM6LG-0Zm$&s~x@bA7&w uoa>L@C6iLMYl*yA)J6fU=>=WzA9Lr$W6SebXk-DCG=rzBpUXO@geCyfFi39z literal 0 HcmV?d00001 diff --git a/images/tabbar/practice_active.svg b/images/tabbar/practice_active.svg new file mode 100644 index 0000000..d2c5e0c --- /dev/null +++ b/images/tabbar/practice_active.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/images/tabbar/practice_active@2x.png b/images/tabbar/practice_active@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1d540d264d168b3bb08833055f33756d0306e954 GIT binary patch literal 1516 zcmcJP|2q>17{FIKw1~AhsafovJUFq6DWX`Wt0~nCJEzUpw9(vSmQ@_)+bNOSXie*o zBVRIK>f}ppg-GPe=G&U7r8a9~Y!pKt*L;SRpj894{SfL zC5ITM@8+a#w7i{~V{y$g%OLg90sWDIx2Fd1xpN6Ik)izs@A7QHB23YBRYy|0C=aCy zDh@)GQn1wy#zv;j&IbV2-fRCKycOPKtv3uk<5V9568D+lyQ%-hG>U$!Czf8hJ}M4e5WYR-mNvgEeB=VcLy1x499 zu#K5btE8!56y2NQMk6E;Rs?rIJC=B*eC<2N%9g%pvM`$b)TwUgqLa-t1cu$&%|fw` zYKzIS+0H_+h?F8`dvoXZRq$MV!N z_q$}x4&f4Av_Hp@gSUgb=PM^AA706ZE5DeAw-D(J`6TnIqj)w7=gZCCfEAXA3~qh& zK#E+5=5^;(e7)YYOuXb`z5cB%rYq1V`{bS&)R`NJOy&pNf zC)%eM>%~JGlVKB^EI}H%umy;rX?76i+-pkGH6`q%HbW`M`WMqxF&nw~7$~-94X*RR68fuDrXfQ8(T&G)@&82hk* zao50@x{w#uPjP&p+^qV16ZK{gr;X55TN$_2p%GiAGJ8!5r>m-#Q+T~^J#4-+{bypF zlDiWx6f*Rb!4xQ=f%imFYoW6dIqeyAZN{h1b*FZwiUTPRzA=;TGVQ^C+<7Gz+ucl} zx)|>!J~DZ8fH42LMv)-1oVX-*lA!&1YTwBi)H7T}VI(bx3KmMBUPo|m;FoP3gjSm` zeXHnRdREV3hECtgD;L%kAhX-gx_$mt@&rRU(M;o_5aye>xy9-hcX~DCWwdB-jwz~T zsK9F%${0IZtRT2W5#NH?{Z-rWL}L_JxZ&zvjy%6_aEh%x+^?am9X^0i1-KK#Btag248IJ`3Qi8AQmOr4Op1w*9kG5&)^b2IDG-Uq~K z!aU~=CM~}zDhfJujkK)}vS8i*WwFfOPqz%K$y&wZe;H{_R94BUZw`jhbzdBC_>e!c I*86nYUzwDiZvX%Q literal 0 HcmV?d00001 diff --git a/images/tabbar/profile.png b/images/tabbar/profile.png new file mode 100644 index 0000000000000000000000000000000000000000..732123f89cb14f539cc03b1cd0ca702df5d5565b GIT binary patch literal 1247 zcmV<51R(o~P)ApT5XB!~f&z)q(XrGNKu5bF8q5w%m0g{LUBoPHjA_|a16d;Kxzz{{uw9RUp(U#YJyV|zu#|>=?1L72h z!$WNg+HQ7uF9luE_7VeN2qIFnRdf^2g}mU~?XtEDEw5kE zHm~gmA_^JP6AKs*bm0(PhY>|t{TYn6&cO_Q7kXrg2*eUG*Y?rr`vxOyg~Q2V1iR|= z-|hH$QDTV5sJmT1HqiSRRCduFexAX__DX*K8jkio-ke5wc;mK!n=l@D)EnQeKeuW1 zHNplZv?pdTUf96E!Ff*M_+&8i=OwG}8Up5$p*?ZVX>$lG_@T5d9r8Q96512LV3ZHi z3uhn)hj-(R^vqibL}*X66Kd(aZfnBt^iD_-GpAiamda^MdijCG6tPcFB*DBfmBIuF z>50l|OM8m@eZkh!Aw5w!?Hsb?PCMADC3JNDYCXRJS#C`Do!$v8Vre~R^jv@p3mA{a zB?1zA*~cNrq*&qjWH3p*E#iq@_9={)^m*f}@M9JGD?$nZiB$?y7%v!<+MeL(rLbj~ zN=^fpk{SMkQW$U0Ef5qiLo!D<^7{RS(?5s60+LvJEDENQZHr0IDll5v{K2>&?7yRJ z5&LVBgxdq_h(PS!z5+&&OA}uItL>Mzi+{k{VWli_DhAH{nPbNu5y{PQ|~zS8yrCN1JJ(mrxRZ>9HsKu`4LiM(hI-`wfv zvA6>LCd3(xhtE3w70mD!|Lwj!@g4RH_*C0tZNJek6zL6&gXC6!rtLO*h~9mO$obD+ ziE#KnfEjKfsbK1g_b?jXS#28-M0z+E7!M*8Fh|_;gUk#nj0YXjU?V9ay>HFw;~W%a zJ?qeRZDJ?LBt>MBn^|p^wpK74m;xg zdl?@1dSbJrYFNT_uw*^w_0PcrUlDVsJ%n_o3+{F1@W59@dwt5)!BFQy7hb1*N@5ls z_=w0gri;>gp26dQmCu2u8K)g{X?Gml>+Mo$Z^g$GbEjQlx~QCXj$@Jzc?#g; ziMIRiVLGv|JIZkG^Pd2r5c$V}&v3TW8HMRY + + + diff --git a/images/tabbar/profile@2x.png b/images/tabbar/profile@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1a1e79ee2ae8fc05ebc2558fe677b01dc496f4b5 GIT binary patch literal 2419 zcmchZ`9Bkm1I9(l5u5v(*kmb3E#(MhGc-y`G@Hno+_{OAbC~;#N#QeuF~`i2qm5CB zF2{0gjjd0PN+Oa^{Sn{q56| zg7CpitELvUQ1resWv@n5j8>!}YPD;aFtR+4X}Jk=EQ(nsWc@TSJO~B_i6ab55z;!@ zz$DQlNEW}aE%pC!>ATdoyN;n5#y-sT)wMX&5d;EMZ3>)wM;c|_o75ON;FjI4YV$FH zawCvQ@|O#e_lDI`lt0pg)5L|pUJ7Vk5rX5bzjW{Hb^-U+#gPDQ)mdH}_Qfd$JF^KDX!SCJ$>sLeP~7Kjm2CdCLSaXQc% zArvsdrW&ofXaV*BP!q0eFQ^#SKDy_7~#-KVU30K1GI#>i)bB)?Ntg+a2fwy=(oYC z?MQ7wNp(+fp@m9=t6x39`pv_;-5pE>R8T2{)nH@gM#1i!a%*;j787SYtQ zt(cm-?XWMiL(Kk~_d;MPv2CTGODvCI53UV-^c3I2|FLv+GLd^#epf~knzB+Q_ABs> z7EmubYW_#3d@YuKjCs2H+Cgk?PiS+PxaEazc{?3-OU0=+Va-JThp@*&4Pd1#rZd1s z$Gq74Qhmy7;5(~ORqGfse z5MC0Fc9;2mIiawiFatIEW_2Fzoi{&SX*7H5_^Nq?&G4YNs!u%Ml*@dHOiD0I5x z6KB{S7g`s4i0hfN97~`-p7wZYVt6kq%}m2+i|E4y z3=P?HA@pYddH1`5c%S|7_ZJ9nlhAIZ{4r&UsiLPASE}0YM9O_AO8=M*(7ho(8S{3R zd0vQSIMTN>U23+LQ45rXACWU7Y9KLvr3Oqz+V7naKe6Xib3K4^?!<{24cOk!Xycnk za(8;-1r$z@s122mpY};o?fFMre83TcUXFb!vA(z_K2Te_=jfT&Mq{>38vy8 zxVKtwiVNXtnPa1s7c2biae_+&2_xYVQ_jZrs@(>5Eo-BL8CYq^fPNTPxy1!j*uc1g zSNU|W+Jo7OYsHsh23pR5$0Wc~Im)jULO!0kUR*ftXM@LLo|xDtf-WvS4)n2J*(t-d zLqT1ae}M`b(DYhJmF5y{CLcQ^Fb7ChawkRW7Tys>RBhEaj&FLjeHMEYp{^%19~B)lxW^n4VUOiX$qMTn{!&1E*e35f3^#^ zrct9t>ZrjJb6V`iCV>EwRgVB{aNd8X$-i8QaWYEbCFMFT&)g$9hRM6!`Sdqpob5QA zUV7H(QKEP3%meDk4}u-!kQZR0Z}{lBZjVu;DM*!dp%B-OHs@jAha&GgAv@8WJRCRU z>RPO>FHedd?BO{~B$$8brzGQ*)t~ROp0#+@h!`}b9oDL_f1#g#*{FWMprMD1!Vf1T z(hW-&ZPI0lu&FU}4Y zdp$Bo#Ss_5nH>ot?D14FD3aP zr(}}WIavo_{UtCRSg`@s1NyT5HKgMt)&ReZji6d;NyKon7cD53ySL;<3R0z?r7h$0FQMHC>4D8Sr_xVdMSdwp6ZLrt>O za>RhL z&3WTFDWU3#{Q*7j&r$KmMFdRpyuN>bQaNR#Dk41Q;qaKf&>DJ10$|iW^ur*(91XPl z#8Es50bi0JL4J7x_uQz7$o*lEbV{9LS&jm#BGzm4yMK<@Zi8Rjr~t`pAM%-0PT8o6==#N*DSdd)z+y_P0LlHH z7jpjEYcT4wIG7yZb(AYFX$8&kNiwxH1AKk$`T?wX$yY9aRn$dHMooKbBH~Lkz?`B@ z2{Im@l*RRYUy%MldoGcgyX65G^o*AW@cmb~JYabQ#`3@|@#sBV9vI?;$A8;AT|%4sK2#xa1R28!S7JKj$}-icxVr0WrztOI}mbU6`RGjHK-{ z17h|}toC*A@K%GLxT{zzqwG>boGbz1eLuk)N`Ir^u7S(p@`9gQ&MtApJ@L`5J-`6c z_LBHLx&3t8sLre~8@rYVo-TRK8x`*EZ<}=zL{!9#L4+p+T2<~Jbo(Dq$QM$0un`l{ zJ@v4;tX1KGA(^%b?jRHE6Y(_mQs;)Y=0QM)?O=QSG@Kuh5F#R?8E*!G^dXvy`tGbr zID1BKF>Ei}O#&1ak;57EgFkvnl}wCILpUMKc1!G(C@f+U;QA8@RyK&1#7zkMEu`t} zFGTU?Z0Kw6>;j$j4$U#nzKc;p+WuS=648x%BQHc}y+aE?U}w|SBR54ovE`p=FIDCx z!_F2Jppb|NYy9`q7xUT|hB!M%yCi~o%uH?(`AxtNYqWPA6velf>%sM*oa7b}qgpC7 zu!CyXIc6oaH>}Sr6lu;a|D=llgj0kJds~!~xXDw1{GMo1?tev#$&KBniso#+J<)ct z`oj^v-InwR7E_~}aK;2%i5CQ^I^^yXd94I%8C`cgmlF=}LaGjP^XCf|nK(KN5JeOq ziYP!7QGh6-08vB%qKE=S5e0}M3J^sUAc`nJ6j6Y;wXx#=hiTnA00000NkvXXu0mjf D@y@9o literal 0 HcmV?d00001 diff --git a/images/tabbar/profile_active.svg b/images/tabbar/profile_active.svg new file mode 100644 index 0000000..8aa213c --- /dev/null +++ b/images/tabbar/profile_active.svg @@ -0,0 +1,4 @@ + + + + diff --git a/images/tabbar/profile_active@2x.png b/images/tabbar/profile_active@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d52211ce41214e6191540790caf35e798c85db67 GIT binary patch literal 2847 zcmchZ`8O1d9>$TF5t$i`M3}K_G%u5Vsm5e!GBfslZ|wU%RH#NoSt7y|%9iXyuRX@t zi^&qNHN1u*B^hgO@4e^Tzu=zx!}C1n^V@UI_lNJ3M8FyH@toyhV`JkpHo}zG4dl%DY$da0{|WYABD*HqPg5)DK&K)I~*Vj|Mh( zIFEakjMcx<)tztu%G#e-VK0aya$IHy!3pg502XX84x!75mrwp5@Y!9_j#}$wKM$%3 ziVFXzt3Ry7XmEqNc9{qvAosarDTw zZ;5k@gW-Bs-pV>m_tsX-G-BWuhci1`hwd^uw>+AbiXG#HcYX4r>A=681Qji1=j|v3 zEb;Bx6has7Jl|42+(?*`WrH>Q?DF57D%DsI3csbx`rK?NLW4UH!Fq*JaKWl;;?l@W zSG5h+53H$~TNwPfd+7fP5c_q1;vA}Es1KL;9->zsRu35Y zkf=0k7{)$n=d{$JT81#fRx>?B>LH6sYOXa}E9kw(4L-!{-y)um0_tHoq|akSY)uM-_*gkZ(7edcc!xt^CVy>w7LU}js0{Rv9)jHfn-dT8m_p_kRh^X9spz7c!ryRp)po}V?0 zc}%<13d1TU;Xd!}A4@BA(`a#z-30Av?i?gULahuycJhRfv^UrUIa$mCDj1eX?h1(J zt(?u0CVP3#LKna5x(C^TknVvW;3CHRrHO@EyrFd;}nR-5}D_!vkd5gF27CZuy;Uou|@<@u~*1qMgWFY;ryAax0ap2qPn$$genIe6~& zu+w7v4;sAS_1#w@`mqa7PW$@$r2Dws zHPQZil<2G_++~Q;-z0EOUfGMo7Y2Qp^P;^bEZ(TyF&R-bV>q?;_)}SB#FIf1?fQvzLBfBs|5pieni41r|$;0{=> z$}BbqjpOB5gZ1$zN@XJVDk!SZc}YRJLmrzUUH;Djfu?{D{(+rEhvr$sHM_T?TidP@ z*&&T@`c8X_5J8<4;g<%(89>2zWK zOY)>E`!iY66*LZCwpGRIOai&H_JgdBm+c=)P}d6?u|N^B7; zjU5l~M)zGRiE#|jj<8BpRpU5@?C3PLsZTgjG_PFKW_dhJ4Uc~D9mi7}5tG*D2wjN& z_?J07R6N8vDtxo0exv6-kilmG!rBz;%Fk_9R=p{75Z#4Y{%B-Ti0fnW&v3xnM*uoF zy>O?zdnVr;Rkb=2hD)J<-T1)beM4_5wO3#=iGJ|+ZZ`>^CU8l$0_fMUE?xC1|z-A^5qrFwpp zMZECIGaayUk1?fyibtK#PR84xcKCM*fh8?6RF0yJ)Hh3brB;-jdepGo*dUmotD5u$ z!aJcj_#^F6?rxD{@~Tb?C{?m6#6jewVNj^8^%}*SPZe5wC%bj<52xzZckO0BZidj`Mgh^UZmMy^{sutAbZjLX>DS{l@1rHc#mqY+WQ7v zm%<1YeF@l#=~CO6ePTy9D@!F=uJjTxMLg;5wqN3xPkhDVLNdd79c-g3i}F;@JS;;m zOL}&L@5~&_(D^(Lxnj*O;&iD|w}6|=PmTZ^cNcwq&cTSzFPamu2cYCEha<59 zyMWWyjZT|Q%d-~^5XqA)nZCsw&Rn|L>ixM=00vyNcG7dj1aR?PcQ#=w;H=aG(8kau z{L@i_lKOCWHYi%%1XAjAT3gYa;~@mGWON$)MT$E6neX^}ajM$&o$0!UPM1+T=d;}H z9gK)-t1nPft4V%o?o#v}@}kn0jL0!SbID^n^n8fD{CVGT-}$(kUbAp6+6gZ4P+!gg zEdxMlIP5o; literal 0 HcmV?d00001 diff --git a/miniprogram_npm/@jdmini/api/index.d.ts b/miniprogram_npm/@jdmini/api/index.d.ts new file mode 100644 index 0000000..e06e58d --- /dev/null +++ b/miniprogram_npm/@jdmini/api/index.d.ts @@ -0,0 +1,295 @@ +// Generated by dts-bundle v0.7.3 + +declare module '@jdmini/api' { + import { onLoginReady, waitLogin } from '@jdmini/api/app'; + import HttpClient, { gatewayHttpClient, baseHttpClient, apiHttpClient } from '@jdmini/api/httpClient'; + import { injectApp, injectPage, injectComponent, hijackApp, hijackAllPage } from '@jdmini/api/injector'; + import adManager from '@jdmini/api/adManager'; + export { onLoginReady, waitLogin, injectApp, injectPage, injectComponent, hijackApp, hijackAllPage, gatewayHttpClient, baseHttpClient, apiHttpClient, HttpClient, adManager, }; +} + +declare module '@jdmini/api/app' { + export interface AppOptions { + gatewayUrl?: string; + baseUrl?: string; + apiUrl?: string; + } + export interface PageOptions { + showInterstitialAd?: boolean; + } + export function initApp(options?: AppOptions): Promise; + export function showPage(options: PageOptions | undefined, pageId: string): Promise; + export const checkTokenValid: () => boolean; + /** + * 确保登录完成 + * @param {Function} callback - 回调函数 + * @returns {void} + */ + export function onLoginReady(callback: (...args: any[]) => void): void; + /** + * 等待登录完成 + * @returns {Promise} + */ + export function waitLogin(): Promise; + export function login(): Promise; + export function fetchEchoData(): Promise; + export function trackVisit(): Promise; +} + +declare module '@jdmini/api/httpClient' { + import { HttpClientOptions, RequestOptions, ApiResponse } from '@jdmini/api/types'; + class HttpClient { + constructor({ baseURL, timeout }: HttpClientOptions); + setBaseURL(baseURL: string): void; + /** + * 请求 + * @param {string} path 路径 + * @param {string} method 方法, 默认GET + * @param {Object} data 数据, 默认{} + * @param {Object} options 传入wx.request的其他配置, 默认{} + * @returns {Promise} 返回一个Promise对象 + */ + request(path: string, method?: WechatMiniprogram.RequestOption['method'], data?: Record, options?: RequestOptions): Promise>; + /** + * 上传文件 + * @param {string} filePath 文件路径 + * @param {Object} data 数据, 默认{} + * @param {'avatar' | 'file'} type 类型, 默认'file' + * @returns {Promise} 返回一个Promise对象 + */ + uploadFile(filePath: string, data?: Record, type?: 'avatar' | 'file'): Promise>; + /** + * 上传文件 + * @param {string} filePath 文件路径 + * @param {Object} data 数据, 默认{} + * @param {'avatar' | 'file'} type 类型, 默认'file' + * @returns {Promise} 返回一个Promise对象 + */ + upload(path: string, filePath: string, data?: Record): Promise>; + /** + * 删除文件 + * @param {number} fileId 文件id + * @returns {Promise} 返回一个Promise对象 + */ + deleteFile(fileId: number): Promise>; + /** + * 上传头像 + * @param {string} filePath 文件路径 + * @returns {Promise} 返回一个Promise对象 + */ + uploadAvatar(filePath: string): Promise>; + } + export const gatewayHttpClient: HttpClient; + export const baseHttpClient: HttpClient; + export const apiHttpClient: HttpClient; + export default HttpClient; +} + +declare module '@jdmini/api/injector' { + interface AppConfig { + onLaunch?: (...args: any[]) => void | Promise; + [key: string]: any; + } + interface PageConfig { + onShow?: (...args: any[]) => void | Promise; + [key: string]: any; + } + interface ComponentConfig { + methods?: { + onLoad?: (...args: any[]) => void | Promise; + onShow?: (...args: any[]) => void | Promise; + [key: string]: any; + }; + [key: string]: any; + } + interface InjectAppOptions { + gatewayUrl?: string; + baseUrl?: string; + apiUrl?: string; + } + interface InjectPageOptions { + showInterstitialAd?: boolean; + } + /** + * 注入应用配置 + * @param {Object} options - 配置选项 + * @param {string} [options.gatewayUrl] - 网关地址,默认使用CONFIG.API.GATEWAY_URL + * @param {string} [options.baseUrl] - 基础地址,默认使用CONFIG.API.BASE_URL + * @param {string} [options.apiUrl] - api地址,默认使用CONFIG.API.API_URL + * @returns {Function} 返回一个接收应用配置的函数 + */ + export function injectApp(options?: InjectAppOptions): (appConfig: AppConfig) => AppConfig; + /** + * 注入页面配置 + * @param {InjectPageOptions} options - 配置选项 + * @param {boolean} [options.showInterstitialAd] - 是否在onShow显示插屏广告,默认不显示 + * @returns {Function} 返回一个接收页面配置的函数 + */ + export function injectPage(options?: InjectPageOptions): (pageConfig?: PageConfig) => PageConfig; + /** + * 注入组件配置 + * @param {InjectPageOptions} options - 配置选项 + * @param {boolean} [options.showInterstitialAd] - 是否在onShow显示插屏广告,默认不显示 + * @returns {Function} 返回一个接收组件配置的函数 + */ + export function injectComponent(options?: InjectPageOptions): (pageConfig?: PageConfig) => ComponentConfig; + /** + * 劫持App + * @param {InjectAppOptions} options - 配置选项 + * @param {string} [options.gatewayUrl] - 网关地址,默认使用CONFIG.API.GATEWAY_URL + * @param {string} [options.baseUrl] - 基础地址,默认使用CONFIG.API.BASE_URL + * @param {string} [options.apiUrl] - api地址,默认使用CONFIG.API.API_URL + * @returns {void} + */ + export const hijackApp: (options?: InjectAppOptions) => void; + /** + * 劫持所有Page + * @param {InjectPageOptions} options - 配置选项 + * @param {boolean} [options.showInterstitialAd] - 是否在onShow显示插屏广告,默认不显示 + * @returns {void} + */ + export const hijackAllPage: (options?: InjectPageOptions) => void; + export {}; +} + +declare module '@jdmini/api/adManager' { + import { AdData, LinkData, TopData } from '@jdmini/api/types'; + type Ads = Partial>; + class AdManager { + /** + * 广告数据 + */ + ads: Ads; + /** + * 友情链接数据 + */ + link: LinkData[]; + /** + * 友情链接顶部广告数据 + */ + top: TopData | null; + constructor(); + /** + * 确保广告数据就绪 + * @param {Function} callback - 回调函数 + * @returns {void} + */ + onDataReady: (callback: (...args: any[]) => void) => void; + /** + * 等待广告数据加载完成 + * @returns {Promise} + */ + waitAdData: () => Promise; + /** + * 初始化广告数据 + * @returns {void} + */ + init: () => void; + /** + * 创建并展示插屏广告 + * @returns {Promise} + */ + createAndShowInterstitialAd: () => Promise; + /** + * 创建并展示激励视频广告 + * @param {any} context - 页面上下文 + * @param {string} [pageId] - 页面ID + * @returns {Promise} 是否完成播放 + */ + createAndShowRewardedVideoAd: (context: any, pageId?: string) => Promise; + } + const _default: AdManager; + export default _default; +} + +declare module '@jdmini/api/types' { + export interface Config { + API: { + GATEWAY_URL: string; + BASE_URL: string; + API_URL: string; + }; + APP: { + APP_ID: number; + LOGIN_MAX_RETRY: number; + }; + HTTP: { + TIMEOUT: number; + }; + DATA: { + PAGE_ID: string; + }; + STORAGE_KEYS: { + TOKEN: string; + USER_INFO: string; + SPA_DATA: string; + LINK_DATA: string; + TOP_DATA: string; + }; + EVENT_KEYS: { + LOGIN_SUCCESS: string; + AD_DATA_READY: string; + REWARDED_VIDEO_AD_CLOSE: string; + }; + } + export interface HttpClientOptions { + baseURL: string; + timeout?: number; + } + export interface RequestOptions { + headers?: Record; + [key: string]: any; + } + export interface LoginData { + appId: number; + code: string; + brand: string; + model: string; + platform: string; + } + export interface ApiResponse { + code: number; + message?: string; + data?: T; + } + export interface UserInfo { + token: string; + user: { + id: number; + name: string; + avatar: string; + openId: string; + }; + } + export interface EchoData { + isPublished: boolean; + spads: AdData[]; + links: LinkData[]; + top: TopData; + version: number; + } + export interface AdData { + id: number; + status: number; + appPage: 'banner' | 'custom' | 'video' | 'interstitial' | 'rewarded'; + ads: { + id: number; + type: number; + adId: number; + adUnitId: string; + }[]; + } + export interface LinkData { + appId: string; + appLogo: string; + linkName: string; + linkPage: string; + } + export interface TopData { + appId: string; + appLogo: string; + linkName: string; + appDsc: string; + } +} + diff --git a/miniprogram_npm/@jdmini/api/index.js b/miniprogram_npm/@jdmini/api/index.js new file mode 100644 index 0000000..fc11843 --- /dev/null +++ b/miniprogram_npm/@jdmini/api/index.js @@ -0,0 +1,4 @@ +/*! + * @jdmini/api v1.0.10 + * + */(()=>{"use strict";var t={616:function(t,e,n){var r=this&&this.__awaiter||function(t,e,n,r){return new(n||(n=Promise))((function(o,a){function i(t){try{u(r.next(t))}catch(t){a(t)}}function s(t){try{u(r.throw(t))}catch(t){a(t)}}function u(t){var e;t.done?o(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(i,s)}u((r=r.apply(t,e||[])).next())}))},o=this&&this.__generator||function(t,e){var n,r,o,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]},i=Object.create(("function"==typeof Iterator?Iterator:Object).prototype);return i.next=s(0),i.throw=s(1),i.return=s(2),"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(s){return function(u){return function(s){if(n)throw new TypeError("Generator is already executing.");for(;i&&(i=0,s[0]&&(a=0)),a;)try{if(n=1,r&&(o=2&s[0]?r.return:s[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,s[1])).done)return o;switch(r=0,o&&(s=[2&s[0],o.value]),s[0]){case 0:case 1:o=s;break;case 4:return a.label++,{value:s[1],done:!1};case 5:a.label++,r=s[1],s=[0];continue;case 7:s=a.ops.pop(),a.trys.pop();continue;default:if(!(o=a.trys,(o=o.length>0&&o[o.length-1])||6!==s[0]&&2!==s[0])){a=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0?e.ads[0].adUnitId:"",t}),{}));var n=wx.getStorageSync(i.default.STORAGE_KEYS.LINK_DATA);n&&(t.link=n);var r=wx.getStorageSync(i.default.STORAGE_KEYS.TOP_DATA);r&&r.appDsc.length>40&&(r.appDsc=r.appDsc.substring(0,40)+"...",t.top=r),t.adDataReady=!0,s.default.emit(i.default.EVENT_KEYS.AD_DATA_READY)},this.createAndShowInterstitialAd=function(){return r(t,void 0,void 0,(function(){var t;return o(this,(function(e){switch(e.label){case 0:return this.interstitialAd&&this.interstitialAd.destroy(),[4,this.waitAdData()];case 1:e.sent(),e.label=2;case 2:return e.trys.push([2,5,,6]),[4,this.createInterstitialAd()];case 3:return e.sent(),[4,this.showInterstitialAd()];case 4:return e.sent(),[3,6];case 5:return t=e.sent(),console.error("创建插屏广告失败:",t),[3,6];case 6:return[2]}}))}))},this.createInterstitialAd=function(){return r(t,void 0,void 0,(function(){var t=this;return o(this,(function(e){return[2,new Promise((function(e,n){t.ads.interstitial?(t.interstitialAd=wx.createInterstitialAd({adUnitId:t.ads.interstitial}),t.interstitialAd.onLoad((function(){console.log("插屏广告加载成功"),e()})),t.interstitialAd.onError((function(t){console.error(t),n(new Error("插屏广告加载失败"))})),t.interstitialAd.onClose((function(){console.log("插屏广告关闭")}))):n(new Error("插屏广告未配置"))}))]}))}))},this.showInterstitialAd=function(){return r(t,void 0,void 0,(function(){var t,e;return o(this,(function(n){switch(n.label){case 0:return n.trys.push([0,2,,3]),console.log("开始展示插屏广告"),[4,null===(e=this.interstitialAd)||void 0===e?void 0:e.show()];case 1:return[2,n.sent()];case 2:return t=n.sent(),console.error("插屏广告展示失败:",t),[3,3];case 3:return[2]}}))}))},this.createAndShowRewardedVideoAd=function(e,n){return r(t,void 0,void 0,(function(){var t,r,a;return o(this,(function(o){switch(o.label){case 0:return[4,this.waitAdData()];case 1:o.sent(),t=n||(null===(a=null==e?void 0:e.data)||void 0===a?void 0:a[i.default.DATA.PAGE_ID]),o.label=2;case 2:if(o.trys.push([2,6,,7]),!t)throw new Error("未指定pageId或者context");return this.rewardedVideoAds[t]?[3,4]:[4,this.createRewardedVideoAd(t)];case 3:o.sent(),o.label=4;case 4:return[4,this.showRewardedVideoAd(t)];case 5:return o.sent(),[3,7];case 6:return r=o.sent(),console.error("创建激励视频广告失败:",r),[3,7];case 7:return[2,new Promise((function(e){s.default.on(i.default.EVENT_KEYS.REWARDED_VIDEO_AD_CLOSE,(function(n,r){n===t&&e(r)}))}))]}}))}))},this.createRewardedVideoAd=function(e){return r(t,void 0,void 0,(function(){var t=this;return o(this,(function(n){return[2,new Promise((function(n,r){t.ads.rewarded?(t.rewardedVideoAds[e]=wx.createRewardedVideoAd({adUnitId:t.ads.rewarded}),t.rewardedVideoAds[e].onLoad((function(){console.log("激励视频广告加载成功"),n()})),t.rewardedVideoAds[e].onError((function(t){console.error(t),r(new Error("激励视频广告加载失败"))})),t.rewardedVideoAds[e].onClose((function(t){s.default.emit(i.default.EVENT_KEYS.REWARDED_VIDEO_AD_CLOSE,e,t.isEnded),console.log("激励视频广告关闭")}))):r(new Error("激励视频广告未配置"))}))]}))}))},this.showRewardedVideoAd=function(e){return r(t,void 0,void 0,(function(){var t,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),console.log("开始展示激励视频广告"),[4,null===(n=this.rewardedVideoAds[e])||void 0===n?void 0:n.show()];case 1:return[2,r.sent()];case 2:return t=r.sent(),console.error("激励视频广告展示失败:",t),[3,3];case 3:return[2]}}))}))}};e.default=new u},859:function(t,e,n){var r=this&&this.__awaiter||function(t,e,n,r){return new(n||(n=Promise))((function(o,a){function i(t){try{u(r.next(t))}catch(t){a(t)}}function s(t){try{u(r.throw(t))}catch(t){a(t)}}function u(t){var e;t.done?o(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(i,s)}u((r=r.apply(t,e||[])).next())}))},o=this&&this.__generator||function(t,e){var n,r,o,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]},i=Object.create(("function"==typeof Iterator?Iterator:Object).prototype);return i.next=s(0),i.throw=s(1),i.return=s(2),"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(s){return function(u){return function(s){if(n)throw new TypeError("Generator is already executing.");for(;i&&(i=0,s[0]&&(a=0)),a;)try{if(n=1,r&&(o=2&s[0]?r.return:s[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,s[1])).done)return o;switch(r=0,o&&(s=[2&s[0],o.value]),s[0]){case 0:case 1:o=s;break;case 4:return a.label++,{value:s[1],done:!1};case 5:a.label++,r=s[1],s=[0];continue;case 7:s=a.ops.pop(),a.trys.pop();continue;default:if(!(o=a.trys,(o=o.length>0&&o[o.length-1])||6!==s[0]&&2!==s[0])){a=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]s.default.APP.LOGIN_MAX_RETRY)throw wx.showToast({title:"网络异常,无法初始化",icon:"none",duration:2e3}),new Error("网络异常,无法初始化");o.label=1;case 1:return o.trys.push([1,7,,9]),t=wx.getDeviceInfo(),[4,wx.login()];case 2:return e=o.sent().code,n={appId:s.default.APP.APP_ID,code:e,brand:t.brand,model:t.model,platform:t.platform},[4,i.gatewayHttpClient.request("/wx/v1/api/login","POST",n)];case 3:return 200===(r=o.sent()).code&&r.data?(wx.setStorageSync(s.default.STORAGE_KEYS.TOKEN,r.data.token),wx.setStorageSync(s.default.STORAGE_KEYS.USER_INFO,r.data.user),l=0,u.default.emit(s.default.EVENT_KEYS.LOGIN_SUCCESS),[3,6]):[3,4];case 4:return l++,[4,h()];case 5:o.sent(),o.label=6;case 6:return[3,9];case 7:return o.sent(),l++,[4,h()];case 8:return o.sent(),[3,9];case 9:return[2]}}))}))}function p(){return r(this,void 0,void 0,(function(){var t;return o(this,(function(n){switch(n.label){case 0:return wx.removeStorageSync(s.default.STORAGE_KEYS.SPA_DATA),wx.removeStorageSync(s.default.STORAGE_KEYS.LINK_DATA),wx.removeStorageSync(s.default.STORAGE_KEYS.TOP_DATA),(0,e.checkTokenValid)()?[4,i.gatewayHttpClient.request("/wx/v1/api/echo","GET")]:[2];case 1:return 200===(t=n.sent()).code&&t.data?(t.data.spads&&wx.setStorageSync(s.default.STORAGE_KEYS.SPA_DATA,t.data.spads),t.data.links&&wx.setStorageSync(s.default.STORAGE_KEYS.LINK_DATA,t.data.links),t.data.top&&wx.setStorageSync(s.default.STORAGE_KEYS.TOP_DATA,t.data.top),[3,5]):[3,2];case 2:return 401!==t.code?[3,5]:[4,h()];case 3:return n.sent(),[4,p()];case 4:n.sent(),n.label=5;case 5:return[2]}}))}))}function w(){return r(this,void 0,void 0,(function(){var t;return o(this,(function(e){switch(e.label){case 0:return e.trys.push([0,5,,6]),[4,i.gatewayHttpClient.request("/wx/v1/api/visit","POST")];case 1:return 401!==e.sent().code?[3,4]:[4,h()];case 2:return e.sent(),[4,w()];case 3:e.sent(),e.label=4;case 4:return[3,6];case 5:return t=e.sent(),console.error("访问统计失败:",t),[3,6];case 6:return[2]}}))}))}e.checkTokenValid=function(){var t=wx.getStorageSync(s.default.STORAGE_KEYS.TOKEN);return!(!t||t.length<32)}},28:(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0});var n={API:{GATEWAY_URL:"https://ca.miniappapi.com",BASE_URL:"https://app.jd027.com/v1/api",API_URL:"https://cp.miniappapi.com"},APP:{APP_ID:wx.getExtConfigSync().appId||313,LOGIN_MAX_RETRY:2},HTTP:{TIMEOUT:5e3},DATA:{PAGE_ID:"jdwx-page-id"},STORAGE_KEYS:{TOKEN:"jdwx-token",USER_INFO:"jdwx-userinfo",SPA_DATA:"jdwx-spadata",LINK_DATA:"jdwx-linkdata",TOP_DATA:"jdwx-topdata"},EVENT_KEYS:{LOGIN_SUCCESS:"jdwx-login-success",AD_DATA_READY:"jdwx-ad-data-ready",REWARDED_VIDEO_AD_CLOSE:"jdwx-rewarded-video-ad-close"}};e.default=n},144:(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0});var n=function(){function t(){this.events={}}return t.prototype.on=function(t,e){if("string"!=typeof t)throw new TypeError("eventName must be a string");if("function"!=typeof e)throw new TypeError("callback must be a function");return this.events[t]||(this.events[t]=[]),this.events[t].push(e),this},t.prototype.emit=function(t){for(var e=[],n=1;n0&&o[o.length-1])||6!==s[0]&&2!==s[0])){a=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]=200&&d.statusCode<300)return[2,d.data];if(401===d.statusCode)return[2,{code:401,message:"未授权"}];throw new Error(d.data.message||"请求失败");case 4:throw f=a.sent(),console.error("网络错误:",f),wx.showToast({title:f instanceof Error?f.message:"网络错误",icon:"none",duration:2e3}),f;case 5:return[2]}}))}))},t.prototype.uploadFile=function(t){return o(this,arguments,void 0,(function(t,e,n){var r,o,i,u;return void 0===e&&(e={}),void 0===n&&(n="file"),a(this,(function(a){r=this.baseURL===s.default.API.GATEWAY_URL,o=wx.getStorageSync(s.default.STORAGE_KEYS.TOKEN),i="avatar"===n?"/avatar":"/file/new",u=this.joinURL(this.baseURL,"".concat(r?"/wx/v1/api":"").concat(i));try{return[2,new Promise((function(n,r){wx.uploadFile({url:u,name:"file",filePath:t,formData:e,header:{"Content-Type":"application/x-www-form-urlencoded",Authorization:o},success:function(t){if(t.statusCode>=200&&t.statusCode<300)n(JSON.parse(t.data));else{if(401!==t.statusCode)throw new Error(t.data.message||"上传失败");n(JSON.parse(t.data))}},fail:function(){throw new Error("网络错误")}}).onProgressUpdate((function(t){console.log("上传进度",t.progress),console.log("已经上传的数据长度",t.totalBytesSent),console.log("预期需要上传的数据总长度",t.totalBytesExpectedToSend)}))}))]}catch(t){throw console.error("上传失败:",t),wx.showToast({title:t instanceof Error?t.message:"上传失败",icon:"none",duration:2e3}),t}return[2]}))}))},t.prototype.upload=function(t,e){return o(this,arguments,void 0,(function(t,e,n){var r,o,i;return void 0===n&&(n={}),a(this,(function(a){r=this.baseURL===s.default.API.GATEWAY_URL,o=wx.getStorageSync(s.default.STORAGE_KEYS.TOKEN),i=this.joinURL(this.baseURL,"".concat(r?"/wx/v1/api":"").concat(t));try{return[2,new Promise((function(t,r){wx.uploadFile({url:i,name:"file",filePath:e,formData:n,header:{"Content-Type":"application/x-www-form-urlencoded",Authorization:o},success:function(e){if(e.statusCode>=200&&e.statusCode<300)t(JSON.parse(e.data));else{if(401!==e.statusCode)throw new Error(e.data.message||"上传失败");t(JSON.parse(e.data))}},fail:function(){throw new Error("网络错误")}}).onProgressUpdate((function(t){console.log("上传进度",t.progress),console.log("已经上传的数据长度",t.totalBytesSent),console.log("预期需要上传的数据总长度",t.totalBytesExpectedToSend)}))}))]}catch(t){throw console.error("上传失败:",t),wx.showToast({title:t instanceof Error?t.message:"上传失败",icon:"none",duration:2e3}),t}return[2]}))}))},t.prototype.deleteFile=function(t){return o(this,void 0,void 0,(function(){var e;return a(this,(function(n){return e=this.baseURL===s.default.API.GATEWAY_URL,[2,this.request("".concat(e?"/wx/v1/api":"","/file/del"),"GET",{id:t})]}))}))},t.prototype.uploadAvatar=function(t){return o(this,void 0,void 0,(function(){return a(this,(function(e){return[2,this.uploadFile(t,{},"avatar")]}))}))},t}();e.gatewayHttpClient=new u({baseURL:s.default.API.GATEWAY_URL,timeout:s.default.HTTP.TIMEOUT}),e.baseHttpClient=new u({baseURL:s.default.API.BASE_URL,timeout:s.default.HTTP.TIMEOUT}),e.apiHttpClient=new u({baseURL:s.default.API.API_URL,timeout:s.default.HTTP.TIMEOUT}),e.default=u},156:function(t,e,n){var r=this&&this.__createBinding||(Object.create?function(t,e,n,r){void 0===r&&(r=n);var o=Object.getOwnPropertyDescriptor(e,n);o&&!("get"in o?!e.__esModule:o.writable||o.configurable)||(o={enumerable:!0,get:function(){return e[n]}}),Object.defineProperty(t,r,o)}:function(t,e,n,r){void 0===r&&(r=n),t[r]=e[n]}),o=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),a=this&&this.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)"default"!==n&&Object.prototype.hasOwnProperty.call(t,n)&&r(e,t,n);return o(e,t),e},i=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(e,"__esModule",{value:!0}),e.adManager=e.HttpClient=e.apiHttpClient=e.baseHttpClient=e.gatewayHttpClient=e.hijackAllPage=e.hijackApp=e.injectComponent=e.injectPage=e.injectApp=e.waitLogin=e.onLoginReady=void 0;var s=n(859);Object.defineProperty(e,"onLoginReady",{enumerable:!0,get:function(){return s.onLoginReady}}),Object.defineProperty(e,"waitLogin",{enumerable:!0,get:function(){return s.waitLogin}});var u=a(n(161));e.HttpClient=u.default,Object.defineProperty(e,"gatewayHttpClient",{enumerable:!0,get:function(){return u.gatewayHttpClient}}),Object.defineProperty(e,"baseHttpClient",{enumerable:!0,get:function(){return u.baseHttpClient}}),Object.defineProperty(e,"apiHttpClient",{enumerable:!0,get:function(){return u.apiHttpClient}});var c=n(718);Object.defineProperty(e,"injectApp",{enumerable:!0,get:function(){return c.injectApp}}),Object.defineProperty(e,"injectPage",{enumerable:!0,get:function(){return c.injectPage}}),Object.defineProperty(e,"injectComponent",{enumerable:!0,get:function(){return c.injectComponent}}),Object.defineProperty(e,"hijackApp",{enumerable:!0,get:function(){return c.hijackApp}}),Object.defineProperty(e,"hijackAllPage",{enumerable:!0,get:function(){return c.hijackAllPage}});var l=i(n(616));e.adManager=l.default},718:function(t,e,n){var r=this&&this.__assign||function(){return r=Object.assign||function(t){for(var e,n=1,r=arguments.length;ntYL|T)GRIfA+a?~DfhW-E^|pqGM7?rA;~3FgrXyt zB9zu>bam-iB$B2?OfD%karB($Ip_cLJfHXbKJWYg_f28YZ4~9zga`M&@4ARg(+bb1(t`477g<5>-x(1OV88Eoi}vDv}hr2Rr#;9<6k{o*CVZ(+Us# z&7+tQYiP7ns7?yHS7*#ZH>$7>@@w@&BYg!X7+bgjpXNCu2KtJXo5(fH)41> z&7wvzvVkY|+nh~jO(yXg{bIX=;o}RITkMwxa0dh4oTAr2x^oKC65flQ#oE$C%Fx%=He1A*c=hr?eOUL#AEhZT%N8lXd@9`N>%JEvlb#PTG42uC z@mhJJ`8~51aVFnW8I>JTk!FSUh6gbtTvfGckGQhZfG-_|hEuOUNJrn-lakxQzRzqN zEG`HLKcvn3=C6BO9ZYk#G&COl&>F%%RuaE!U&{uN0vU2JLA}#0KA@C%VChhF`-&q9 z>B^cdFPKnh>gH3w*GKcIx0VuJ>2dnYz!sj|$d8*0h}@HM7DZ< zom_lPRBTMw!qG{%*{K7xUg|i9XDej_jIDw2Cn z=lX45DekndIx{>ahvQjxF7Omlr>;$shDLH3R`}=v{vHRqI^Lj8ZM8yiPl2vQ!P3Kk z0TherS~QvuvehIF!vj41Rb(C-ltgS5H7;{l4FMsgp{&}*6>RMvP)1e1Hz~Ba%XpiT zO>X+dbt7Btw58YV%Qt;K$uS?wNMc5qiGHNJ(o3aXUiZ8FlJd~r+#7%LY8?2)^FQOa z)eDhHCO&K855g?ZzE5x6;evuki~EtNA=Q}v>U$QPuy zLzNefra8UqbMhOjl&=h%)D!h@hLc?Am*?0oQLr6im7;LWv~2#T;pu$PE_^DK4XFbEV;K$#8+LHdIXAB^KVWM`2dgc;PW}x9 CY1HNb literal 0 HcmV?d00001 diff --git a/miniprogram_npm/@jdmini/components/icons/home.png b/miniprogram_npm/@jdmini/components/icons/home.png new file mode 100644 index 0000000000000000000000000000000000000000..11016daa08d27f848fe96b9b380b9c335fb72a3e GIT binary patch literal 1070 zcmeAS@N?(olHy`uVBq!ia0vp^fgsGm1|(PYdzk?#&H|6fVg?31We{epSZZI!z`*># z)5S5QV$R#yXS@9ZB@Wb=+a+}bWvOU#KT-Bwlwz9JXdz3F1k+6@b*u>Oqr!9FE& z2JddkrI88Fy;ocWjk&#?vW%oAJ&c-ToqFufcf0TRp5G~sx7}~Gx77Oi&htON-m9Mf zeeZLZLYd?#i&aaQ{xhouGEQF6;2Cm2B}?FVVH)=lu?J=b^J?-qC$^mM_f?f+UDo(x z{Wn>Qz*42N8ij%db9a7vww-ao;#DoPd>=(6OgYAsemmpRee1I-8>N3V_&mt@&GY8- z+z4Zjo`%f_cz&{++cq<$=85*fUEVQ#cNWgr^D~zt;-T_MEAKZe#BoDgc1rfQ%hu{e(Hs_&vDHJy$3J@q~Q`GCG7&sL)qenQt7 z`cKaLBT}HE942p|+3{5*BHZlbE{`{EVf*&5m~khsW1q*FG)=GB$-Ae)_{6E{(FZQg z`DooB-E?DrqqoSP&8kaWGJk#&R^P?*_W=LFom^8L7pjyg>c^Ko=bd+m??LZ_Ir)uq zE1B9SL@cSfc*t8aWa*JjPrJolr3QownVuHcy>e=SmP*;;_7ndC{14crx>Mt&M`4?oFk3NKl6#C^y8Q)kbZ(h-qQgF5d%% zCGk7_nam68%R;<3I;$IYA4uZg=+DGnpcbf^A-^^6{>v$?h#r(zpvld(r*4w#a@{#NV%`$&?uk3W$JxA1_6KWA65osj| z5fl#U+#l4pJxf(+#+J$@XRG;wI_C%V<%{;Mx6WLxX6MD`t?IW#<~{Q_uH-9I|1Q~I zB|DvE!oG4jwsX1#dqlrqs%5BXiq?sn5|`lI_pkTYH|}HGADI1>QT}_{M8{Lb>*j9r z?ZG0h2P7E9xLH$pZpd-7cWb*JRN{3U;@HKwk}D~EC2 zm4r!lw?+T$a;o*1^Hkv7Nztl9v)-B(6#R2hty{uBt(9;4j~CK@kB9p9lQ> YvrilkX{>z)EC3ihUHx3vIVCg!0C%_Ik^lez literal 0 HcmV?d00001 diff --git a/miniprogram_npm/@jdmini/components/icons/link-active.png b/miniprogram_npm/@jdmini/components/icons/link-active.png new file mode 100644 index 0000000000000000000000000000000000000000..433ec7bf24d3f2a0367fcaf00069d9c7ca3105ed GIT binary patch literal 2796 zcmVe@dwWW-u0tq*p$S_j1pq;*)QL1C7p`(y}gx!E73>qwIMcRtC@@jdg z_(*FKlihoM{UeEu;hwv9ckhM`-R}&4aDV6g&W{i0p6Bl_F*6FvPU2h#a6YI2(FPSD z+MohN8&rU3gPbY@5shRVwx~=CUFH_zW(heNOyePFJTV+#oPgj{3>-`7i9K!Fl8-X+ zuUg5F(lAOO-rN|v+$HG_27CbKA@$pp1b7dDLM&a+#5M1`R@*x7AXcxBj!P!wN+R3~ zpg2P}UCeka!HEr4dgplqacNVNqw~$;pMa?rnZp~d_W`Vq*ZF^AR_MHd=+$;y$|Ctk z0E=?rp9ouvbiOU51-gywqaLECaoZF~XAHpH95>ZL(&>+BRUZsDjB1D#TIdQPXg7cv z7OT7g;0Orsfr$W~2w-L!^*h{>D~9(-6)WWuCtu`ij7&{R-VI=SilT>^=wa4nXr~r9 zZd9OLi_CF4cnLFp2wtHfD5%~MDy1=gn|32zoXNv$2O38jz&r79>VB~57hX|OeDDpv{ zOw1fy{-{{x<+jh$&^%oidK*jtGH8FFOx&0$KmgcTS98!Q#X<&-DT-X`7OwkJ`N_MU zSiL#w)<4n@0Q3cAA;zEjIN{B7`!-x2EqoOOUII{JVD55CnibV*PLG2`bU0tf(c&Hon$*DK7WNx`?8)z3SKB=I?=k$Imia(j3Fw6fFgc0azb=TQJBjoU@Q ztZv>x6imAT^ct8t9PG#mKmh3Y@v+d<+vg~{nmxXV=a@(v;Z#Oi9E(Vcx$L84qp{hD~4zovQB;{W8j-IiM4 z+W^05WMhFVRQ2+9$Fghn*WU;5{7ARUpnni>*2Kn=FIUyc%bs{HMPxj=2!TneIt7Rp z#SC3#D?l73Vtn54IY@bpP=Gj0Of=2#Iq;sU&WKMemD=`6a5w`1z9Lv&%b-aB#seVq zuMdwCaG06*5vcv5x!rrVE?cI2mF$#Hi%du|Uu|e)`A=275rH^xFR}!Z?g8NjFcSbU z&f1^50nBD#Hi*0opz~14DNlXqX=eHJR&BBIj>0A-1^a-rg95Y$je}S`79gZwRo-nx)DEs@NA5gdK$uy6I09v zfB@ho!pb61`ZrHw7J+ zx7ODDBJE%?c-KNJ{2hIF>HpWWvN#Z+Tx*y;v zCPkABQPx`Ldm){8JsV@C=t)KZzDZSv7!9#?{!e>*P9a(Z__h?{)}3p8`z$*KP^L9q z=%(TgOtjpj*p7IezuKx#D*yp>^vTj2+q6Z=!3Q~;oh<@o zVB7{M_pj5e`U_~G67(cODF7bT9r}`?0L_#YL~8)49mOth1AgMq{F#00+jrrA#zdK(_CwyCr3>stPvGlMpu%^ZUKrvo+NY z#^M-wQkS&I44`cjOJzf7HuYa7DNlNdvA37It5g2t`ZK-SF*MJHNQ>9jsID*5mR$e= zn72Z5jP)`uP|)0!?!!yf>kqA|44kt}){gaszWYyk*ZJWuQt#q|U>>^1$$Y!+sS zMuMzR#lb|&i>UM#PcXcpPj>$<)qhY$FnXOJUIod^V0z%frH8jVs%4wrq1#Zj9<5RbccRTIxBlKc^bm^fbp=R zt?WhgdXl8?`L%%)nYzemiIbG_PN>E!rWJMp1b}$0|4|4k2S|A#l(87UcI$F}HUQ0J z3Xupth3T(fq=%k1mF`JTpj?4^7uOcU8-+W?2z(x_SLsc)`dI{ zgISJ2x3|{SJejMYfc|wUbp1yR{x-}r{fA2eXb17;1^O$^nx;v+VoLiH3@P-R0mHqY zbUGNGWuxe>j)9Q~7U9gbIQ3mF(5-@}N6V=`5KN@=%z)28ZCCMF0r;H8` zGx1ypIUprOCYl0c@PBSu?Neq0*sXz!`}To9c`5an$UI(QIGUSKMz2gJ7BYm$5F^$L zM~l)vlqGN>32K;>KnSGBd!ck@%Krvk0$cdry-q zDSM)nP<`$-*VE9P>Wj~;Ri-_6i6ZQ@O&bHW1fc4H*(!>CQ?F7Fz<%qijtX@ zE~SLnrUlLrqr=EXpcV5UZnEq57e1p;0TQk^ zFqep=!}g4@Y-KvrO5%{hGD_h*lL8euxnsgw)nKJI%E zFn)yyn3@!FZ$&U-oGflZ6kJ4_FuWB&s&(3FfHpzOZE-Dd$f$s)p?Nwn)i7u| zfG?)-mp1?b8z2s3mF@&E-(s6b2sq536HJnbX&ivr=>L5hE6OW?fK9nL13xnrhINpc z>J2HmDxUxXHs#{Pw5ku>k}HU?)t0D?)!HW$3-S%1nKDi{_ST2)1krj1F0(0?9uTct zpznXk&U+C%m6M$9YAt$EQplAAtN<`3!`PBc{Cn21Hs5<+P7~j&*GI?cN&XhV0RT4v zaHXiC1K0;bi`4n4e1|uv5&M~|iVdw7i$vcI3``}K6VQL*rGs&b1-}Q0<3&Z(mak`n yW=b1kLDp5cFR?)dh&HGI(FPSD+MohN8}whlcnOCslS2vs0000zdhv-2$L=Va#dQcA0gHF_b zC=?3y^uw1%0oMTSz?gDs0(Jv?fStff;9#7>)tOAGR0>TQ(s*DY&;twrB}?iDJ^~&_ zAUjKCm0^gZfyKZP?BKL?s9Z+T#o$na*aSR9GI-VLH{c$d;h_R?1n@Eb;8mx$fM(73 zP=GiA*hqQ+gR}N||JFp)p0Bt}=g#2rPYmr;Vf6DUc zJQ3K2y#;Lr?ni)Y>I9wu`W(u8yE=1LyYnQZMVTu5J1`G8DMGo;z?0Yqr+X_iWf9^e z>;N}lZh1Cc0PHZ7e}IbVPu3t##$34)@CAk;#evTi2fzAgG zE4B^+V+cm-4YVC7?~xA0JkIeqM#D*1E`F^~XA>SUTRiE`nIouPccPs@-)tkopt`~6ym!5CP^ z0XGAe0Mp8!#~^XPzkyyP^won*a#v#LN}e?q2}*=%mzt|iQ=BH?HU!FX?6JJc)Catb z;l}P!cPl;|2igX6o_Ia*gW_D2bQE|6X!Yq_g$lseitTS1Z;MHur z6I`gV^1a*FiR{X`3FCFwgBChvY)HWkal-=0k{RYG)84xfhCr* zmqjeo1>hR0=Fw?FbOE>;I6Y!Tt92J}Oeud;gmRj(A4SX{6K8Oem2In z1qRxrlyeN26QP_ijRZck`0ET1l0!Th^U^5pAhOI-<`UvXZLYxnherYk$sxXIaj2Vk zqwox5#bGMD7nqM5%;~_J*khKF0fNrTW$io>*o?HsFs%k|L^j*wXg+Rh-^0LiWF_%) z`EwHzjh+Tv2b||nP9L&X-W=6Yb*yk`iKduf4}Ru>&lqg@Ogo9u0t3bE&K>C8Z7=%H zSR6fQu;nAI$FKqKqPP$j8=RV*#34m9NhcQh8iq$JToo7M2F0oGNpHKg71>MqOR*g# ztwYZ9cu^dP?dYT29AAzZ)CRnS+e!b*G=Qv;Z^82)9cOgD6A7SJ>o_tZs+-xK)r8FZ zu0rCGQ;-?MFa+2>1oSt^A5LA^+K(xb-ZD}}?WtmQeX%MHT;fO)gkq3X)0e{rlwwny2^DOka*yK96 zRzw00fz3aWlVyXZIFs#B=v$PJrtEJK_%g)qVf?PwBPs5(Zx!JXcOg$8rJ*9>0E09u zCA(I|nI{6gt9;*%oPqJJB62?GBEvO}cVD!$#5Vr_WK(t5atVUEs))G-mXLr*l!wE; zDsFcMIpC;OY6sFPld6av;8v17Af8xBAQq8V1eid&obu8%1qrP+G(boO5#(J6rb-?G zo+jE>K@;*qpM#dN8v-CChY0d^4qGg55AuSFa}2&rMg}Eq&$%H1LcDll^;gZnhrnfy zHn$^3M0O)5^G72`P&yp*Z$ozFdL6G%U2)=xVH$yKS=w4q(zKBVYJq`#bmu_L$UC*t z0=OK%FUjyGpAZAhLt=}*;2i-Tqk%(hX+Q*dC&RNya3C(ce&lpm!*$ { + this.setData({ ads: adManager.ads }) + }) + }, + }, + methods: { + } +}) diff --git a/miniprogram_npm/@jdmini/components/jdwx-ad/index.json b/miniprogram_npm/@jdmini/components/jdwx-ad/index.json new file mode 100644 index 0000000..32640e0 --- /dev/null +++ b/miniprogram_npm/@jdmini/components/jdwx-ad/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} \ No newline at end of file diff --git a/miniprogram_npm/@jdmini/components/jdwx-ad/index.wxml b/miniprogram_npm/@jdmini/components/jdwx-ad/index.wxml new file mode 100644 index 0000000..1b24adf --- /dev/null +++ b/miniprogram_npm/@jdmini/components/jdwx-ad/index.wxml @@ -0,0 +1,5 @@ + + + + + diff --git a/miniprogram_npm/@jdmini/components/jdwx-ad/index.wxss b/miniprogram_npm/@jdmini/components/jdwx-ad/index.wxss new file mode 100644 index 0000000..de627db --- /dev/null +++ b/miniprogram_npm/@jdmini/components/jdwx-ad/index.wxss @@ -0,0 +1,7 @@ +.jdwx-ad-component { + padding: 10rpx; +} + +.jdwx-ad-item { + bottom: 10rpx; +} \ No newline at end of file diff --git a/miniprogram_npm/@jdmini/components/jdwx-link/index.js b/miniprogram_npm/@jdmini/components/jdwx-link/index.js new file mode 100644 index 0000000..887d749 --- /dev/null +++ b/miniprogram_npm/@jdmini/components/jdwx-link/index.js @@ -0,0 +1,37 @@ +import { adManager } from '@jdmini/api' + +Component({ + properties: { + }, + data: { + link: [], + top: {}, + }, + pageLifetimes: { + show: function () { + adManager.onDataReady(() => { + this.setData({ + link: adManager.link, + top: adManager.top + }) + }) + }, + }, + methods: { + gotopLink: function () { + wx.vibrateShort() + wx.openEmbeddedMiniProgram({ + appId: this.data.top.appId, + path: this.data.top.linkPage + }); + }, + goLink: function (e) { + let index = e.currentTarget.id + wx.vibrateShort() + wx.openEmbeddedMiniProgram({ + appId: this.data.link[index].appId, + path: this.data.link[index].linkPage + }); + }, + } +}) diff --git a/miniprogram_npm/@jdmini/components/jdwx-link/index.json b/miniprogram_npm/@jdmini/components/jdwx-link/index.json new file mode 100644 index 0000000..fba482a --- /dev/null +++ b/miniprogram_npm/@jdmini/components/jdwx-link/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} \ No newline at end of file diff --git a/miniprogram_npm/@jdmini/components/jdwx-link/index.wxml b/miniprogram_npm/@jdmini/components/jdwx-link/index.wxml new file mode 100644 index 0000000..30ff009 --- /dev/null +++ b/miniprogram_npm/@jdmini/components/jdwx-link/index.wxml @@ -0,0 +1,11 @@ + + + + {{top.linkName}} + {{top.appDsc}} + + + + {{item.linkName}} + + \ No newline at end of file diff --git a/miniprogram_npm/@jdmini/components/jdwx-link/index.wxss b/miniprogram_npm/@jdmini/components/jdwx-link/index.wxss new file mode 100644 index 0000000..111b9f1 --- /dev/null +++ b/miniprogram_npm/@jdmini/components/jdwx-link/index.wxss @@ -0,0 +1,63 @@ +/* 页面容器 */ +.jdwx-link-component { + /* background-image: linear-gradient(to right, #4F9863, #4F9863); */ + background-attachment: fixed; + background-size: cover; /* 确保背景图像覆盖整个元素 */ + /* height: 100vh; */ + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + padding: 20rpx; + box-sizing: border-box; + overflow: auto; /* 允许内容滚动 */ +} + +/* 列表项样式 */ +.jdwx-applink-list { + display: flex; + align-items: center; + width: 95%; + /* 假设我们想要每个view的高度约为屏幕高度的1/8 */ + /* 使用小程序的wx.getSystemInfo API动态计算并设置这个值会更准确 */ + height: calc((100vh - 40rpx) / 10); /* 减去容器padding的影响 */ + padding: 20rpx; + background-color: rgba(248, 250, 252, 1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + border-radius: 8rpx; + margin-bottom: 10rpx; +} +.jdwx-applink-top { + display: flex; + align-items: center; + width: 95%; + /* 假设我们想要每个view的高度约为屏幕高度的1/8 */ + /* 使用小程序的wx.getSystemInfo API动态计算并设置这个值会更准确 */ + height: calc((100vh - 40rpx) / 6); /* 减去容器padding的影响 */ + padding: 20rpx; + border-radius: 8rpx; + margin-bottom: 30rpx; + background-color: rgba(248, 250, 252, 1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} +.jdwx-applink-top-linkname{ + display: flex; + font-size: 36rpx; + color: rgb(39, 37, 37); + padding-bottom: 10rpx; +} +/* 图标样式 */ +.jdwx-applink-icon { + width: 120rpx; + height: 120rpx; + border-radius: 50%; + margin-right: 50rpx; + margin-left: 30rpx; +} + +/* 文本样式 */ +.jdwx-applink-text { + flex: 1; + font-size: 32rpx; + color: rgb(39, 37, 37); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5e075aa --- /dev/null +++ b/package-lock.json @@ -0,0 +1,26 @@ +{ + "name": "template", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@jdmini/api": "^1.0.10", + "@jdmini/components": "^1.0.6" + } + }, + "node_modules/@jdmini/api": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/@jdmini/api/-/api-1.0.10.tgz", + "integrity": "sha512-bVFU0awuY033mUT4QqArrYbrnPkBaBFKHoqCMHTVnRCk4b6gTs+cCGDH8uyf2t8ybCgWITKxaaH4Vjzyq8VF8g==" + }, + "node_modules/@jdmini/components": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/@jdmini/components/-/components-1.0.6.tgz", + "integrity": "sha512-ndva1nlZ1QJqDVgHfB0GPxMGmXsZ7SbWjUkRm/WoQIkow75fFbaQCW/xhtQQ+bPbJLjXmCg2p2356klsLLib8A==", + "peerDependencies": { + "@jdmini/api": ">=1.0.8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3a13fe8 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "@jdmini/api": "^1.0.10", + "@jdmini/components": "^1.0.6" + } +} diff --git a/pages/category/category.js b/pages/category/category.js new file mode 100644 index 0000000..603308b --- /dev/null +++ b/pages/category/category.js @@ -0,0 +1,43 @@ +import { injectPage } from '@jdmini/api' +const { CATEGORIES, ALL_COURSES } = require('../../utils/data.js') + +Page(injectPage()({ + data: { + categories: CATEGORIES, + activeCategory: '', + courseList: [], + keyword: '' + }, + + onLoad(options) { + const category = options.category ? decodeURIComponent(options.category) : CATEGORIES[0] + const keyword = options.keyword ? decodeURIComponent(options.keyword) : '' + this.setData({ activeCategory: category, keyword }) + this.filterCourses(category, keyword) + }, + + filterCourses(category, keyword) { + let list = ALL_COURSES + if (keyword) { + list = list.filter(c => c.title.includes(keyword) || c.desc.includes(keyword) || c.category.includes(keyword)) + } else { + list = list.filter(c => c.category === category) + } + this.setData({ courseList: list }) + }, + + onCategorySwitch(e) { + const cat = e.currentTarget.dataset.category + this.setData({ activeCategory: cat, keyword: '' }) + this.filterCourses(cat, '') + }, + + onCourseTap(e) { + const { courseId } = e.currentTarget.dataset + wx.navigateTo({ url: `/pages/course-detail/course-detail?courseId=${courseId}` }) + }, + + onShareAppMessage() { + return { title: '画画怎么画 — 课程分类', path: '/pages/home/home' } + } +})) diff --git a/pages/category/category.json b/pages/category/category.json new file mode 100644 index 0000000..2c1c23a --- /dev/null +++ b/pages/category/category.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "课程分类", + "backgroundColor": "#f7f8fc" +} diff --git a/pages/category/category.wxml b/pages/category/category.wxml new file mode 100644 index 0000000..184514c --- /dev/null +++ b/pages/category/category.wxml @@ -0,0 +1,51 @@ + + + + + + {{item}} + + + + + + + 搜索"{{keyword}}"的结果,共{{courseList.length}}个课程 + + + + + + + + {{item.coverEmoji}} + + + {{item.title}} + + {{item.difficulty}} + {{item.lessons}}节课 + + ⏱ {{item.duration}} + + + + + 🔍 + 暂无该分类的课程 + + + + diff --git a/pages/category/category.wxss b/pages/category/category.wxss new file mode 100644 index 0000000..9beb6d3 --- /dev/null +++ b/pages/category/category.wxss @@ -0,0 +1,99 @@ +.category-page { + display: flex; + flex-direction: column; + height: 100vh; + background: #f7f8fc; +} + +/* Tab 分类 */ +.tab-scroll { + background: #ffffff; + white-space: nowrap; + flex-shrink: 0; + border-bottom: 1rpx solid #F0F0F0; +} +.tab-list { + display: flex; + flex-direction: row; + padding: 0 16rpx; +} +.tab-item { + display: inline-flex; + align-items: center; + padding: 28rpx 24rpx; + font-size: 28rpx; + color: #666666; + border-bottom: 4rpx solid transparent; + white-space: nowrap; + flex-shrink: 0; +} +.tab-item.active { + color: #6C8CFF; + font-weight: 600; + border-bottom-color: #6C8CFF; +} + +/* 关键词提示 */ +.keyword-tip { + padding: 16rpx 32rpx; + font-size: 24rpx; + color: #888888; + background: #FFF8F0; + border-bottom: 1rpx solid #FFE5B0; +} + +/* 课程列表 */ +.course-scroll { + flex: 1; + overflow: hidden; +} +.course-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20rpx; + padding: 24rpx 24rpx 0; +} +.course-card { + background: #ffffff; + border-radius: 20rpx; + overflow: hidden; + box-shadow: 0 4rpx 16rpx rgba(108,140,255,0.08); +} +.course-cover { + height: 200rpx; + display: flex; + align-items: center; + justify-content: center; +} +.course-cover-emoji { + font-size: 72rpx; +} +.course-info { + padding: 20rpx; +} +.course-title { + font-size: 28rpx; + font-weight: 600; + color: #333333; + display: block; + margin-bottom: 12rpx; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} +.course-meta { + display: flex; + align-items: center; + gap: 12rpx; + margin-bottom: 8rpx; +} +.course-lessons { + font-size: 22rpx; + color: #AAAAAA; +} +.course-duration { + font-size: 22rpx; + color: #AAAAAA; + display: block; +} diff --git a/pages/course-detail/course-detail.js b/pages/course-detail/course-detail.js new file mode 100644 index 0000000..0f33335 --- /dev/null +++ b/pages/course-detail/course-detail.js @@ -0,0 +1,60 @@ +import { injectPage } from '@jdmini/api' +const { getCourseById } = require('../../utils/data.js') +const storage = require('../../utils/storage.js') + +Page(injectPage()({ + data: { + course: null, + progress: null, + isFavorite: false, + hasStarted: false + }, + + onLoad(options) { + const courseId = options.courseId + const course = getCourseById(courseId) + if (!course) { + wx.showToast({ title: '课程不存在', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + return + } + const progress = storage.getCourseProgress(courseId) + const isFav = storage.isFavorite(courseId) + this.setData({ + course, + progress, + isFavorite: isFav, + hasStarted: progress.currentStep > 0 || progress.completed + }) + }, + + onToggleFavorite() { + const { course } = this.data + const isFav = storage.toggleFavorite(course.id) + this.setData({ isFavorite: isFav }) + wx.showToast({ title: isFav ? '已收藏' : '已取消收藏', icon: 'none' }) + }, + + onStartLearning() { + const { course, progress } = this.data + const stepIndex = progress.completed ? 0 : (progress.currentStep || 0) + storage.addRecentHistory({ + courseId: course.id, + courseTitle: course.title, + coverEmoji: course.coverEmoji, + coverColor: course.coverColor, + stepIndex + }) + wx.navigateTo({ + url: `/pages/study-step/study-step?courseId=${course.id}&stepIndex=${stepIndex}` + }) + }, + + onShareAppMessage() { + const { course } = this.data + return { + title: `学画画:${course ? course.title : ''}`, + path: `/pages/course-detail/course-detail?courseId=${course ? course.id : ''}` + } + } +})) diff --git a/pages/course-detail/course-detail.json b/pages/course-detail/course-detail.json new file mode 100644 index 0000000..34bf9a8 --- /dev/null +++ b/pages/course-detail/course-detail.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "课程详情", + "backgroundColor": "#f7f8fc" +} diff --git a/pages/course-detail/course-detail.wxml b/pages/course-detail/course-detail.wxml new file mode 100644 index 0000000..0c26f0c --- /dev/null +++ b/pages/course-detail/course-detail.wxml @@ -0,0 +1,74 @@ + + + + {{course.coverEmoji}} + + {{course.title}} + + + + {{isFavorite ? '❤️' : '🤍'}} + + + + + + + + {{course.difficulty}} + {{course.lessons}}节课 + ⏱ {{course.duration}} + + {{course.desc}} + + + + + + 🎯 学习目标 + {{course.target}} + + + + 👤 适合人群 + {{course.suitable}} + + + + 🖌 所需工具 + {{course.tools}} + + + + + + 课程步骤({{course.steps.length}}步) + + + + + {{index + 1}} + + + {{item.title}} + + + + + + + + + + + + 已学到第{{progress.currentStep + 1}}步,继续加油! + + + ✅ 已完成本课,可重新学习 + + + + diff --git a/pages/course-detail/course-detail.wxss b/pages/course-detail/course-detail.wxss new file mode 100644 index 0000000..76b7492 --- /dev/null +++ b/pages/course-detail/course-detail.wxss @@ -0,0 +1,165 @@ +.detail-page { + display: flex; + flex-direction: column; + height: 100vh; +} + +/* 封面 */ +.detail-cover { + height: 380rpx; + display: flex; + align-items: center; + justify-content: center; + position: relative; + flex-shrink: 0; +} +.cover-emoji { + font-size: 120rpx; +} +.cover-overlay { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(transparent, rgba(0,0,0,0.35)); + padding: 32rpx; +} +.cover-title { + font-size: 40rpx; + font-weight: 700; + color: #ffffff; +} +.fav-btn { + position: absolute; + top: 24rpx; + right: 24rpx; + width: 72rpx; + height: 72rpx; + background: rgba(255,255,255,0.9); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 36rpx; +} + +/* 滚动内容 */ +.detail-body { + flex: 1; + overflow: hidden; + padding: 24rpx 32rpx 0; +} + +/* 基本信息 */ +.info-section { + padding: 28rpx; + margin-bottom: 20rpx; +} +.info-badges { + display: flex; + flex-wrap: wrap; + gap: 12rpx; + margin-bottom: 20rpx; +} +.info-desc { + font-size: 28rpx; + color: #555555; + line-height: 1.7; +} + +/* 元信息 */ +.meta-section { + padding: 8rpx 28rpx; + margin-bottom: 20rpx; +} +.meta-item { + padding: 24rpx 0; + display: flex; + flex-direction: column; + gap: 10rpx; +} +.meta-label { + font-size: 26rpx; + color: #888888; + font-weight: 500; +} +.meta-value { + font-size: 28rpx; + color: #333333; + line-height: 1.6; +} + +/* 步骤列表 */ +.steps-section { + margin-bottom: 20rpx; +} +.steps-title { + font-size: 30rpx; + font-weight: 700; + color: #333333; + display: block; + margin-bottom: 20rpx; +} +.steps-list { + display: flex; + flex-direction: column; + gap: 0; + background: #ffffff; + border-radius: 20rpx; + overflow: hidden; +} +.step-item { + display: flex; + align-items: center; + padding: 24rpx 28rpx; + gap: 20rpx; + border-bottom: 1rpx solid #F5F5F5; +} +.step-item:last-child { + border-bottom: none; +} +.step-num { + width: 56rpx; + height: 56rpx; + border-radius: 50%; + background: #F0F0F0; + color: #999999; + font-size: 26rpx; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} +.step-num.done { + background: #6CE5A0; + color: #ffffff; +} +.step-num.current { + background: #6C8CFF; + color: #ffffff; +} +.step-name { + font-size: 28rpx; + color: #333333; +} + +/* 底部按钮 */ +.detail-footer { + padding: 20rpx 32rpx; + background: #ffffff; + border-top: 1rpx solid #F0F0F0; + padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); +} +.progress-hint { + text-align: center; + font-size: 24rpx; + color: #888888; + margin-bottom: 16rpx; +} +.completed-hint { + color: #3CB371; +} +.start-btn { + width: 100%; +} diff --git a/pages/home/home.js b/pages/home/home.js new file mode 100644 index 0000000..1481984 --- /dev/null +++ b/pages/home/home.js @@ -0,0 +1,68 @@ +import { injectPage } from '@jdmini/api' +const { BEGINNER_PATH, CATEGORIES, DAILY_RECOMMEND, ALL_COURSES } = require('../../utils/data.js') +const storage = require('../../utils/storage.js') + +Page(injectPage()({ + data: { + beginnerPath: BEGINNER_PATH, + categories: CATEGORIES, + dailyRecommend: DAILY_RECOMMEND, + recentHistory: [], + searchKeyword: '' + }, + + onLoad() { + this.loadRecentHistory() + }, + + onShow() { + this.loadRecentHistory() + }, + + loadRecentHistory() { + const history = storage.getRecentHistory() + this.setData({ recentHistory: history.slice(0, 4) }) + }, + + onSearchInput(e) { + this.setData({ searchKeyword: e.detail.value }) + }, + + onSearchConfirm() { + const kw = this.data.searchKeyword.trim() + if (!kw) return + wx.navigateTo({ + url: `/pages/category/category?keyword=${encodeURIComponent(kw)}` + }) + }, + + onPathCardTap(e) { + const { courseId } = e.currentTarget.dataset + wx.navigateTo({ url: `/pages/course-detail/course-detail?courseId=${courseId}` }) + }, + + onCategoryTap(e) { + const { category } = e.currentTarget.dataset + wx.navigateTo({ url: `/pages/category/category?category=${encodeURIComponent(category)}` }) + }, + + onRecommendTap(e) { + const { courseId } = e.currentTarget.dataset + wx.navigateTo({ url: `/pages/course-detail/course-detail?courseId=${courseId}` }) + }, + + onHistoryTap(e) { + const { courseId } = e.currentTarget.dataset + const progress = storage.getCourseProgress(courseId) + wx.navigateTo({ + url: `/pages/study-step/study-step?courseId=${courseId}&stepIndex=${progress.currentStep}` + }) + }, + + onShareAppMessage() { + return { + title: '画画怎么画 — 零基础绘画学习', + path: '/pages/home/home' + } + } +})) diff --git a/pages/home/home.json b/pages/home/home.json new file mode 100644 index 0000000..e1a2f98 --- /dev/null +++ b/pages/home/home.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "画画怎么画", + "navigationBarBackgroundColor": "#6C8CFF", + "navigationBarTextStyle": "white", + "backgroundColor": "#f7f8fc" +} diff --git a/pages/home/home.wxml b/pages/home/home.wxml new file mode 100644 index 0000000..abb43b5 --- /dev/null +++ b/pages/home/home.wxml @@ -0,0 +1,126 @@ + + + + + + 画画怎么画 + 从零开始,画出属于你的世界 🎨 + + + + + 🔍 + + + + + + + + + 零基础入门路径 + 按顺序学,稳稳起步 + + + + + 步骤 {{index + 1}} + {{item.icon}} + {{item.title}} + {{item.desc}} + + + + + + + + + 课程分类 + + + + {{index === 0 ? '✏️' : index === 1 ? '🧍' : index === 2 ? '🐾' : index === 3 ? '🌿' : index === 4 ? '🌄' : '📐'}} + {{item}} + + + + + + + + 今日推荐 + + + + + {{item.coverEmoji}} + + + {{item.title}} + {{item.desc}} + + {{item.difficulty}} + {{item.lessons}}节课 · {{item.duration}} + + + + + + + + + + 继续学习 + + + + + {{item.coverEmoji}} + + + {{item.courseTitle}} + 上次学习:第{{item.stepIndex + 1}}步 + + + + + + + + + + diff --git a/pages/home/home.wxss b/pages/home/home.wxss new file mode 100644 index 0000000..c3afacb --- /dev/null +++ b/pages/home/home.wxss @@ -0,0 +1,241 @@ +.home-page { + height: 100vh; + background: #f7f8fc; +} + +/* Header */ +.home-header { + background: linear-gradient(135deg, #6C8CFF 0%, #8BA4FF 100%); + padding: 60rpx 32rpx 40rpx; +} +.header-top { + margin-bottom: 28rpx; +} +.header-title { + display: block; + font-size: 48rpx; + font-weight: 700; + color: #ffffff; + letter-spacing: 2rpx; +} +.header-slogan { + display: block; + font-size: 26rpx; + color: rgba(255,255,255,0.85); + margin-top: 8rpx; +} + +/* 搜索框 */ +.search-bar { + display: flex; + align-items: center; + background: #ffffff; + border-radius: 50rpx; + padding: 18rpx 28rpx; + gap: 12rpx; +} +.search-icon { + font-size: 30rpx; +} +.search-input { + flex: 1; + font-size: 28rpx; + color: #333333; + height: 40rpx; + line-height: 40rpx; +} + +/* 主体 */ +.home-body { + padding: 0 32rpx; +} + +/* 通用section */ +.section { + margin-top: 48rpx; +} +.section-header { + display: flex; + align-items: baseline; + gap: 16rpx; + margin-bottom: 24rpx; +} +.section-title { + font-size: 34rpx; + font-weight: 700; + color: #333333; +} +.section-sub { + font-size: 24rpx; + color: #AAAAAA; +} + +/* 入门路径 */ +.path-scroll { + margin: 0 -32rpx; + padding: 0 32rpx; +} +.path-list { + display: flex; + flex-direction: row; + gap: 20rpx; + padding-right: 32rpx; + width: max-content; +} +.path-card { + width: 240rpx; + border-radius: 24rpx; + padding: 28rpx 24rpx; + display: flex; + flex-direction: column; + flex-shrink: 0; +} +.path-step { + font-size: 22rpx; + color: rgba(255,255,255,0.75); + margin-bottom: 12rpx; +} +.path-icon { + font-size: 56rpx; + margin-bottom: 16rpx; +} +.path-title { + font-size: 30rpx; + font-weight: 700; + color: #ffffff; + margin-bottom: 8rpx; +} +.path-desc { + font-size: 22rpx; + color: rgba(255,255,255,0.85); + line-height: 1.5; +} + +/* 分类网格 */ +.category-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20rpx; +} +.category-item { + background: #ffffff; + border-radius: 20rpx; + padding: 28rpx 16rpx; + display: flex; + flex-direction: column; + align-items: center; + gap: 12rpx; + box-shadow: 0 4rpx 16rpx rgba(108,140,255,0.08); +} +.category-emoji { + font-size: 48rpx; +} +.category-name { + font-size: 26rpx; + color: #333333; + font-weight: 500; +} + +/* 今日推荐 */ +.recommend-list { + display: flex; + flex-direction: column; + gap: 20rpx; +} +.recommend-card { + background: #ffffff; + border-radius: 20rpx; + display: flex; + flex-direction: row; + overflow: hidden; + box-shadow: 0 4rpx 16rpx rgba(108,140,255,0.08); +} +.recommend-cover { + width: 160rpx; + height: 160rpx; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} +.recommend-emoji { + font-size: 64rpx; +} +.recommend-info { + flex: 1; + padding: 24rpx 24rpx 24rpx 20rpx; + display: flex; + flex-direction: column; + justify-content: space-between; +} +.recommend-title { + font-size: 30rpx; + font-weight: 600; + color: #333333; +} +.recommend-desc { + font-size: 24rpx; + color: #888888; + line-height: 1.5; + margin: 8rpx 0; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} +.recommend-meta { + display: flex; + align-items: center; + gap: 16rpx; +} +.recommend-lessons { + font-size: 22rpx; + color: #AAAAAA; +} + +/* 最近学习 */ +.history-list { + display: flex; + flex-direction: column; + gap: 16rpx; +} +.history-item { + background: #ffffff; + border-radius: 20rpx; + display: flex; + align-items: center; + padding: 20rpx; + gap: 20rpx; + box-shadow: 0 4rpx 16rpx rgba(108,140,255,0.06); +} +.history-cover { + width: 80rpx; + height: 80rpx; + border-radius: 16rpx; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} +.history-emoji { + font-size: 36rpx; +} +.history-info { + flex: 1; +} +.history-title { + font-size: 28rpx; + font-weight: 600; + color: #333333; + display: block; +} +.history-time { + font-size: 24rpx; + color: #AAAAAA; + margin-top: 6rpx; + display: block; +} +.history-arrow { + font-size: 36rpx; + color: #CCCCCC; +} diff --git a/pages/practice/practice.js b/pages/practice/practice.js new file mode 100644 index 0000000..6ec4483 --- /dev/null +++ b/pages/practice/practice.js @@ -0,0 +1,60 @@ +import { injectPage } from '@jdmini/api' +const { PRACTICE_TASKS } = require('../../utils/data.js') +const storage = require('../../utils/storage.js') + +Page(injectPage()({ + data: { + allTasks: PRACTICE_TASKS, + filteredTasks: PRACTICE_TASKS, + activeDifficulty: '全部', + difficulties: ['全部', '入门', '初级'], + worksRecords: [], + completedCount: 0 + }, + + onLoad() { + this.loadData() + }, + + onShow() { + this.loadData() + }, + + loadData() { + const records = storage.getWorksRecords() + const completedIds = records.map(r => r.courseId) + const tasks = this.data.allTasks.map(t => ({ + ...t, + done: completedIds.includes(t.courseId) + })) + this.setData({ + allTasks: tasks, + filteredTasks: this.filterByDifficulty(tasks, this.data.activeDifficulty), + completedCount: completedIds.length + }) + }, + + filterByDifficulty(tasks, diff) { + if (diff === '全部') return tasks + return tasks.filter(t => t.difficulty === diff) + }, + + onDifficultySwitch(e) { + const diff = e.currentTarget.dataset.diff + this.setData({ + activeDifficulty: diff, + filteredTasks: this.filterByDifficulty(this.data.allTasks, diff) + }) + }, + + onTaskTap(e) { + const { courseId, stepIndex } = e.currentTarget.dataset + wx.navigateTo({ + url: `/pages/study-step/study-step?courseId=${courseId}&stepIndex=${stepIndex}` + }) + }, + + onShareAppMessage() { + return { title: '画画怎么画 — 每日练习', path: '/pages/practice/practice' } + } +})) diff --git a/pages/practice/practice.json b/pages/practice/practice.json new file mode 100644 index 0000000..54833f6 --- /dev/null +++ b/pages/practice/practice.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "每日练习", + "backgroundColor": "#f7f8fc" +} diff --git a/pages/practice/practice.wxml b/pages/practice/practice.wxml new file mode 100644 index 0000000..1a567fd --- /dev/null +++ b/pages/practice/practice.wxml @@ -0,0 +1,62 @@ + + + + + + + + + + {{item}} + + + + + + + + {{item.icon}} + + + + {{item.title}} + + ✓ 已完成 + + + {{item.type}} + {{item.desc}} + + {{item.difficulty}} + ⏱ {{item.duration}} + + + + + + + 📝 + 没有该难度的练习 + + + + diff --git a/pages/practice/practice.wxss b/pages/practice/practice.wxss new file mode 100644 index 0000000..48f2348 --- /dev/null +++ b/pages/practice/practice.wxss @@ -0,0 +1,151 @@ +.practice-page { + display: flex; + flex-direction: column; + height: 100vh; + background: #f7f8fc; +} + +/* Banner */ +.practice-banner { + background: linear-gradient(135deg, #FFB84D 0%, #FF9A1F 100%); + padding: 40rpx 32rpx; + display: flex; + justify-content: space-between; + align-items: center; +} +.banner-title { + display: block; + font-size: 40rpx; + font-weight: 700; + color: #ffffff; +} +.banner-sub { + display: block; + font-size: 24rpx; + color: rgba(255,255,255,0.85); + margin-top: 8rpx; +} +.banner-stats { + text-align: center; + background: rgba(255,255,255,0.2); + border-radius: 20rpx; + padding: 16rpx 32rpx; +} +.stats-num { + display: block; + font-size: 56rpx; + font-weight: 700; + color: #ffffff; + line-height: 1; +} +.stats-label { + display: block; + font-size: 22rpx; + color: rgba(255,255,255,0.85); + margin-top: 4rpx; +} + +/* 筛选栏 */ +.filter-bar { + background: #ffffff; + display: flex; + flex-direction: row; + padding: 0 24rpx; + border-bottom: 1rpx solid #F0F0F0; + flex-shrink: 0; +} +.filter-item { + padding: 24rpx 20rpx; + font-size: 28rpx; + color: #666666; + border-bottom: 4rpx solid transparent; + white-space: nowrap; +} +.filter-item.active { + color: #FFB84D; + font-weight: 600; + border-bottom-color: #FFB84D; +} + +/* 任务列表 */ +.task-scroll { + flex: 1; + overflow: hidden; +} +.task-list { + display: flex; + flex-direction: column; + gap: 16rpx; + padding: 24rpx 32rpx 0; +} +.task-card { + background: #ffffff; + border-radius: 20rpx; + display: flex; + align-items: center; + padding: 24rpx; + gap: 20rpx; + box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06); +} +.task-card.done { + opacity: 0.7; +} +.task-icon-wrap { + width: 88rpx; + height: 88rpx; + border-radius: 20rpx; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} +.task-icon { + font-size: 44rpx; +} +.task-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 8rpx; +} +.task-title-row { + display: flex; + align-items: center; + gap: 12rpx; +} +.task-title { + font-size: 30rpx; + font-weight: 600; + color: #333333; +} +.task-done-badge { + background: #E8FAF0; + color: #3CB371; + font-size: 20rpx; + padding: 4rpx 12rpx; + border-radius: 20rpx; +} +.task-type-badge { + font-size: 22rpx; + color: #AAAAAA; +} +.task-desc { + font-size: 24rpx; + color: #888888; + line-height: 1.5; +} +.task-meta { + display: flex; + align-items: center; + gap: 12rpx; + margin-top: 4rpx; +} +.task-duration { + font-size: 22rpx; + color: #AAAAAA; +} +.task-arrow { + font-size: 36rpx; + color: #CCCCCC; + flex-shrink: 0; +} diff --git a/pages/profile/profile.js b/pages/profile/profile.js new file mode 100644 index 0000000..b3c1b69 --- /dev/null +++ b/pages/profile/profile.js @@ -0,0 +1,86 @@ +import { injectPage } from '@jdmini/api' +const { getCourseById, ALL_COURSES } = require('../../utils/data.js') +const storage = require('../../utils/storage.js') + +Page(injectPage()({ + data: { + settings: null, + completedCount: 0, + practiceCount: 0, + favoriteCourses: [], + recentWorks: [], + showReminderModal: false + }, + + onLoad() { + this.loadData() + }, + + onShow() { + this.loadData() + }, + + loadData() { + const settings = storage.initSettings() + const completedCount = storage.getCompletedCount() + const practiceCount = storage.getCompletedPracticeCount() + const favoriteIds = storage.getFavorites() + const favoriteCourses = favoriteIds + .map(id => getCourseById(id)) + .filter(Boolean) + .slice(0, 4) + const recentWorks = storage.getWorksRecords().slice(0, 6) + + this.setData({ + settings, + completedCount, + practiceCount, + favoriteCourses, + recentWorks + }) + }, + + onToggleReminder() { + const { settings } = this.data + const newVal = !settings.reminderEnabled + const updated = storage.saveSettings({ reminderEnabled: newVal }) + this.setData({ settings: updated }) + wx.showToast({ title: newVal ? '学习提醒已开启' : '学习提醒已关闭', icon: 'none' }) + }, + + onFavCourseTap(e) { + const { courseId } = e.currentTarget.dataset + wx.navigateTo({ url: `/pages/course-detail/course-detail?courseId=${courseId}` }) + }, + + onWorkTap(e) { + const { imagePath } = e.currentTarget.dataset + wx.previewImage({ urls: [imagePath], current: imagePath }) + }, + + onAllCategoryTap() { + wx.navigateTo({ url: '/pages/category/category' }) + }, + + onHelpTap() { + wx.showModal({ + title: '帮助说明', + content: '零基础也能学会画画!从「零基础入门路径」开始,跟着步骤一步步完成,每天坚持练习,你会进步很快的 🎨', + showCancel: false, + confirmText: '知道了' + }) + }, + + onPrivacyTap() { + wx.showModal({ + title: '隐私说明', + content: '本小程序所有学习数据(进度、收藏、作品)均保存在你的手机本地,不会上传到服务器,请放心使用。', + showCancel: false, + confirmText: '好的' + }) + }, + + onShareAppMessage() { + return { title: '画画怎么画 — 零基础绘画学习', path: '/pages/home/home' } + } +})) diff --git a/pages/profile/profile.json b/pages/profile/profile.json new file mode 100644 index 0000000..47bd3e5 --- /dev/null +++ b/pages/profile/profile.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "我的", + "backgroundColor": "#f7f8fc" +} diff --git a/pages/profile/profile.wxml b/pages/profile/profile.wxml new file mode 100644 index 0000000..7d405d6 --- /dev/null +++ b/pages/profile/profile.wxml @@ -0,0 +1,111 @@ + + + + + 🎨 + + + + + + + + {{completedCount}} + 已学课程 + + + + {{practiceCount}} + 完成练习 + + + + {{favoriteCourses.length}} + 收藏课程 + + + + + + + 收藏课程 + + + + + {{item.coverEmoji}} + + {{item.title}} + + + + 暂无收藏,去课程详情页收藏你喜欢的课程吧 + + + + + + + 最近作品 + + + + + + {{item.courseTitle}} + + + + + + + + + + 🔔 + 学习提醒 + + + {{settings && settings.reminderEnabled ? '已开启' : '已关闭'}} + + + + + + + + 帮助说明 + + + + + + + 🔒 + 隐私说明 + + + + + + + diff --git a/pages/profile/profile.wxss b/pages/profile/profile.wxss new file mode 100644 index 0000000..bec48d9 --- /dev/null +++ b/pages/profile/profile.wxss @@ -0,0 +1,214 @@ +.profile-page { + height: 100vh; + background: #f7f8fc; +} + +/* 顶部用户信息 */ +.profile-header { + background: linear-gradient(135deg, #6C8CFF 0%, #8BA4FF 100%); + padding: 48rpx 32rpx 40rpx; + display: flex; + align-items: center; + gap: 28rpx; +} +.avatar-wrap { + width: 120rpx; + height: 120rpx; + border-radius: 50%; + background: rgba(255,255,255,0.25); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} +.avatar-emoji { + font-size: 60rpx; +} +.user-info { + flex: 1; +} +.user-name { + display: block; + font-size: 36rpx; + font-weight: 700; + color: #ffffff; + margin-bottom: 12rpx; +} +.user-days { + display: flex; + align-items: baseline; + gap: 4rpx; +} +.days-num { + font-size: 44rpx; + font-weight: 700; + color: #FFB84D; + line-height: 1; +} +.days-label { + font-size: 24rpx; + color: rgba(255,255,255,0.85); +} +.days-text { + font-size: 24rpx; + color: rgba(255,255,255,0.75); + margin-left: 6rpx; +} + +/* 数据统计 */ +.stats-section { + margin: 24rpx 32rpx 0; + padding: 32rpx; + display: flex; + align-items: center; +} +.stats-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 8rpx; +} +.stats-value { + font-size: 52rpx; + font-weight: 700; + color: #6C8CFF; + line-height: 1; +} +.stats-key { + font-size: 22rpx; + color: #888888; +} +.stats-divider { + width: 1rpx; + height: 60rpx; + background: #F0F0F0; +} + +/* 通用section */ +.section { + margin: 32rpx 32rpx 0; +} +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20rpx; +} +.section-title { + font-size: 30rpx; + font-weight: 700; + color: #333333; +} + +/* 收藏课程 */ +.fav-list { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16rpx; +} +.fav-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 10rpx; +} +.fav-cover { + width: 100%; + aspect-ratio: 1; + border-radius: 16rpx; + display: flex; + align-items: center; + justify-content: center; +} +.fav-emoji { + font-size: 44rpx; +} +.fav-title { + font-size: 20rpx; + color: #555555; + text-align: center; + line-height: 1.3; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} +.empty-tip { + font-size: 24rpx; + color: #BBBBBB; + text-align: center; + padding: 32rpx 0; +} + +/* 最近作品 */ +.works-scroll { + margin: 0 -32rpx; + padding: 0 32rpx; +} +.works-list { + display: flex; + flex-direction: row; + gap: 16rpx; + padding-right: 32rpx; + width: max-content; +} +.work-thumb { + width: 180rpx; + flex-shrink: 0; +} +.work-thumb-img { + width: 180rpx; + height: 180rpx; + border-radius: 16rpx; + background: #EEEEEE; +} +.work-thumb-title { + display: block; + font-size: 20rpx; + color: #888888; + text-align: center; + margin-top: 8rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* 设置列表 */ +.settings-section { + margin: 32rpx 32rpx 0; + background: #ffffff; + border-radius: 20rpx; + overflow: hidden; +} +.settings-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 28rpx 28rpx; +} +.settings-left { + display: flex; + align-items: center; + gap: 16rpx; +} +.settings-icon { + font-size: 36rpx; +} +.settings-label { + font-size: 28rpx; + color: #333333; +} +.settings-right { + display: flex; + align-items: center; + gap: 8rpx; +} +.settings-value { + font-size: 26rpx; + color: #AAAAAA; +} +.settings-arrow { + font-size: 36rpx; + color: #CCCCCC; +} diff --git a/pages/study-step/study-step.js b/pages/study-step/study-step.js new file mode 100644 index 0000000..3346853 --- /dev/null +++ b/pages/study-step/study-step.js @@ -0,0 +1,83 @@ +import { injectPage } from '@jdmini/api' +const { getCourseById } = require('../../utils/data.js') +const storage = require('../../utils/storage.js') + +Page(injectPage()({ + data: { + course: null, + currentStepIndex: 0, + totalSteps: 0, + currentStep: null, + isLastStep: false, + progressPercent: 0 + }, + + onLoad(options) { + const courseId = options.courseId + const stepIndex = parseInt(options.stepIndex) || 0 + const course = getCourseById(courseId) + if (!course) { + wx.showToast({ title: '课程不存在', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + return + } + wx.setNavigationBarTitle({ title: course.title }) + this.setCourse(course, stepIndex) + }, + + setCourse(course, stepIndex) { + const total = course.steps.length + const safeIndex = Math.max(0, Math.min(stepIndex, total - 1)) + const percent = Math.round((safeIndex / total) * 100) + this.setData({ + course, + totalSteps: total, + currentStepIndex: safeIndex, + currentStep: course.steps[safeIndex], + isLastStep: safeIndex === total - 1, + progressPercent: percent + }) + // 保存进度 + storage.saveProgress(course.id, safeIndex, false) + // 更新最近记录 + storage.addRecentHistory({ + courseId: course.id, + courseTitle: course.title, + coverEmoji: course.coverEmoji, + coverColor: course.coverColor, + stepIndex: safeIndex + }) + }, + + onPrevStep() { + const { currentStepIndex, course } = this.data + if (currentStepIndex <= 0) return + this.setCourse(course, currentStepIndex - 1) + }, + + onNextStep() { + const { currentStepIndex, course, isLastStep } = this.data + if (isLastStep) { + this.onFinishCourse() + return + } + this.setCourse(course, currentStepIndex + 1) + }, + + onFinishCourse() { + const { course } = this.data + // 标记完成 + storage.saveProgress(course.id, course.steps.length - 1, true) + wx.navigateTo({ + url: `/pages/work-submit/work-submit?courseId=${course.id}` + }) + }, + + onShareAppMessage() { + const { course } = this.data + return { + title: `我在学:${course ? course.title : ''}`, + path: `/pages/course-detail/course-detail?courseId=${course ? course.id : ''}` + } + } +})) diff --git a/pages/study-step/study-step.json b/pages/study-step/study-step.json new file mode 100644 index 0000000..71a3b8d --- /dev/null +++ b/pages/study-step/study-step.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "学习", + "backgroundColor": "#f7f8fc" +} diff --git a/pages/study-step/study-step.wxml b/pages/study-step/study-step.wxml new file mode 100644 index 0000000..8a85e9b --- /dev/null +++ b/pages/study-step/study-step.wxml @@ -0,0 +1,55 @@ + + + + + + + + 第 {{currentStepIndex + 1}} 步 + 共 {{totalSteps}} 步 + + + + + + + + 步骤 {{currentStepIndex + 1}} + {{currentStep.title}} + + + + + + {{currentStep.imageEmoji}} + 示意图 + + + + + + 📖 步骤说明 + {{currentStep.desc}} + + + + + {{currentStep.tip}} + + + + + + + + + + + diff --git a/pages/study-step/study-step.wxss b/pages/study-step/study-step.wxss new file mode 100644 index 0000000..eb387e3 --- /dev/null +++ b/pages/study-step/study-step.wxss @@ -0,0 +1,154 @@ +.step-page { + display: flex; + flex-direction: column; + height: 100vh; + background: #f7f8fc; +} + +/* 顶部进度 */ +.step-header { + background: #ffffff; + padding: 20rpx 32rpx 16rpx; + border-bottom: 1rpx solid #F0F0F0; + flex-shrink: 0; +} +.step-progress-bar { + height: 8rpx; + background: #EEEEEE; + border-radius: 4rpx; + overflow: hidden; + margin-bottom: 12rpx; +} +.step-progress-fill { + height: 100%; + background: linear-gradient(90deg, #6C8CFF, #FFB84D); + border-radius: 4rpx; + transition: width 0.3s ease; +} +.step-counter { + display: flex; + justify-content: space-between; + align-items: center; +} +.step-current { + font-size: 28rpx; + font-weight: 600; + color: #6C8CFF; +} +.step-total { + font-size: 24rpx; + color: #AAAAAA; +} + +/* 主内容 */ +.step-body { + flex: 1; + overflow: hidden; +} + +.step-title-wrap { + padding: 32rpx 32rpx 0; + display: flex; + align-items: center; + gap: 16rpx; +} +.step-badge { + background: #EEF1FF; + color: #6C8CFF; + font-size: 22rpx; + padding: 6rpx 18rpx; + border-radius: 20rpx; + font-weight: 500; + flex-shrink: 0; +} +.step-name { + font-size: 36rpx; + font-weight: 700; + color: #333333; +} + +/* 示意图 */ +.step-image-wrap { + padding: 24rpx 32rpx; +} +.step-image-placeholder { + border-radius: 24rpx; + height: 380rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12rpx; +} +.step-image-emoji { + font-size: 100rpx; +} +.step-image-hint { + font-size: 24rpx; + color: rgba(255,255,255,0.7); +} + +/* 讲解文字 */ +.step-desc-wrap { + margin: 0 32rpx 20rpx; + padding: 28rpx; +} +.step-desc-title { + display: block; + font-size: 26rpx; + font-weight: 600; + color: #888888; + margin-bottom: 16rpx; +} +.step-desc { + font-size: 30rpx; + color: #333333; + line-height: 1.8; +} + +/* 关键提示 */ +.step-tip-wrap { + margin: 0 32rpx 20rpx; + background: #FFF9E6; + border-left: 6rpx solid #FFB84D; + border-radius: 0 16rpx 16rpx 0; + padding: 20rpx 24rpx; +} +.step-tip { + font-size: 26rpx; + color: #B8860B; + line-height: 1.6; +} + +/* 底部按钮 */ +.step-footer { + display: flex; + gap: 20rpx; + padding: 20rpx 32rpx; + background: #ffffff; + border-top: 1rpx solid #F0F0F0; + padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); + flex-shrink: 0; +} +.step-btn { + flex: 1; + border-radius: 50rpx; + font-size: 30rpx; + font-weight: 500; + padding: 24rpx 0; + text-align: center; +} +.step-btn::after { border: none; } +.btn-prev { + background: #F0F0F0; + color: #666666; +} +.btn-prev.disabled { + background: #F5F5F5; + color: #CCCCCC; +} +.btn-next { + flex: 2; + background: #6C8CFF; + color: #ffffff; +} diff --git a/pages/work-submit/work-submit.js b/pages/work-submit/work-submit.js new file mode 100644 index 0000000..24a1a72 --- /dev/null +++ b/pages/work-submit/work-submit.js @@ -0,0 +1,65 @@ +import { injectPage } from '@jdmini/api' +const { getCourseById } = require('../../utils/data.js') +const storage = require('../../utils/storage.js') + +Page(injectPage()({ + data: { + course: null, + myWorkPath: '', + viewMode: 'example', // 'example' | 'mywork' | 'compare' + saved: false + }, + + onLoad(options) { + const courseId = options.courseId + const course = getCourseById(courseId) + this.setData({ course: course || null }) + }, + + onSwitchView(e) { + const { mode } = e.currentTarget.dataset + this.setData({ viewMode: mode }) + }, + + onChooseWork() { + wx.chooseMedia({ + count: 1, + mediaType: ['image'], + sourceType: ['album', 'camera'], + success: (res) => { + const filePath = res.tempFiles[0].tempFilePath + this.setData({ myWorkPath: filePath, viewMode: 'compare', saved: false }) + } + }) + }, + + onSaveRecord() { + const { course, myWorkPath } = this.data + if (!myWorkPath) { + wx.showToast({ title: '请先上传你的作品', icon: 'none' }) + return + } + storage.addWorkRecord({ + courseId: course ? course.id : '', + courseTitle: course ? course.title : '', + imagePath: myWorkPath + }) + this.setData({ saved: true }) + wx.showToast({ title: '保存成功!', icon: 'success' }) + }, + + onContinueLearning() { + wx.navigateBack({ delta: 2 }) + }, + + onBackToHome() { + wx.switchTab({ url: '/pages/home/home' }) + }, + + onShareAppMessage() { + return { + title: '我完成了一节绘画课!', + path: '/pages/home/home' + } + } +})) diff --git a/pages/work-submit/work-submit.json b/pages/work-submit/work-submit.json new file mode 100644 index 0000000..9a3024d --- /dev/null +++ b/pages/work-submit/work-submit.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "作品记录", + "backgroundColor": "#f7f8fc" +} diff --git a/pages/work-submit/work-submit.wxml b/pages/work-submit/work-submit.wxml new file mode 100644 index 0000000..3f0ab93 --- /dev/null +++ b/pages/work-submit/work-submit.wxml @@ -0,0 +1,91 @@ + + + + 🎉 + 完成本课! + {{course ? course.title : ''}} · 学完啦 + + + + + 示例作品 + 我的作品 + 对比查看 + + + + + + + + {{course ? course.coverEmoji : '🎨'}} + 范例参考图 + + 这是本课的参考范例,上传你的作品后可以对比查看 + + + + + + 📷 + 点击上传我的作品 + 支持相册选择或拍照 + + + + 点击重新上传 + + + + + + + + 范例 + + {{course ? course.coverEmoji : '🎨'}} + + + VS + + 我的作品 + + 📷 + 点击上传 + + + + + + + 太棒了!每一笔都是进步的证明 💪 + + + + + + + + + + ✅ 已保存记录 + + + + diff --git a/pages/work-submit/work-submit.wxss b/pages/work-submit/work-submit.wxss new file mode 100644 index 0000000..4527f4c --- /dev/null +++ b/pages/work-submit/work-submit.wxss @@ -0,0 +1,206 @@ +.submit-page { + display: flex; + flex-direction: column; + height: 100vh; + background: #f7f8fc; +} + +/* 完成横幅 */ +.complete-banner { + background: linear-gradient(135deg, #6C8CFF 0%, #8BA4FF 100%); + padding: 40rpx 32rpx 32rpx; + text-align: center; +} +.complete-emoji { + font-size: 72rpx; + display: block; + margin-bottom: 12rpx; +} +.complete-title { + display: block; + font-size: 44rpx; + font-weight: 700; + color: #ffffff; +} +.complete-sub { + display: block; + font-size: 26rpx; + color: rgba(255,255,255,0.85); + margin-top: 8rpx; +} + +/* 视图切换 */ +.view-switch { + background: #ffffff; + display: flex; + border-bottom: 1rpx solid #F0F0F0; + flex-shrink: 0; +} +.switch-item { + flex: 1; + text-align: center; + padding: 24rpx 0; + font-size: 28rpx; + color: #666666; + border-bottom: 4rpx solid transparent; +} +.switch-item.active { + color: #6C8CFF; + font-weight: 600; + border-bottom-color: #6C8CFF; +} + +/* 内容区 */ +.submit-body { + flex: 1; + overflow: hidden; +} +.work-display { + padding: 32rpx; + display: flex; + flex-direction: column; + align-items: center; + gap: 20rpx; +} +.work-frame { + width: 100%; + height: 500rpx; + border-radius: 24rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12rpx; + overflow: hidden; +} +.example-frame { + /* background from style */ +} +.my-frame { + background: #F5F5F5; + border: 4rpx dashed #CCCCCC; +} +.work-emoji { + font-size: 100rpx; +} +.work-label { + font-size: 24rpx; + color: rgba(255,255,255,0.8); +} +.upload-icon { + font-size: 72rpx; +} +.upload-text { + font-size: 28rpx; + color: #666666; + font-weight: 500; +} +.upload-hint { + font-size: 22rpx; + color: #AAAAAA; +} +.work-image { + width: 100%; + height: 100%; +} +.reupload-hint { + position: absolute; + bottom: 16rpx; + font-size: 22rpx; + color: rgba(255,255,255,0.7); + background: rgba(0,0,0,0.3); + padding: 6rpx 16rpx; + border-radius: 20rpx; +} +.work-hint { + font-size: 24rpx; + color: #AAAAAA; + text-align: center; + line-height: 1.6; +} + +/* 对比查看 */ +.compare-display { + padding: 32rpx; +} +.compare-row { + display: flex; + flex-direction: row; + align-items: center; + gap: 16rpx; +} +.compare-item { + flex: 1; + display: flex; + flex-direction: column; + gap: 12rpx; +} +.compare-label { + text-align: center; + font-size: 24rpx; + color: #888888; + font-weight: 500; +} +.compare-frame { + height: 320rpx; + border-radius: 20rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8rpx; + overflow: hidden; +} +.compare-emoji { + font-size: 64rpx; +} +.compare-image { + width: 100%; + height: 100%; +} +.upload-icon-sm { + font-size: 48rpx; +} +.upload-text-sm { + font-size: 22rpx; + color: #888888; +} +.compare-divider { + font-size: 28rpx; + font-weight: 700; + color: #CCCCCC; + flex-shrink: 0; +} +.compare-encourage { + display: block; + text-align: center; + margin-top: 24rpx; + font-size: 28rpx; + color: #6C8CFF; + font-weight: 500; +} + +/* 底部操作 */ +.submit-footer { + display: flex; + gap: 20rpx; + padding: 20rpx 32rpx; + background: #ffffff; + border-top: 1rpx solid #F0F0F0; + padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); + align-items: center; + flex-shrink: 0; +} +.save-btn { + flex: 1; +} +.continue-btn { + flex: 1; +} +.saved-hint { + flex: 1; + text-align: center; + font-size: 26rpx; + color: #3CB371; + font-weight: 500; +} diff --git a/project.config.json b/project.config.json new file mode 100644 index 0000000..960d67c --- /dev/null +++ b/project.config.json @@ -0,0 +1,41 @@ +{ + "compileType": "miniprogram", + "libVersion": "3.10.1", + "packOptions": { + "ignore": [], + "include": [] + }, + "setting": { + "coverView": true, + "es6": true, + "postcss": true, + "minified": true, + "enhance": true, + "showShadowRootInWxmlPanel": true, + "packNpmRelationList": [], + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + }, + "compileWorklet": false, + "uglifyFileName": false, + "uploadWithSourceMap": true, + "packNpmManually": false, + "minifyWXSS": true, + "minifyWXML": true, + "localPlugins": false, + "condition": false, + "swc": false, + "disableSWC": true, + "disableUseStrict": false, + "useCompilerPlugins": false + }, + "condition": {}, + "editorSetting": { + "tabIndent": "auto", + "tabSize": 2 + }, + "appid": "wxe4ef1cc6e75de032", + "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..250e19e --- /dev/null +++ b/project.private.config.json @@ -0,0 +1,24 @@ +{ + "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", + "projectname": "miniapp-huahuazenmehua", + "setting": { + "compileHotReLoad": true, + "urlCheck": false, + "coverView": true, + "lazyloadPlaceholderEnable": false, + "skylineRenderEnable": false, + "preloadBackgroundData": false, + "autoAudits": false, + "useApiHook": true, + "useApiHostProcess": true, + "showShadowRootInWxmlPanel": true, + "useStaticServer": false, + "useLanDebug": false, + "showES6CompileOption": false, + "bigPackageSizeSupport": false, + "checkInvalidKey": true, + "ignoreDevUnusedFiles": true + }, + "libVersion": "3.10.1", + "condition": {} +} \ No newline at end of file diff --git a/sitemap.json b/sitemap.json new file mode 100644 index 0000000..ca02add --- /dev/null +++ b/sitemap.json @@ -0,0 +1,7 @@ +{ + "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", + "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..5bd609f --- /dev/null +++ b/utils/api.js @@ -0,0 +1,10 @@ +/** + * API 接口定义 + * 画画怎么画 — 纯本地存储项目,无后端 API 调用 + * 所有数据操作通过 utils/storage.js 进行 + */ + +// 本项目为本地存储型工具,所有接口通过 storage.js 实现 +// 如后续需要接入后端,在此统一定义接口 + +module.exports = {} diff --git a/utils/auth.js b/utils/auth.js new file mode 100644 index 0000000..4d6437b --- /dev/null +++ b/utils/auth.js @@ -0,0 +1,269 @@ +/** + * 用户认证工具模块 + * 统一管理登录状态检查、登录跳转等 + */ + +/** + * 检查用户是否已登录 + * @returns {Boolean} 是否已登录 + */ +function isLoggedIn() { + const userId = wx.getStorageSync('userId') + const token = wx.getStorageSync('token') + return !!(userId && token) +} + +/** + * 获取用户信息 + * @returns {Object|null} 用户信息对象或null + */ +function getUserInfo() { + if (!isLoggedIn()) { + return null + } + + const userInfo = wx.getStorageSync('userInfo') + return userInfo || null +} + +/** + * 获取用户ID + * @returns {String|null} 用户ID或null + */ +function getUserId() { + return wx.getStorageSync('userId') || null +} + +/** + * 获取Token + * @returns {String|null} Token或null + */ +function getToken() { + return wx.getStorageSync('token') || null +} + +/** + * 检查登录状态,未登录则跳转登录页 + * @param {Object} options 配置项 + * @param {String} options.returnUrl 登录成功后返回的页面URL + * @param {String} options.returnType 返回类型: navigateTo/redirectTo/switchTab + * @param {Function} options.success 已登录时的回调 + * @param {Function} options.fail 未登录时的回调 + * @returns {Boolean} 是否已登录 + */ +function checkLogin(options = {}) { + const { + returnUrl = '', + returnType = 'navigateTo', + success, + fail + } = options + + if (isLoggedIn()) { + // 已登录 + success && success() + return true + } else { + // 未登录,跳转登录页 + fail && fail() + + // 保存返回信息 + if (returnUrl) { + wx.setStorageSync('returnTo', { + url: returnUrl, + type: returnType + }) + } + + wx.navigateTo({ + url: '/pages/login/login' + }) + + return false + } +} + +/** + * 要求登录后执行操作 + * @param {Function} callback 登录后执行的回调 + * @param {Object} options 配置项 + */ +function requireLogin(callback, options = {}) { + return checkLogin({ + ...options, + success: callback, + fail: () => { + wx.showToast({ + title: '请先登录', + icon: 'none', + duration: 2000 + }) + } + }) +} + +/** + * 保存用户信息到本地 + * @param {Object} data 用户数据 + * @param {String} data.userId 用户ID + * @param {String} data.token Token + * @param {Object} data.userInfo 用户信息 + */ +function saveUserData(data) { + const { userId, token, userInfo } = data + + if (userId) { + wx.setStorageSync('userId', userId) + } + + if (token) { + wx.setStorageSync('token', token) + } + + if (userInfo) { + wx.setStorageSync('userInfo', userInfo) + } +} + +/** + * 清除用户登录信息 + */ +function clearUserData() { + wx.removeStorageSync('userId') + wx.removeStorageSync('token') + wx.removeStorageSync('userInfo') +} + +/** + * 跳转到登录页面(带返回功能) + * 参考 checkin.js 的实现,显示弹窗确认后跳转登录页 + * @param {Object} options 配置项 + * @param {String} options.message 提示信息 + * @param {String} options.returnUrl 登录成功后返回的页面URL(不传则自动获取当前页面) + * @param {String} options.returnType 返回类型: navigateTo/redirectTo/switchTab + * @param {Boolean} options.showCancel 是否显示取消按钮 + * @param {Function} options.onCancel 取消回调 + */ +function navigateToLogin(options = {}) { + const { + message = '请先登录', + returnUrl = '', + returnType = 'navigateTo', + showCancel = true, + onCancel = null + } = options + + // 自动获取当前页面路径 + let finalReturnUrl = returnUrl + if (!finalReturnUrl) { + const pages = getCurrentPages() + if (pages.length > 0) { + const currentPage = pages[pages.length - 1] + finalReturnUrl = '/' + currentPage.route + // 添加页面参数 + if (currentPage.options && Object.keys(currentPage.options).length > 0) { + const params = Object.keys(currentPage.options) + .map(key => `${key}=${currentPage.options[key]}`) + .join('&') + finalReturnUrl += '?' + params + } + } + } + + wx.showModal({ + title: '提示', + content: message, + confirmText: '去登录', + cancelText: '返回', + showCancel: showCancel, + success: (res) => { + if (res.confirm) { + // 保存返回信息,登录成功后返回当前页面 + if (finalReturnUrl) { + wx.setStorageSync('returnTo', { + type: returnType, + url: finalReturnUrl + }) + } + wx.redirectTo({ + url: '/pages/login/login' + }) + } else { + // 点击取消 + if (onCancel) { + onCancel() + } + } + } + }) +} + +/** + * 退出登录 + * @param {Object} options 配置项 + * @param {String} options.redirectUrl 退出后跳转的页面 + * @param {Boolean} options.confirm 是否需要确认 + * @param {Function} options.success 成功回调 + */ +function logout(options = {}) { + const { + redirectUrl = '/pages/home/home', + confirm = true, + success + } = options + + const doLogout = () => { + clearUserData() + + wx.showToast({ + title: '已退出登录', + icon: 'success', + duration: 1500 + }) + + setTimeout(() => { + success && success() + + // 跳转到指定页面 + if (redirectUrl.startsWith('/pages/home/') || + redirectUrl.startsWith('/pages/index/') || + redirectUrl.startsWith('/pages/record/') || + redirectUrl.startsWith('/pages/settings/')) { + wx.reLaunch({ + url: redirectUrl + }) + } else { + wx.redirectTo({ + url: redirectUrl + }) + } + }, 1500) + } + + if (confirm) { + wx.showModal({ + title: '退出登录', + content: '确定要退出登录吗?', + success: (res) => { + if (res.confirm) { + doLogout() + } + } + }) + } else { + doLogout() + } +} + +module.exports = { + isLoggedIn, + getUserInfo, + getUserId, + getToken, + checkLogin, + requireLogin, + navigateToLogin, + saveUserData, + clearUserData, + logout +} diff --git a/utils/config.js b/utils/config.js new file mode 100644 index 0000000..b136e81 --- /dev/null +++ b/utils/config.js @@ -0,0 +1,36 @@ +/** + * API 配置文件 + * 统一管理开发环境和生产环境的配置 + */ + +// 开发模式开关 +const IS_DEV = false + +// 开发环境配置 +const DEV_CONFIG = { + apiBase: 'http://localhost:3001/api', + timeout: 30000, + enableLog: true +} + +// 生产环境配置 +const PROD_CONFIG = { + apiBase: '/mp/jd-huahuazenmehua', // 模块名 + timeout: 30000, + enableLog: false +} + +// 当前环境配置 +const CONFIG = IS_DEV ? DEV_CONFIG : PROD_CONFIG + +module.exports = { + IS_DEV, + API_BASE: CONFIG.apiBase, + TIMEOUT: CONFIG.timeout, + ENABLE_LOG: CONFIG.enableLog, + + // 切换环境方法(用于调试) + switchEnv: (isDev) => { + return isDev ? DEV_CONFIG : PROD_CONFIG + } +} diff --git a/utils/data.js b/utils/data.js new file mode 100644 index 0000000..a3a3278 --- /dev/null +++ b/utils/data.js @@ -0,0 +1,644 @@ +/** + * 静态课程数据 + */ + +// 入门路径卡片 +const BEGINNER_PATH = [ + { + id: 'path_1', + title: '线条练习', + desc: '学会控笔,从直线到曲线', + icon: '✏️', + color: '#6C8CFF', + courseId: 'c_001' + }, + { + id: 'path_2', + title: '形状组合', + desc: '圆形、三角形、方形的变换', + icon: '⬡', + color: '#FFB84D', + courseId: 'c_002' + }, + { + id: 'path_3', + title: '临摹入门', + desc: '跟着范例一步步临摹', + icon: '🖼', + color: '#6CE5A0', + courseId: 'c_003' + }, + { + id: 'path_4', + title: '简单上色', + desc: '认识颜色,学会基础涂色', + icon: '🎨', + color: '#FF7B7B', + courseId: 'c_004' + } +] + +// 课程分类 +const CATEGORIES = ['简笔画', '人物', '动物', '植物', '风景', '素描基础'] + +// 所有课程数据 +const ALL_COURSES = [ + // 简笔画 + { + id: 'c_001', + category: '简笔画', + title: '直线与曲线基础', + cover: '', + coverColor: '#6C8CFF', + coverEmoji: '✏️', + desc: '掌握控笔基础,学会画出流畅的直线和曲线,是一切绘画的起点。', + difficulty: '零基础', + lessons: 5, + duration: '15分钟', + target: '能独立画出均匀流畅的线条', + suitable: '完全没有绘画经验的初学者', + tools: '铅笔、白纸', + steps: [ + { + title: '准备工具', + image: '', + imageEmoji: '📝', + desc: '准备好一支HB铅笔和一张白纸。握笔时手腕放松,不要用力捏住铅笔。', + tip: '💡 手腕放松是画好线条的关键' + }, + { + title: '画水平直线', + image: '', + imageEmoji: '➖', + desc: '从左到右,匀速画出一条水平直线。注意力度均匀,不要停顿。多练习几组,间距保持一致。', + tip: '💡 眼睛看终点,手跟着眼走' + }, + { + title: '画垂直直线', + image: '', + imageEmoji: '|', + desc: '从上往下,画出垂直直线。可以在纸上先标出起点和终点,帮助对齐方向。', + tip: '💡 不要一次画很长,先从短线练起' + }, + { + title: '画弧线', + image: '', + imageEmoji: '⌒', + desc: '以肘关节为轴,画出圆滑的弧线。弧线要圆润,不能有折点。', + tip: '💡 弧线靠手臂运动,不是手腕扭动' + }, + { + title: '综合练习', + image: '', + imageEmoji: '🌊', + desc: '结合直线和弧线,画出波浪形线条。这是检验你控笔能力的最好方式。', + tip: '💡 每天练习5分钟,一周后你会明显进步' + } + ] + }, + { + id: 'c_002', + category: '简笔画', + title: '基础形状练习', + cover: '', + coverColor: '#FFB84D', + coverEmoji: '⬡', + desc: '从圆形、三角形、方形出发,学会用简单形状组合出各种物体。', + difficulty: '零基础', + lessons: 4, + duration: '20分钟', + target: '能用基础形状拼出简单图案', + suitable: '练习过线条的初学者', + tools: '铅笔、橡皮、白纸', + steps: [ + { + title: '画正圆', + image: '', + imageEmoji: '⭕', + desc: '用手腕转动的方式画圆,一笔完成。先画小圆练手感,再逐渐加大。', + tip: '💡 可以用硬币辅助,熟悉圆的弧度感' + }, + { + title: '画正三角形', + image: '', + imageEmoji: '△', + desc: '先画底边,再从两端分别向上延伸,在顶点汇合。注意三条边长度要接近。', + tip: '💡 先轻轻画,满意后再加重' + }, + { + title: '画正方形', + image: '', + imageEmoji: '⬜', + desc: '四条边依次画出,转角处要成直角。可以先画一条参考线保证水平。', + tip: '💡 四个角要90度,不然看起来会歪' + }, + { + title: '形状组合练习', + image: '', + imageEmoji: '🏠', + desc: '用正方形和三角形组合出一栋小房子。圆形变成太阳,长方形变成门。', + tip: '💡 这就是简笔画的基本原理——形状组合' + } + ] + }, + { + id: 'c_003', + category: '简笔画', + title: '临摹入门:太阳花', + cover: '', + coverColor: '#FFE566', + coverEmoji: '🌻', + desc: '跟着步骤一步步临摹一朵向日葵,体验从无到有的成就感。', + difficulty: '入门', + lessons: 6, + duration: '25分钟', + target: '完成一幅向日葵简笔画', + suitable: '已掌握基础形状的初学者', + tools: '铅笔、彩色笔、白纸', + steps: [ + { + title: '画花心圆', + image: '', + imageEmoji: '⭕', + desc: '在纸张中央画一个中等大小的圆形,这是向日葵的花心。', + tip: '💡 花心不用画太大,留出空间给花瓣' + }, + { + title: '添加花瓣', + image: '', + imageEmoji: '🌼', + desc: '围绕花心,均匀画出12-16片椭圆形花瓣。每片花瓣从花心边缘向外延伸。', + tip: '💡 花瓣间距均匀,看起来更好看' + }, + { + title: '画茎和叶', + image: '', + imageEmoji: '🌿', + desc: '从花心底部画一条向下弯曲的茎,两侧加上心形的叶片。', + tip: '💡 茎可以略微弯曲,更自然' + }, + { + title: '添加细节', + image: '', + imageEmoji: '✨', + desc: '在花心内部画出小格子纹理,花瓣上添加几条纹路线。', + tip: '💡 细节不用太多,点到为止' + }, + { + title: '上色:花瓣', + image: '', + imageEmoji: '🟡', + desc: '用黄色给花瓣上色,从花瓣根部向外涂,注意留白产生光泽感。', + tip: '💡 涂色方向统一,颜色更均匀' + }, + { + title: '上色:完成', + image: '', + imageEmoji: '🌻', + desc: '花心涂深棕色,茎叶涂绿色。完成!', + tip: '💡 恭喜你完成了第一幅作品!' + } + ] + }, + { + id: 'c_004', + category: '简笔画', + title: '简单上色技法', + cover: '', + coverColor: '#FF7B7B', + coverEmoji: '🎨', + desc: '学习基础上色方法,让你的画作变得生动有色彩。', + difficulty: '入门', + lessons: 4, + duration: '20分钟', + target: '掌握平涂、渐变两种基本上色技法', + suitable: '完成线稿练习的初学者', + tools: '彩色笔或蜡笔、白纸', + steps: [ + { + title: '认识颜色', + image: '', + imageEmoji: '🌈', + desc: '三原色:红、黄、蓝。它们两两混合产生橙、绿、紫。了解颜色的基本关系。', + tip: '💡 先从最常用的几个颜色开始' + }, + { + title: '平涂练习', + image: '', + imageEmoji: '▪️', + desc: '在一个正方形内,用彩笔均匀平涂。涂色方向保持一致(全部横向或全部竖向)。', + tip: '💡 用力均匀,才能颜色均匀' + }, + { + title: '渐变上色', + image: '', + imageEmoji: '🌅', + desc: '从一侧开始用力涂,向另一侧逐渐减轻力度,产生由深到浅的渐变效果。', + tip: '💡 渐变让画面更有立体感' + }, + { + title: '给简笔画上色', + image: '', + imageEmoji: '🍎', + desc: '用平涂技法给一个苹果线稿上色:主体红色,顶部留白显光泽,底部稍深。', + tip: '💡 留白是让画看起来有光感的秘诀' + } + ] + }, + // 动物 + { + id: 'c_005', + category: '动物', + title: '可爱小猫咪', + cover: '', + coverColor: '#FFB84D', + coverEmoji: '🐱', + desc: '用简单的几何形状画出一只萌萌的小猫,适合零基础入门。', + difficulty: '入门', + lessons: 5, + duration: '20分钟', + target: '完成一幅小猫简笔画', + suitable: '喜欢动物的初学者', + tools: '铅笔、黑色勾线笔、彩色笔', + steps: [ + { + title: '画猫头', + image: '', + imageEmoji: '⭕', + desc: '画一个稍大的圆形作为猫的头部,上方两侧各加一个小三角形作为耳朵。', + tip: '💡 耳朵角度向外微微张开,更可爱' + }, + { + title: '画五官', + image: '', + imageEmoji: '👁', + desc: '画两个大圆眼睛,中间画小椭圆瞳孔。鼻子是小三角形,嘴巴是W形。', + tip: '💡 眼睛大一些,猫咪看起来更萌' + }, + { + title: '画胡须', + image: '', + imageEmoji: '~', + desc: '鼻子两侧各画3根细长的胡须线,要画得自然弯曲。', + tip: '💡 胡须是猫咪最有特色的部分' + }, + { + title: '画身体', + image: '', + imageEmoji: '🐾', + desc: '头部下方画一个椭圆形身体,加上前后四条腿和一条弯曲的尾巴。', + tip: '💡 尾巴末端可以卷起来,更生动' + }, + { + title: '上色完成', + image: '', + imageEmoji: '🐱', + desc: '用橙色或灰色给猫咪上色,耳朵内侧涂粉色,加上几条条纹斑纹。', + tip: '💡 完成!你的第一只猫咪诞生了' + } + ] + }, + { + id: 'c_006', + category: '动物', + title: '萌萌小兔子', + cover: '', + coverColor: '#FF9EC4', + coverEmoji: '🐰', + desc: '画出一只长耳朵可爱兔子,学习动物五官的表达方式。', + difficulty: '入门', + lessons: 4, + duration: '15分钟', + target: '完成一幅兔子简笔画', + suitable: '已有初步控笔能力的初学者', + tools: '铅笔、彩色笔', + steps: [ + { + title: '画兔子头和耳朵', + image: '', + imageEmoji: '🐰', + desc: '画圆形头部,顶部画两个细长的竖耳朵(椭圆形),耳朵比头稍长。', + tip: '💡 长耳朵是兔子最显眼的特征' + }, + { + title: '画五官', + image: '', + imageEmoji: '👀', + desc: '两个圆眼睛,小圆鼻子,嘴巴是"Y"形(两片嘴唇)。腮部加两个小圆圈。', + tip: '💡 腮红让兔子更可爱' + }, + { + title: '画身体', + image: '', + imageEmoji: '🫁', + desc: '圆润的椭圆形身体,短短的四肢,背面有一个小圆尾巴。', + tip: '💡 兔子身体圆润,不要画得太尖' + }, + { + title: '上色完成', + image: '', + imageEmoji: '🐇', + desc: '白色兔子留白,耳朵内侧和腮红涂粉色,眼睛可以涂红色或蓝色。', + tip: '💡 白色兔子的轮廓线用浅灰色更好看' + } + ] + }, + // 植物 + { + id: 'c_007', + category: '植物', + title: '多肉植物', + cover: '', + coverColor: '#6CE5A0', + coverEmoji: '🪴', + desc: '画出可爱的多肉植物,学习植物形态的表达和重叠关系。', + difficulty: '入门', + lessons: 5, + duration: '20分钟', + target: '完成一盆多肉植物图案', + suitable: '喜欢植物的初学者', + tools: '铅笔、绿色系彩笔', + steps: [ + { + title: '画花盆', + image: '', + imageEmoji: '🪣', + desc: '画一个梯形花盆:上宽下窄,底部加一条横线表示盆底,两侧弧度自然。', + tip: '💡 花盆大小要和上方植物匹配' + }, + { + title: '画中心叶片', + image: '', + imageEmoji: '🌿', + desc: '在花盆上方中央画一片椭圆形叶片,尖端向上,这是多肉的最顶部。', + tip: '💡 叶片要厚实饱满,多肉的特征' + }, + { + title: '添加外层叶片', + image: '', + imageEmoji: '🍃', + desc: '围绕中心叶片,向外交错排列6-8片叶片,越外层越大越向外张开。', + tip: '💡 叶片之间稍微重叠,有层次感' + }, + { + title: '添加细节', + image: '', + imageEmoji: '✨', + desc: '每片叶片中间画一条中脉,花盆上画几条纹路。', + tip: '💡 细节不用多,一两条线就够了' + }, + { + title: '上色完成', + image: '', + imageEmoji: '🪴', + desc: '叶片涂绿色,叶尖可以点一点红色或紫色(多肉晒红的效果),花盆涂浅棕色。', + tip: '💡 多肉叶尖的颜色变化是亮点' + } + ] + }, + // 人物 + { + id: 'c_008', + category: '人物', + title: '简笔小人基础', + cover: '', + coverColor: '#A78BFA', + coverEmoji: '🧍', + desc: '学会画一个基础的简笔小人,掌握人体比例关系。', + difficulty: '入门', + lessons: 5, + duration: '20分钟', + target: '画出比例协调的简笔小人', + suitable: '想学画人物的初学者', + tools: '铅笔、彩色笔', + steps: [ + { + title: '画头部', + image: '', + imageEmoji: '😶', + desc: '画一个圆形头部,大小适中。简笔画中头部约占全身的1/6。', + tip: '💡 先确定好头的大小,其他部分按比例来' + }, + { + title: '画躯干', + image: '', + imageEmoji: '🫀', + desc: '从脖子向下画一个长方形躯干,高度约为头的2倍。肩部略宽,腰部略窄。', + tip: '💡 躯干是人体的核心,要画得端正' + }, + { + title: '画手臂', + image: '', + imageEmoji: '💪', + desc: '从肩部向下画两条手臂,末端加上简单的手形(可以是手套形)。', + tip: '💡 手臂自然下垂时,手腕在腰部左右' + }, + { + title: '画腿和脚', + image: '', + imageEmoji: '🦵', + desc: '从腰部向下画两条腿,略比手臂粗。末端画简单的椭圆形鞋子。', + tip: '💡 腿的长度约为躯干的1.5倍' + }, + { + title: '添加五官和服装', + image: '', + imageEmoji: '🧍', + desc: '给头部添加简单五官,躯干部分画上衬衫领口和口袋等细节。', + tip: '💡 简笔小人不必精细,可爱就够了' + } + ] + }, + // 风景 + { + id: 'c_009', + category: '风景', + title: '简单风景:晴天', + cover: '', + coverColor: '#87CEEB', + coverEmoji: '🌤', + desc: '画出一幅包含天空、山丘和草地的简单风景,学习风景构图基础。', + difficulty: '入门', + lessons: 5, + duration: '25分钟', + target: '完成一幅简单的晴天风景画', + suitable: '想学风景画的初学者', + tools: '铅笔、彩色笔或蜡笔', + steps: [ + { + title: '画地平线', + image: '', + imageEmoji: '➖', + desc: '在纸张约2/3高度处画一条水平线作为地平线,上方是天空,下方是地面。', + tip: '💡 地平线的高低决定了画面的空间感' + }, + { + title: '画远山', + image: '', + imageEmoji: '⛰', + desc: '在地平线上方画几个大小不一的弧形山丘,前后叠加产生远近感。', + tip: '💡 远处的山要画得小一些、颜色淡一些' + }, + { + title: '画太阳和云', + image: '', + imageEmoji: '☀️', + desc: '右上角画一个圆形太阳,周围加短线条表示光芒。画几朵简单的棉花云。', + tip: '💡 云的形状:多个小圆形叠在一起' + }, + { + title: '画草地和树', + image: '', + imageEmoji: '🌲', + desc: '地平线下方涂绿色草地,加几棵三角形松树和圆形树冠的树。', + tip: '💡 树的大小和远近要有变化' + }, + { + title: '上色完成', + image: '', + imageEmoji: '🌄', + desc: '天空涂浅蓝色,山丘涂蓝绿色,草地涂绿色,太阳涂黄色。', + tip: '💡 颜色可以从浅到深,层次更丰富' + } + ] + }, + // 素描基础 + { + id: 'c_010', + category: '素描基础', + title: '排线入门', + cover: '', + coverColor: '#888888', + coverEmoji: '📐', + desc: '学习素描最基础的排线技法,这是素描的核心功夫。', + difficulty: '入门', + lessons: 4, + duration: '20分钟', + target: '掌握均匀排线的基本方法', + suitable: '想系统学习素描的初学者', + tools: 'HB/2B铅笔、素描纸', + steps: [ + { + title: '认识铅笔硬度', + image: '', + imageEmoji: '✏️', + desc: 'H系列越硬线条越细浅,B系列越软线条越粗深。初学者用HB或2B最合适。', + tip: '💡 同一支笔,用力不同也能画出深浅变化' + }, + { + title: '单向排线练习', + image: '', + imageEmoji: '|||', + desc: '均匀画出平行的斜线,线条之间间距相等,粗细相同。从左到右,不回笔。', + tip: '💡 排线要平行,不能交叉弯曲' + }, + { + title: '交叉排线', + image: '', + imageEmoji: '###', + desc: '在第一层排线上,换一个角度叠加第二层排线,形成交叉网格效果。', + tip: '💡 交叉排线可以制造丰富的明暗层次' + }, + { + title: '渐变调子', + image: '', + imageEmoji: '▓', + desc: '用排线的疏密变化制造渐变:左侧排线密(颜色深),向右逐渐变疏(颜色浅)。', + tip: '💡 素描的光影就是靠排线疏密来表现的' + } + ] + } +] + +// 今日推荐(取前3个) +const DAILY_RECOMMEND = ALL_COURSES.slice(0, 3) + +// 练习任务数据 +const PRACTICE_TASKS = [ + { + id: 'pt_001', + title: '直线描线练习', + type: '描线练习', + difficulty: '入门', + duration: '5分钟', + courseId: 'c_001', + stepIndex: 1, + icon: '➖', + iconColor: '#6C8CFF', + desc: '画100条均匀的水平直线,感受控笔节奏' + }, + { + id: 'pt_002', + title: '圆形临摹练习', + type: '形状练习', + difficulty: '入门', + duration: '5分钟', + courseId: 'c_002', + stepIndex: 0, + icon: '⭕', + iconColor: '#FFB84D', + desc: '连续画50个大小不一的圆形,提升圆弧控制能力' + }, + { + id: 'pt_003', + title: '向日葵临摹', + type: '临摹练习', + difficulty: '初级', + duration: '15分钟', + courseId: 'c_003', + stepIndex: 0, + icon: '🌻', + iconColor: '#FFE566', + desc: '完整临摹一朵向日葵,综合线条和形状能力' + }, + { + id: 'pt_004', + title: '小猫描线', + type: '描线练习', + difficulty: '初级', + duration: '10分钟', + courseId: 'c_005', + stepIndex: 0, + icon: '🐱', + iconColor: '#FFB84D', + desc: '对照范例,描出小猫的轮廓线' + }, + { + id: 'pt_005', + title: '多肉形状组合', + type: '形状练习', + difficulty: '初级', + duration: '10分钟', + courseId: 'c_007', + stepIndex: 1, + icon: '🪴', + iconColor: '#6CE5A0', + desc: '用椭圆形叶片组合画出多肉植物' + }, + { + id: 'pt_006', + title: '素描排线', + type: '描线练习', + difficulty: '初级', + duration: '10分钟', + courseId: 'c_010', + stepIndex: 1, + icon: '📐', + iconColor: '#888888', + desc: '完成单向排线和交叉排线各一组' + } +] + +module.exports = { + BEGINNER_PATH, + CATEGORIES, + ALL_COURSES, + DAILY_RECOMMEND, + PRACTICE_TASKS, + getCourseById(id) { + return ALL_COURSES.find(c => c.id === id) || null + }, + getCoursesByCategory(category) { + return ALL_COURSES.filter(c => c.category === category) + } +} diff --git a/utils/httpClient.js b/utils/httpClient.js new file mode 100644 index 0000000..36c1ddc --- /dev/null +++ b/utils/httpClient.js @@ -0,0 +1,88 @@ +/** + * 统一的 HTTP 客户端 + * 自动根据环境切换使用 gatewayHttpClient 或 HttpClient + */ + +import { gatewayHttpClient, HttpClient } from '@jdmini/api' +const { IS_DEV, API_BASE } = require('./config.js') + +// 开发环境使用 HttpClient +const devHttpClient = IS_DEV ? new HttpClient({ + baseURL: API_BASE, + timeout: 30000, +}) : null + +/** + * 统一的 HTTP 请求方法 + * @param {String} url - 请求路径(相对路径,不包含 baseURL) + * @param {String} method - 请求方法 GET/POST/PUT/DELETE + * @param {Object} data - 请求数据 + * @param {Object} options - 额外选项 + * @returns {Promise} + */ +function request(url, method = 'GET', data = {}, options = {}) { + // 确保 URL 以 / 开头 + const apiUrl = url.startsWith('/') ? url : `/${url}` + + if (IS_DEV) { + // 开发环境:使用 HttpClient + console.log(`[DEV] ${method} ${API_BASE}${apiUrl}`, data) + return devHttpClient.request(apiUrl, method, data, options) + } else { + // 生产环境:使用 gatewayHttpClient + // gatewayHttpClient 需要完整的路径:mp/jd-haiba/user/login + const fullUrl = `${API_BASE}${apiUrl}` + console.log(`[PROD] ${method} ${fullUrl}`, data) + + // gatewayHttpClient.request 的参数顺序:(url, method, data, options) + return gatewayHttpClient.request(fullUrl, method, data, options) + } +} + +/** + * GET 请求 + */ +function get(url, params = {}, options = {}) { + return request(url, 'GET', params, options) +} + +/** + * POST 请求 + */ +function post(url, data = {}, options = {}) { + return request(url, 'POST', data, options) +} + +/** + * PUT 请求 + */ +function put(url, data = {}, options = {}) { + return request(url, 'PUT', data, options) +} + +/** + * DELETE 请求 + */ +function del(url, data = {}, options = {}) { + return request(url, 'DELETE', data, options) +} + +/** + * 上传文件(暂时保留原有逻辑) + */ +function upload(filePath, formData = {}) { + return gatewayHttpClient.uploadFile(filePath,formData) +} + +module.exports = { + request, + get, + post, + put, + del, + upload, + + // 导出原始客户端供特殊场景使用 + gatewayHttpClient, + devHttpClient +} diff --git a/utils/index.js b/utils/index.js new file mode 100644 index 0000000..4922715 --- /dev/null +++ b/utils/index.js @@ -0,0 +1,27 @@ +/** + * Utils 统一导出 + * 方便各个页面统一引入 + */ + +const config = require('./config.js') +const request = require('./request.js') +const api = require('./api.js') +const auth = require('./auth.js') +const httpClient = require('./httpClient.js') + +module.exports = { + // 配置 + ...config, + + // 请求方法 + ...request, + + // API接口 + ...api, + + // 认证 + ...auth, + + // 统一 HTTP 客户端 + httpClient +} diff --git a/utils/request.js b/utils/request.js new file mode 100644 index 0000000..7d8a16b --- /dev/null +++ b/utils/request.js @@ -0,0 +1,278 @@ +/** + * 统一请求模块 + * 封装所有 API 请求,支持拦截器、错误处理等 + */ + +const { API_BASE, TIMEOUT, ENABLE_LOG, IS_DEV } = require('./config.js') +const httpClient = require('./httpClient.js') + +/** + * 统一请求方法 + * @param {Object} options 请求配置 + * @param {String} options.url 请求路径(相对路径,会自动拼接 API_BASE) + * @param {String} options.method 请求方法 GET/POST/PUT/DELETE + * @param {Object} options.data 请求数据 + * @param {Object} options.header 自定义请求头 + * @param {Boolean} options.showLoading 是否显示加载提示 + * @param {String} options.loadingText 加载提示文字 + * @param {Boolean} options.showError 是否显示错误提示 + * @returns {Promise} + */ +function request(options = {}) { + const { + url, + method = 'GET', + data = {}, + header = {}, + showLoading = false, + loadingText = '加载中...', + showError = true + } = options + + // 显示加载提示 + if (showLoading) { + wx.showLoading({ + title: loadingText, + mask: true + }) + } + + // 拼接完整URL + const fullUrl = url.startsWith('http') ? url : `${API_BASE}${url}` + + // 构建请求头 + const requestHeader = { + 'Content-Type': 'application/json', + ...header + } + + // 添加 token(如果存在) + const token = wx.getStorageSync('token') + if (token) { + requestHeader['Authorization'] = `Bearer ${token}` + } + + // 日志输出 + if (ENABLE_LOG) { + console.log('[API Request]', { + url: fullUrl, + method, + data, + header: requestHeader + }) + } + + return new Promise((resolve, reject) => { + wx.request({ + url: fullUrl, + method, + data, + header: requestHeader, + timeout: TIMEOUT, + success: (res) => { + // 隐藏加载提示 + if (showLoading) { + wx.hideLoading() + } + + // 日志输出 + if (ENABLE_LOG) { + console.log('[API Response]', res.data) + } + + // 统一处理响应 + const { statusCode, data: resData } = res + + // HTTP 状态码检查 + if (statusCode >= 200 && statusCode < 300) { + // 业务状态码检查 + if (resData.code === 200 || resData.success === true) { + resolve(resData) + } else { + // 业务错误 + const errorMsg = resData.msg || resData.message || '请求失败' + if (showError) { + wx.showToast({ + title: errorMsg, + icon: 'none', + duration: 2000 + }) + } + reject({ + code: resData.code, + msg: errorMsg, + data: resData + }) + } + } else { + // HTTP 错误 + const errorMsg = `服务器错误 (${statusCode})` + if (showError) { + wx.showToast({ + title: errorMsg, + icon: 'none', + duration: 2000 + }) + } + reject({ + code: statusCode, + msg: errorMsg, + data: res + }) + } + }, + fail: (err) => { + // 隐藏加载提示 + if (showLoading) { + wx.hideLoading() + } + + // 日志输出 + if (ENABLE_LOG) { + console.error('[API Error]', err) + } + + // 网络错误 + const errorMsg = err.errMsg || '网络请求失败' + if (showError) { + wx.showToast({ + title: errorMsg, + icon: 'none', + duration: 2000 + }) + } + + reject({ + code: -1, + msg: errorMsg, + data: err + }) + } + }) + }) +} + +/** + * GET 请求 + */ +function get(url, data = {}, options = {}) { + // 直接使用 httpClient,它会自动处理环境切换 + return httpClient.get(url, data, options) +} + +/** + * POST 请求 + */ +function post(url, data = {}, options = {}) { + // 直接使用 httpClient,它会自动处理环境切换 + return httpClient.post(url, data, options) +} + +/** + * PUT 请求 + */ +function put(url, data = {}, options = {}) { + // 直接使用 httpClient,它会自动处理环境切换 + return httpClient.put(url, data, options) +} + +/** + * DELETE 请求 + */ +function del(url, data = {}, options = {}) { + // 直接使用 httpClient,它会自动处理环境切换 + return httpClient.del(url, data, options) +} + +/** + * 上传文件 + * @param {String} url 上传地址 + * @param {String} filePath 文件路径 + * @param {Object} formData 额外的表单数据 + * @param {Object} options 其他配置 + */ +function uploadFile(filePath, formData = {}) { + const { + name = 'file', + showLoading = true, + loadingText = '上传中...', + showError = true + } = options + + if (showLoading) { + wx.showLoading({ + title: loadingText, + mask: true + }) + } + + const fullUrl = url.startsWith('http') ? url : `${API_BASE}${url}` + + // 构建请求头 + const header = {} + const token = wx.getStorageSync('token') + if (token) { + header['Authorization'] = `Bearer ${token}` + } + + return new Promise((resolve, reject) => { + wx.uploadFile({ + url: fullUrl, + filePath, + name, + formData, + header, + timeout: TIMEOUT, + success: (res) => { + if (showLoading) { + wx.hideLoading() + } + + const data = JSON.parse(res.data) + if (data.code === 200 || data.success === true) { + resolve(data) + } else { + const errorMsg = data.msg || data.message || '上传失败' + if (showError) { + wx.showToast({ + title: errorMsg, + icon: 'none' + }) + } + reject({ + code: data.code, + msg: errorMsg, + data + }) + } + }, + fail: (err) => { + if (showLoading) { + wx.hideLoading() + } + + const errorMsg = err.errMsg || '上传失败' + if (showError) { + wx.showToast({ + title: errorMsg, + icon: 'none' + }) + } + reject({ + code: -1, + msg: errorMsg, + data: err + }) + } + }) + }) +} + +module.exports = { + request, + get, + post, + put, + del, + uploadFile +} diff --git a/utils/storage.js b/utils/storage.js new file mode 100644 index 0000000..9a63280 --- /dev/null +++ b/utils/storage.js @@ -0,0 +1,148 @@ +/** + * 本地存储工具 + * storage key 设计: + * draw_learning_progress - 学习进度 + * draw_recent_history - 最近学习记录 + * draw_favorite_courses - 收藏课程 + * draw_works_records - 作品上传记录 + * draw_user_settings - 用户设置 + */ + +const KEYS = { + PROGRESS: 'draw_learning_progress', + HISTORY: 'draw_recent_history', + FAVORITE: 'draw_favorite_courses', + WORKS: 'draw_works_records', + SETTINGS: 'draw_user_settings', +} + +// --------- 学习进度 --------- +// 格式:{ [courseId]: { currentStep: 0, completed: false, updatedAt: timestamp } } +function getProgress() { + return wx.getStorageSync(KEYS.PROGRESS) || {} +} + +function saveProgress(courseId, stepIndex, completed = false) { + const progress = getProgress() + progress[courseId] = { + currentStep: stepIndex, + completed, + updatedAt: Date.now() + } + wx.setStorageSync(KEYS.PROGRESS, progress) +} + +function getCourseProgress(courseId) { + const progress = getProgress() + return progress[courseId] || { currentStep: 0, completed: false } +} + +function getCompletedCount() { + const progress = getProgress() + return Object.values(progress).filter(p => p.completed).length +} + +// --------- 最近学习记录 --------- +// 格式:[{ courseId, courseTitle, coverEmoji, coverColor, stepIndex, timestamp }] +function getRecentHistory() { + return wx.getStorageSync(KEYS.HISTORY) || [] +} + +function addRecentHistory(courseInfo) { + let history = getRecentHistory() + // 去重 + history = history.filter(h => h.courseId !== courseInfo.courseId) + history.unshift({ ...courseInfo, timestamp: Date.now() }) + // 最多保留10条 + history = history.slice(0, 10) + wx.setStorageSync(KEYS.HISTORY, history) +} + +// --------- 收藏课程 --------- +// 格式:[courseId] +function getFavorites() { + return wx.getStorageSync(KEYS.FAVORITE) || [] +} + +function toggleFavorite(courseId) { + let favorites = getFavorites() + const idx = favorites.indexOf(courseId) + if (idx >= 0) { + favorites.splice(idx, 1) + } else { + favorites.unshift(courseId) + } + wx.setStorageSync(KEYS.FAVORITE, favorites) + return idx < 0 // true=已收藏 +} + +function isFavorite(courseId) { + return getFavorites().includes(courseId) +} + +// --------- 作品记录 --------- +// 格式:[{ id, courseId, courseTitle, imagePath, savedAt }] +function getWorksRecords() { + return wx.getStorageSync(KEYS.WORKS) || [] +} + +function addWorkRecord(record) { + let records = getWorksRecords() + records.unshift({ ...record, savedAt: Date.now(), id: Date.now().toString() }) + wx.setStorageSync(KEYS.WORKS, records) +} + +function getCompletedPracticeCount() { + return getWorksRecords().length +} + +// --------- 用户设置 --------- +function getSettings() { + return wx.getStorageSync(KEYS.SETTINGS) || { + reminderEnabled: false, + reminderTime: '08:00', + nickname: '学画的你', + avatar: '', + joinDays: 0, + joinDate: null + } +} + +function saveSettings(settings) { + const current = getSettings() + const merged = { ...current, ...settings } + // 计算学习天数 + if (!merged.joinDate) { + merged.joinDate = Date.now() + merged.joinDays = 1 + } else { + merged.joinDays = Math.max(1, Math.ceil((Date.now() - merged.joinDate) / 86400000)) + } + wx.setStorageSync(KEYS.SETTINGS, merged) + return merged +} + +function initSettings() { + const settings = getSettings() + if (!settings.joinDate) { + saveSettings({}) + } + return getSettings() +} + +module.exports = { + saveProgress, + getCourseProgress, + getCompletedCount, + getRecentHistory, + addRecentHistory, + getFavorites, + toggleFavorite, + isFavorite, + getWorksRecords, + addWorkRecord, + getCompletedPracticeCount, + getSettings, + saveSettings, + initSettings +}