commit f457c1d04cdaa7195775c5c92254e51319fdedc9 Author: honghefly Date: Wed Jan 14 15:33:15 2026 +0800 first commit diff --git a/app.js b/app.js new file mode 100644 index 0000000..b66271e --- /dev/null +++ b/app.js @@ -0,0 +1,219 @@ +import { injectApp, waitLogin, gatewayHttpClient } from '@jdmini/api' +App(injectApp()({ + globalData: { + userInfo: null, + openid: null, + wxUserInfo: null, + inviterId: null // 邀请人ID + }, + async onLaunch(options) { + // 保存邀请人ID + if (options && options.query && options.query.inviter) { + this.globalData.inviterId = options.query.inviter + wx.setStorageSync('inviterId', options.query.inviter) + } + 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(); + } + } + }); + }); + } + }); + } + const accountInfo = wx.getExtConfigSync() + //console.log('#1',accountInfo) + const appId = accountInfo.appId; + // 等待登陆完成以后请求自动携带token + await waitLogin() + // 三方登录本地缓存 + console.log(wx.getStorageSync('jdwx-userinfo')) + const wxUserInfo = wx.getStorageSync('jdwx-userinfo'); + wx.setStorageSync('sfUserId', wxUserInfo.id) + wx.setStorageSync('appId',appId) + // 保存到全局 + this.globalData.openid = wxUserInfo.openId; + this.globalData.wxUserInfo = wxUserInfo; + const userId = wx.getStorageSync('userId'); + if (userId) { + console.log('用户已登录', userId); + } else { + console.log('用户未设置个人信息,需要跳转登录页'); + } + //this.getqrcode('logo=1') + }, + // 生成小程序二维码 + async getqrcode(value) { + try { + // 构建场景值:仓库ID和仓库code + const scene = value; + + console.log('生成二维码请求参数:', { + scene, + page: "pages/index/index" + }); + + const result = await gatewayHttpClient.request('/wx/v1/api/app/qrcode', 'POST', { + "scene": scene, + "page": "pages/index/index", // 跳转到index页面 + "check_path": true, + "env_version": "release" + },{ + responseType: 'arraybuffer' + }); + + console.log('二维码接口返回:', result); + console.log('返回数据类型:', typeof result); + console.log('是否为ArrayBuffer:', result instanceof ArrayBuffer); + + // 当设置 responseType: 'arraybuffer' 时,返回的直接就是 ArrayBuffer + if (result instanceof ArrayBuffer) { + console.log('检测到 ArrayBuffer,转换为 base64'); + const base64Data = wx.arrayBufferToBase64(result); + console.log('转换后的 base64 数据长度:', base64Data?.length); + console.log('base64 数据前50个字符:', base64Data?.substring(0, 50)); + + return { + success: true, + data: base64Data // 二维码图片数据(base64字符串) + }; + } else { + console.error('二维码生成失败,返回数据不是ArrayBuffer:', result); + return { + success: false, + message: '生成二维码失败:数据格式错误' + }; + } + } catch (error) { + console.error('生成二维码异常:', error); + return { + success: false, + message: error.message || '生成二维码失败' + }; + } + }, + //内容安全 + 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 { + console.error('内容检测API调用失败:', data) + return 2 + } + } catch (error) { + console.error('checkdata函数执行错误:', error) + return 2 + } + }, +// 轮询检查图片是否合规 +//imgurl 为本地图片地址 +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)); // 间隔1秒 + + const passcode = await this.checkSafetyResults(checkid); + + // 检查具体的错误码 + switch (passcode) { + case 100: + wx.hideLoading(); + return upfileData.data; + case 20001: + wx.hideLoading(); + await gatewayHttpClient.deleteFile(upfileData.data.id); + this.showwarning('图片时政内容,请重新选择'); + return null; + case 20002: + wx.hideLoading(); + await gatewayHttpClient.deleteFile(upfileData.data.id); + this.showwarning('图片含有色情内容,请重新选择'); + return null; + case 20006: + wx.hideLoading(); + await gatewayHttpClient.deleteFile(upfileData.data.id); + this.showwarning('图片含有违法犯罪内容,请重新选择'); + return null; + case 21000: + wx.hideLoading(); + await gatewayHttpClient.deleteFile(upfileData.data.id); + this.showwarning('图片非法内容,请重新选择'); + return null; + default: + break + } + retryCount++; + } + + // 5次超时,返回失败 + wx.hideLoading(); + await gatewayHttpClient.deleteFile(upfileData.data.id)//删除图片 + wx.showToast({ + title: '图片检查超时,请重试', + icon: 'none' + }); + return null; + } catch (error) { + wx.hideLoading(); + console.error('图片检查失败:', error); + wx.showToast({ + title: '检查失败,请重试', + icon: 'none' + }); + return null; + } +}, +showwarning(txt){ + wx.showModal({ + title: '提示', + content: txt, + showCancel: false, + success: () => { + } + }); +} +})) diff --git a/app.json b/app.json new file mode 100644 index 0000000..067ee53 --- /dev/null +++ b/app.json @@ -0,0 +1,53 @@ +{ + "pages": [ + "pages/index/index", + "pages/category/category", + "pages/category-detail/category-detail", + "pages/detail/detail", + "pages/download/download", + "pages/mine/mine", + "pages/search/search", + "pages/login/login" + ], + "window": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "儿童练习表", + "navigationBarBackgroundColor": "#ffffff" + }, + "tabBar": { + "color": "#999999", + "selectedColor": "#4CAF50", + "backgroundColor": "#ffffff", + "borderStyle": "black", + "list": [ + { + "pagePath": "pages/index/index", + "text": "首页", + "iconPath": "images/tabbar/home.png", + "selectedIconPath": "images/tabbar/home-active.png" + }, + { + "pagePath": "pages/category/category", + "text": "分类", + "iconPath": "images/tabbar/category.png", + "selectedIconPath": "images/tabbar/category-active.png" + }, + { + "pagePath": "pages/download/download", + "text": "下载", + "iconPath": "images/tabbar/download.png", + "selectedIconPath": "images/tabbar/download-active.png" + }, + { + "pagePath": "pages/mine/mine", + "text": "我的", + "iconPath": "images/tabbar/mine.png", + "selectedIconPath": "images/tabbar/mine-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..e7ad8ec --- /dev/null +++ b/app.wxss @@ -0,0 +1,60 @@ +/* 全局样式 */ +page { + background-color: #f5f5f5; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-size: 28rpx; + color: #333; + box-sizing: border-box; +} + +/* 清除默认边距 */ +view, text, image { + box-sizing: border-box; +} + +/* 常用flex布局 */ +.flex { + display: flex; +} + +.flex-center { + display: flex; + align-items: center; + justify-content: center; +} + +.flex-between { + display: flex; + align-items: center; + justify-content: space-between; +} + +.flex-wrap { + display: flex; + flex-wrap: wrap; +} + +.flex-column { + display: flex; + flex-direction: column; +} + +/* 文本省略 */ +.text-ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.text-ellipsis-2 { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +/* 安全区域底部内边距 */ +.safe-area-bottom { + padding-bottom: env(safe-area-inset-bottom); +} diff --git a/images/README.md b/images/README.md new file mode 100644 index 0000000..ea258ad --- /dev/null +++ b/images/README.md @@ -0,0 +1,55 @@ +# 图片资源说明 + +## 需要准备的图片资源 + +### TabBar 图标 (images/tabbar/) +- home.png / home-active.png - 首页图标 +- category.png / category-active.png - 分类图标 +- download.png / download-active.png - 下载图标 +- mine.png / mine-active.png - 我的图标 + +建议尺寸: 81x81px (实际显示约 27x27pt) + +### 功能图标 (images/icons/) +- search.png - 搜索图标 +- arrow-right.png - 右箭头 +- preview.png - 预览图标 +- download.png - 下载图标 +- print.png - 打印图标 +- open.png - 打开文件图标 +- delete.png - 删除图标 +- clear.png - 清除图标 +- empty.png - 空状态图标 +- empty-download.png - 下载空状态图标 +- empty-search.png - 搜索空状态图标 +- download-menu.png - 菜单下载图标 +- share.png - 分享图标 +- feedback.png - 反馈图标 +- about.png - 关于图标 + +建议尺寸: 48x48px 或 64x64px + +### Banner 图片 (images/banner/) +- banner1.png - 轮播图1 +- banner2.png - 轮播图2 +- banner3.png - 轮播图3 + +建议尺寸: 690x280rpx (可适当调整) + +### 默认图片 +- default-avatar.png - 默认头像 + +建议尺寸: 120x120px + +## 图标建议 + +可使用以下免费图标库: +1. iconfont.cn (阿里图标库) +2. Material Design Icons +3. Feather Icons +4. Heroicons + +图标风格建议: +- 线性图标,线宽 2px +- 颜色: 未选中 #999999, 选中 #4CAF50 +- 格式: PNG (带透明背景) 或 SVG diff --git a/images/icons/about.png b/images/icons/about.png new file mode 100644 index 0000000..dc04e45 Binary files /dev/null and b/images/icons/about.png differ diff --git a/images/icons/arrow-right.png b/images/icons/arrow-right.png new file mode 100644 index 0000000..8a143ba Binary files /dev/null and b/images/icons/arrow-right.png differ diff --git a/images/icons/clear.png b/images/icons/clear.png new file mode 100644 index 0000000..42a015d Binary files /dev/null and b/images/icons/clear.png differ diff --git a/images/icons/download-menu.png b/images/icons/download-menu.png new file mode 100644 index 0000000..3efaaf2 Binary files /dev/null and b/images/icons/download-menu.png differ diff --git a/images/icons/empty-download.png b/images/icons/empty-download.png new file mode 100644 index 0000000..be82b78 Binary files /dev/null and b/images/icons/empty-download.png differ diff --git a/images/icons/search.png b/images/icons/search.png new file mode 100644 index 0000000..f5de589 Binary files /dev/null and b/images/icons/search.png differ diff --git a/images/icons/share.png b/images/icons/share.png new file mode 100644 index 0000000..2044c0f Binary files /dev/null and b/images/icons/share.png differ diff --git a/images/tabbar/category-active.png b/images/tabbar/category-active.png new file mode 100644 index 0000000..0982be6 Binary files /dev/null and b/images/tabbar/category-active.png differ diff --git a/images/tabbar/category.png b/images/tabbar/category.png new file mode 100644 index 0000000..237d843 Binary files /dev/null and b/images/tabbar/category.png differ diff --git a/images/tabbar/download-active.png b/images/tabbar/download-active.png new file mode 100644 index 0000000..6d4c061 Binary files /dev/null and b/images/tabbar/download-active.png differ diff --git a/images/tabbar/download.png b/images/tabbar/download.png new file mode 100644 index 0000000..c5bad93 Binary files /dev/null and b/images/tabbar/download.png differ diff --git a/images/tabbar/home-active.png b/images/tabbar/home-active.png new file mode 100644 index 0000000..2e2bbf9 Binary files /dev/null and b/images/tabbar/home-active.png differ diff --git a/images/tabbar/home.png b/images/tabbar/home.png new file mode 100644 index 0000000..9fa7332 Binary files /dev/null and b/images/tabbar/home.png differ diff --git a/images/tabbar/mine-active.png b/images/tabbar/mine-active.png new file mode 100644 index 0000000..dc5d033 Binary files /dev/null and b/images/tabbar/mine-active.png differ diff --git a/images/tabbar/mine.png b/images/tabbar/mine.png new file mode 100644 index 0000000..b0ca71e Binary files /dev/null and b/images/tabbar/mine.png differ diff --git a/images/tmpclaude-ecff-cwd b/images/tmpclaude-ecff-cwd new file mode 100644 index 0000000..067cda1 --- /dev/null +++ b/images/tmpclaude-ecff-cwd @@ -0,0 +1 @@ +/d/code/youerqimeng-server/miniapp/images 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;n { + 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/node_modules/.package-lock.json b/node_modules/.package-lock.json new file mode 100644 index 0000000..e3fe0ce --- /dev/null +++ b/node_modules/.package-lock.json @@ -0,0 +1,20 @@ +{ + "name": "template", + "lockfileVersion": 3, + "requires": true, + "packages": { + "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/node_modules/@jdmini/api/README.md b/node_modules/@jdmini/api/README.md new file mode 100644 index 0000000..a82f0ec --- /dev/null +++ b/node_modules/@jdmini/api/README.md @@ -0,0 +1,252 @@ +## 安装/更新 + +1、终端使用命令安装 npm 包 + +```bash +npm install --save @jdmini/api@latest +``` + +> ```bash +> npm install --save @jdmini/components@latest +> ``` + +2、在小程序开发者工具中:菜单选择工具 -> 构建 npm + +## 使用 + +```js +import { + onLoginReady, + waitLogin, + + injectApp, + injectPage, + injectComponent, + hijackApp, + hijackAllPage, + + gatewayHttpClient, + baseHttpClient, + apiHttpClient, + HttpClient, + + adManager, +} from '@jdmini/api' +``` + +### `waitLogin`/`onLoginReady` - 确保登录完成 + +- 同步的写法 + +```js +async function onLoad() { + await waitLogin() + // 此处代码将在已登录或登陆完成后执行。请求将自动携带Token + await gatewayHttpClient.request('/xxx', 'GET', {}) +} +``` + +- 异步的写法 + +```js +function onLoad() { + onLoginReady(() => { + // 此处代码将在已登录或登陆完成后执行。请求将自动携带Token + gatewayHttpClient.request('/xxx', 'GET', {}) + }) +} +``` + +### `injectApp` - 向App注入基础代码 + +- 注入之后实现自动登录、广告初始化等功能 + +```js +// app.js +App(injectApp()({ + // 业务代码 + onLaunch() { + + } +})) +``` + +### `injectPage` - 向Page注入基础代码 + +- 注入之后实现页面自动统计、自动展示插屏广告以及激励视频广告的调用支持 +- 参数: + - showInterstitialAd: 是否自动展示插屏广告 + +```js +// pages/xxx/xxx.js +Page(injectPage({ + showInterstitialAd: true +})({ + // 业务代码 + onLoad() { + + } +})) +``` + +### `injectComponent` - 向Component注入基础代码 + +- 适用于使用Component构造页面的场景 +- 注入之后实现页面自动统计、自动展示插屏广告以及激励视频广告的调用支持 +- 参数: + - showInterstitialAd: 是否自动展示插屏广告 + +```js +// pages/xxx/xxx.js +Component(injectComponent({ + showInterstitialAd: true +})({ + // 业务代码 + methods: { + onLoad() { + + } + } +})) +``` + +### `hijackApp` - 劫持全局App方法,注入基础代码 + +- 在不方便使用injectApp时使用(如解包后代码复杂,难以修改App调用) +- 此方法会修改全局App方法,存在未知风险,使用时请进行完整测试 +- 不可与injectApp同时使用 + +```js +// app.js +hijackApp() +``` + +### `hijackAllPage` - 劫持全局Page方法,注入基础代码 + +- 在不方便使用injectPage/injectComponent时使用(如解包后代码复杂,难以修改Page/Component调用) +- 此方法会修改全局Page方法,并影响所有的页面,存在未知风险,使用时请进行完整测试 +- 参数同injectPage/injectComponent方法,不可与这些方法同时使用 + +```js +// app.js +hijackAllPage({ + showInterstitialAd: true +}) +``` + +### `gatewayHttpClient` - 网关API调用封装 + +- 同步的写法 + +```js +async function onLoad() { + try { + // 网关请求。参数:路径、方法、数据、其他选项(如headers、responseType) + const data = await gatewayHttpClient.request(path, method, data,options) + + // 头像上传。参数:文件路径 + const data = await gatewayHttpClient.uploadAvatar(filePath) + + // 文件上传。参数:文件路径、数据 + const data = await gatewayHttpClient.uploadFile(filePath, data) + + // 文件删除。参数:文件ID + const data = await gatewayHttpClient.deleteFile(fileId) + } catch(err) { + // 响应HTTP状态码非200时自动showToast并抛出异常 + } +} +``` + +- 所有方法均支持异步的写法 + +```js +function onLoad() { + gatewayHttpClient.request('/xxx') + .then(data => { + console.log(data) + }) + .catch(err => {}) +} +``` + +### `baseHttpClient`/`apiHttpClient` - 为老版本兼容保留,不推荐使用 + +### `HttpClient` - API底层类,用于封装自定义请求 + +- 示例:封装一个百度的请求客户端,并调用百度搜索 + +```js +const baiduHttpClient = new HttpClient({ + baseURL: 'https://www.baidu.com', + timeout: 5000, +}); + +baiduHttpClient.request('/s', 'GET', { wd: '测试' }, { responseType: 'text' }) + .then(data => console.log(data)) +``` + +### `adManager` - 广告管理器 + +- 确保广告数据加载完成,支持同步/异步的写法 + +```js +// 同步的写法 +async function onLoad() { + await adManager.waitAdData() + // 此处代码将在广告数据加载完成后执行 + await adManager.createAndShowInterstitialAd() +} + +// 异步的写法 +function onLoad () { + adManager.onDataReady(() => { + // 此处代码将在广告数据加载完成后执行 + adManager.createAndShowInterstitialAd() + }) +} +``` + +- 广告数据 + +```js +// 格式化之后的广告数据对象,如{banner: "adunit-f7709f349de05edc", custom: "adunit-34c76b0c3e4a6ed0", ...} +const ads = adManager.ads + +// 友情链接顶部广告数据 +const top = adManager.top + +// 友情链接数据 +const link = adManager.link +``` + +- 创建并展示插屏广告 + +```js +function onLoad() { + adManager.createAndShowInterstitialAd() +} +``` + +- 创建并展示激励视频广告 + - 传入当前页面的上下文this,返回用户是否已看完广告 + - 由于微信的底层限制,需要先在调用的页面上进行injectPage注入,且该方法必须放在用户的点击事件里调用 + - 使用示例可参考[jdwx-demo](https://code.miniappapi.com/wx/jdwx-demo) + +```js +// 同步的写法 +page({ + async handleClick() { + const isEnded = await adManager.createAndShowRewardedVideoAd(this) + } +}) + +// 异步的写法 +page({ + handleClick() { + adManager.createAndShowRewardedVideoAd(this).then((isEnded) => { + + }) + } +}) +``` diff --git a/node_modules/@jdmini/api/miniprogram_dist/index.d.ts b/node_modules/@jdmini/api/miniprogram_dist/index.d.ts new file mode 100644 index 0000000..e06e58d --- /dev/null +++ b/node_modules/@jdmini/api/miniprogram_dist/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/node_modules/@jdmini/api/miniprogram_dist/index.js b/node_modules/@jdmini/api/miniprogram_dist/index.js new file mode 100644 index 0000000..fc11843 --- /dev/null +++ b/node_modules/@jdmini/api/miniprogram_dist/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;n 构建 npm + +`注意:依赖@jdmini/api,请确保小程序项目已安装@jdmini/api` + +## 使用 + +1、在页面的 json 文件中引入组件: + +```json +{ + "usingComponents": { + "jdwx-ad": "@jdmini/components/jdwx-ad", + "jdwx-link": "@jdmini/components/jdwx-link" + } +} +``` + +2、在页面的 wxml 文件中使用组件: + +```html + + +``` diff --git a/node_modules/@jdmini/components/miniprogram_dist/icons/home-active.png b/node_modules/@jdmini/components/miniprogram_dist/icons/home-active.png new file mode 100644 index 0000000..127b0ed Binary files /dev/null and b/node_modules/@jdmini/components/miniprogram_dist/icons/home-active.png differ diff --git a/node_modules/@jdmini/components/miniprogram_dist/icons/home.png b/node_modules/@jdmini/components/miniprogram_dist/icons/home.png new file mode 100644 index 0000000..11016da Binary files /dev/null and b/node_modules/@jdmini/components/miniprogram_dist/icons/home.png differ diff --git a/node_modules/@jdmini/components/miniprogram_dist/icons/link-active.png b/node_modules/@jdmini/components/miniprogram_dist/icons/link-active.png new file mode 100644 index 0000000..433ec7b Binary files /dev/null and b/node_modules/@jdmini/components/miniprogram_dist/icons/link-active.png differ diff --git a/node_modules/@jdmini/components/miniprogram_dist/icons/link.png b/node_modules/@jdmini/components/miniprogram_dist/icons/link.png new file mode 100644 index 0000000..c021ab7 Binary files /dev/null and b/node_modules/@jdmini/components/miniprogram_dist/icons/link.png differ diff --git a/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.js b/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.js new file mode 100644 index 0000000..19671f8 --- /dev/null +++ b/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.js @@ -0,0 +1,22 @@ +import { adManager } from '@jdmini/api' + +Component({ + properties: { + type: { + type: String, + value: 'custom' // 可选banner, video, custom + } + }, + data: { + ads: {} + }, + lifetimes: { + attached: function () { + adManager.onDataReady(() => { + this.setData({ ads: adManager.ads }) + }) + }, + }, + methods: { + } +}) diff --git a/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.json b/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.json new file mode 100644 index 0000000..32640e0 --- /dev/null +++ b/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} \ No newline at end of file diff --git a/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.wxml b/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.wxml new file mode 100644 index 0000000..1b24adf --- /dev/null +++ b/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.wxml @@ -0,0 +1,5 @@ + + + + + diff --git a/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.wxss b/node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.wxss new file mode 100644 index 0000000..de627db --- /dev/null +++ b/node_modules/@jdmini/components/miniprogram_dist/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/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.js b/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.js new file mode 100644 index 0000000..887d749 --- /dev/null +++ b/node_modules/@jdmini/components/miniprogram_dist/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/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.json b/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.json new file mode 100644 index 0000000..fba482a --- /dev/null +++ b/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} \ No newline at end of file diff --git a/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.wxml b/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.wxml new file mode 100644 index 0000000..30ff009 --- /dev/null +++ b/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.wxml @@ -0,0 +1,11 @@ + + + + {{top.linkName}} + {{top.appDsc}} + + + + {{item.linkName}} + + \ No newline at end of file diff --git a/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.wxss b/node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.wxss new file mode 100644 index 0000000..111b9f1 --- /dev/null +++ b/node_modules/@jdmini/components/miniprogram_dist/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/node_modules/@jdmini/components/package.json b/node_modules/@jdmini/components/package.json new file mode 100644 index 0000000..7d6ed6f --- /dev/null +++ b/node_modules/@jdmini/components/package.json @@ -0,0 +1,20 @@ +{ + "name": "@jdmini/components", + "version": "1.0.6", + "description": "", + "files": [ + "miniprogram_dist", + "resources" + ], + "scripts": { + "pub": "npm publish --access public" + }, + "miniprogram": "miniprogram_dist", + "author": "", + "peerDependencies": { + "@jdmini/api": ">=1.0.8" + }, + "devDependencies": { + "@types/wechat-miniprogram": "^3.4.8" + } +} 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-detail/category-detail.js b/pages/category-detail/category-detail.js new file mode 100644 index 0000000..0232b2e --- /dev/null +++ b/pages/category-detail/category-detail.js @@ -0,0 +1,90 @@ +import { injectPage } from '@jdmini/api' +const { getWorksheets } = require('../../utils/api.js') +const { DATA_BASE_URL } = require('../../utils/config.js') + +Page(injectPage({})({ + data: { + dataBaseUrl: DATA_BASE_URL, + categoryId: null, + categoryName: '', + worksheets: [], + page: 1, + pageSize: 20, + hasMore: true, + loading: false + }, + + onLoad(options) { + if (options.id) { + this.setData({ categoryId: Number(options.id) }) + } + if (options.name) { + const name = decodeURIComponent(options.name) + this.setData({ categoryName: name }) + wx.setNavigationBarTitle({ title: name }) + } + this.loadWorksheets() + }, + + onPullDownRefresh() { + this.setData({ + page: 1, + hasMore: true, + worksheets: [] + }) + this.loadWorksheets().finally(() => { + wx.stopPullDownRefresh() + }) + }, + + onReachBottom() { + if (this.data.hasMore && !this.data.loading) { + this.loadMore() + } + }, + + // 加载练习表列表 + async loadWorksheets() { + if (this.data.loading) return + + try { + this.setData({ loading: true }) + + const res = await getWorksheets({ + category_id: this.data.categoryId, + page: this.data.page, + pageSize: this.data.pageSize + }) + + if (res.success) { + const newWorksheets = res.data.list || [] + this.setData({ + worksheets: this.data.page === 1 ? newWorksheets : [...this.data.worksheets, ...newWorksheets], + hasMore: newWorksheets.length >= this.data.pageSize, + loading: false + }) + } + } catch (error) { + console.error('加载练习表失败:', error) + this.setData({ loading: false }) + } + }, + + // 加载更多 + async loadMore() { + if (this.data.loading || !this.data.hasMore) return + + this.setData({ + page: this.data.page + 1 + }) + await this.loadWorksheets() + }, + + // 跳转详情页 + goDetail(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/detail/detail?id=${id}` + }) + } +})) diff --git a/pages/category-detail/category-detail.json b/pages/category-detail/category-detail.json new file mode 100644 index 0000000..287657f --- /dev/null +++ b/pages/category-detail/category-detail.json @@ -0,0 +1,5 @@ +{ + "usingComponents": {}, + "enablePullDownRefresh": true, + "backgroundTextStyle": "dark" +} diff --git a/pages/category-detail/category-detail.wxml b/pages/category-detail/category-detail.wxml new file mode 100644 index 0000000..693ba9c --- /dev/null +++ b/pages/category-detail/category-detail.wxml @@ -0,0 +1,30 @@ + + + + + + + + + + {{item.title}} + {{item.e_title}} + + + + + + + 加载中... + 已加载全部 + + + + + 暂无练习表 + + diff --git a/pages/category-detail/category-detail.wxss b/pages/category-detail/category-detail.wxss new file mode 100644 index 0000000..b5ae588 --- /dev/null +++ b/pages/category-detail/category-detail.wxss @@ -0,0 +1,77 @@ +/* 分类详情页样式 */ +.container { + padding: 20rpx; + min-height: 100vh; + background: #f5f5f5; +} + +/* 练习表网格 */ +.worksheet-grid { + display: flex; + flex-wrap: wrap; + gap: 20rpx; +} + +.worksheet-item { + width: calc(50% - 20rpx); + background: #fff; + border-radius: 16rpx; + overflow: hidden; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); +} + +.worksheet-cover { + position: relative; + width: 100%; + padding-top: 75%; + background: #f5f5f5; +} + +.cover-img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.worksheet-info { + padding: 16rpx; +} + +.worksheet-title { + display: block; + font-size: 26rpx; + color: #333; + line-height: 1.4; + height: 72rpx; +} + +.worksheet-e-title { + display: block; + font-size: 22rpx; + color: #999; + margin-top: 8rpx; +} + +/* 加载状态 */ +.load-status { + text-align: center; + padding: 30rpx 0; + font-size: 26rpx; + color: #999; +} + +/* 空状态 */ +.empty-tip { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 100rpx 0; +} + +.empty-tip text { + font-size: 28rpx; + color: #999; +} diff --git a/pages/category/category.js b/pages/category/category.js new file mode 100644 index 0000000..3ca3cb3 --- /dev/null +++ b/pages/category/category.js @@ -0,0 +1,131 @@ +import { injectPage } from '@jdmini/api' +const { getCategories, getWorksheets } = require('../../utils/api.js') +const { DATA_BASE_URL } = require('../../utils/config.js') + +Page(injectPage({})({ + data: { + dataBaseUrl: DATA_BASE_URL, + categories: [], + currentCategory: 1, + currentCategoryName: '', + worksheets: [], + page: 1, + pageSize: 20, + total: 0, + hasMore: true, + loading: false + }, + + onLoad(options) { + // 如果传入了分类ID + if (options.id) { + this.setData({ + currentCategory: Number(options.id) + }) + } + this.loadCategories() + }, + + onPullDownRefresh() { + this.setData({ + page: 1, + hasMore: true, + worksheets: [] + }) + this.loadWorksheets().finally(() => { + wx.stopPullDownRefresh() + }) + }, + + // 加载分类列表 + async loadCategories() { + try { + const res = await getCategories() + if (res.success && res.data.length > 0) { + const categories = res.data + // 如果没有设置当前分类,默认选中第一个 + const currentCategory = this.data.currentCategory || categories[0].id + const currentCategoryObj = categories.find(c => c.id === currentCategory) || categories[0] + + this.setData({ + categories, + currentCategory: currentCategoryObj.id, + currentCategoryName: currentCategoryObj.name + }) + + // 加载该分类下的练习表 + await this.loadWorksheets() + } + } catch (error) { + console.error('加载分类失败:', error) + wx.showToast({ + title: '加载失败', + icon: 'none' + }) + } + }, + + // 选择分类 + async selectCategory(e) { + const categoryId = Number(e.currentTarget.dataset.id) + if (categoryId === this.data.currentCategory) return + + const category = this.data.categories.find(c => c.id === categoryId) + + this.setData({ + currentCategory: categoryId, + currentCategoryName: category?.name || '', + page: 1, + hasMore: true, + worksheets: [] + }) + + await this.loadWorksheets() + }, + + // 加载练习表列表 + async loadWorksheets() { + if (this.data.loading) return + + try { + this.setData({ loading: true }) + + const res = await getWorksheets({ + category_id: this.data.currentCategory, + page: this.data.page, + pageSize: this.data.pageSize + }) + + if (res.success) { + const newWorksheets = res.data.list || [] + this.setData({ + worksheets: this.data.page === 1 ? newWorksheets : [...this.data.worksheets, ...newWorksheets], + total: res.data.pagination?.total || 0, + hasMore: newWorksheets.length >= this.data.pageSize, + loading: false + }) + } + } catch (error) { + console.error('加载练习表失败:', error) + this.setData({ loading: false }) + } + }, + + // 加载更多 + async loadMore() { + if (this.data.loading || !this.data.hasMore) return + + this.setData({ + page: this.data.page + 1 + }) + await this.loadWorksheets() + }, + + // 跳转详情页 + goDetail(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/detail/detail?id=${id}` + }) + } +})) diff --git a/pages/category/category.json b/pages/category/category.json new file mode 100644 index 0000000..7f8f211 --- /dev/null +++ b/pages/category/category.json @@ -0,0 +1,6 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "分类", + "enablePullDownRefresh": true, + "backgroundTextStyle": "dark" +} diff --git a/pages/category/category.wxml b/pages/category/category.wxml new file mode 100644 index 0000000..3e9dd9d --- /dev/null +++ b/pages/category/category.wxml @@ -0,0 +1,56 @@ + + + + + + + {{item.name}} + {{item.e_name}} + + + + + + + + {{currentCategoryName}} + 共{{total}}个练习表 + + + + + + + + + + {{item.title}} + + + + + + + 加载中... + 已加载全部 + + + + + + 暂无练习表 + + + diff --git a/pages/category/category.wxss b/pages/category/category.wxss new file mode 100644 index 0000000..db65939 --- /dev/null +++ b/pages/category/category.wxss @@ -0,0 +1,155 @@ +/* 分类页面样式 */ +.container { + display: flex; + height: 100vh; + background: #f5f5f5; +} + +/* 左侧分类列表 */ +.category-sidebar { + width: 200rpx; + height: 100%; + background: #fff; + flex-shrink: 0; +} + +.category-item { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 24rpx 16rpx; + background: #f8f8f8; + border-bottom: 1rpx solid #eee; +} + +.category-item.active { + background: #fff; +} + +.category-indicator { + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 6rpx; + height: 50rpx; + background: linear-gradient(180deg, #4CAF50, #8BC34A); + border-radius: 0 6rpx 6rpx 0; +} + +.category-name { + font-size: 26rpx; + color: #333; + text-align: center; + margin-bottom: 6rpx; +} + +.category-item.active .category-name { + color: #4CAF50; + font-weight: bold; +} + +.category-e-name { + font-size: 20rpx; + color: #999; + text-align: center; +} + +/* 右侧内容区 */ +.content-area { + flex: 1; + height: 100%; + padding: 20rpx; +} + +.content-header { + display: flex; + align-items: baseline; + margin-bottom: 20rpx; +} + +.content-title { + font-size: 32rpx; + font-weight: bold; + color: #333; + margin-right: 16rpx; +} + +.content-count { + font-size: 24rpx; + color: #999; +} + +/* 练习表网格 */ +.worksheet-grid { + display: flex; + flex-wrap: wrap; + margin: 0 -8rpx; +} + +.worksheet-item { + width: calc(50% - 16rpx); + margin: 0 8rpx 16rpx; + background: #fff; + border-radius: 12rpx; + overflow: hidden; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05); +} + +.worksheet-cover { + position: relative; + width: 100%; + padding-top: 75%; + background: #f5f5f5; +} + +.cover-img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.worksheet-info { + padding: 12rpx; +} + +.worksheet-title { + display: block; + font-size: 24rpx; + color: #333; + line-height: 1.4; + height: 67rpx; +} + +/* 加载状态 */ +.load-status { + text-align: center; + padding: 20rpx 0; + font-size: 24rpx; + color: #999; +} + +/* 空状态 */ +.empty-tip { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 80rpx 0; +} + +.empty-img { + width: 160rpx; + height: 160rpx; + margin-bottom: 16rpx; + opacity: 0.5; +} + +.empty-tip text { + font-size: 26rpx; + color: #999; +} diff --git a/pages/detail/detail.js b/pages/detail/detail.js new file mode 100644 index 0000000..fac9369 --- /dev/null +++ b/pages/detail/detail.js @@ -0,0 +1,388 @@ +import { injectPage } from '@jdmini/api' +const { getWorksheetDetail, checkPoints, deductPoints } = require('../../utils/api.js') +const { DATA_BASE_URL } = require('../../utils/config.js') + +Page(injectPage({})({ + data: { + dataBaseUrl: DATA_BASE_URL, + id: null, + detail: {}, + recommended: [], + loading: false, + downloadedPath: '' + }, + + onLoad(options) { + if (options.id) { + this.setData({ id: Number(options.id) }) + this.loadDetail() + } + }, + + onShareAppMessage() { + const userId = wx.getStorageSync('userId') + let path = `/pages/detail/detail?id=${this.data.id}` + // 携带邀请人ID + if (userId) { + path += `&inviter=${userId}` + } + return { + title: this.data.detail.title || '儿童练习表', + path: path, + imageUrl: DATA_BASE_URL + this.data.detail.coverurl + } + }, + + // 加载详情 + async loadDetail() { + try { + this.setData({ loading: true }) + + const res = await getWorksheetDetail(this.data.id) + if (res.success) { + // 设置导航栏标题 + wx.setNavigationBarTitle({ + title: res.data.detail.title || '练习表详情' + }) + + this.setData({ + detail: res.data.detail || {}, + recommended: res.data.recommended || [], + loading: false + }) + } + } catch (error) { + console.error('加载详情失败:', error) + this.setData({ loading: false }) + wx.showToast({ + title: '加载失败', + icon: 'none' + }) + } + }, + + // 预览封面大图 + previewImage() { + const url = DATA_BASE_URL + this.data.detail.coverurl + wx.previewImage({ + current: url, + urls: [url] + }) + }, + + // 预览PDF + previewPdf() { + const pdfUrl = DATA_BASE_URL + this.data.detail.pdfurl + + wx.showLoading({ + title: '加载中...', + mask: true + }) + + // 先下载PDF文件 + wx.downloadFile({ + url: pdfUrl, + success: (res) => { + wx.hideLoading() + if (res.statusCode === 200) { + // 打开PDF文档 + wx.openDocument({ + filePath: res.tempFilePath, + fileType: 'pdf', + showMenu: true, + success: () => { + console.log('PDF打开成功') + }, + fail: (err) => { + console.error('打开PDF失败:', err) + wx.showToast({ + title: '打开失败', + icon: 'none' + }) + } + }) + } + }, + fail: (err) => { + wx.hideLoading() + console.error('下载PDF失败:', err) + wx.showToast({ + title: '加载失败', + icon: 'none' + }) + } + }) + }, + + // 检查是否已下载过 + hasDownloaded(worksheetId) { + const downloads = wx.getStorageSync('downloads') || [] + return downloads.some(d => d.id === worksheetId) + }, + + // 下载PDF + async downloadPdf() { + // 1. 检查是否登录 + const userId = wx.getStorageSync('userId') + if (!userId) { + wx.showModal({ + title: '提示', + content: '请先登录后再下载', + confirmText: '去登录', + cancelText: '取消', + success: (res) => { + if (res.confirm) { + // 保存返回信息 + wx.setStorageSync('returnTo', { + type: 'navigateTo', + url: `/pages/detail/detail?id=${this.data.id}` + }) + wx.navigateTo({ + url: '/pages/login/login' + }) + } + } + }) + return + } + + // 2. 检查是否已下载过(已下载过的不扣积分) + const alreadyDownloaded = this.hasDownloaded(this.data.id) + + // 3. 如果未下载过,检查积分 + if (!alreadyDownloaded) { + try { + const pointsRes = await checkPoints(userId) + if (!pointsRes.success || !pointsRes.data.canDownload) { + wx.showModal({ + title: '积分不足', + content: `下载需要 ${pointsRes.data?.costPoints || 1} 积分,您当前积分为 ${pointsRes.data?.points || 0}。分享小程序可获得积分!`, + confirmText: '去分享', + cancelText: '取消', + success: (res) => { + if (res.confirm) { + // 触发分享 + this.shareToGetPoints() + } + } + }) + return + } + } catch (error) { + console.error('检查积分失败:', error) + wx.showToast({ + title: '网络错误', + icon: 'none' + }) + return + } + } + + // 4. 开始下载 + const pdfUrl = DATA_BASE_URL + this.data.detail.pdfurl + const title = this.data.detail.e_title || this.data.detail.title || 'worksheet' + const fileName = `${title}.pdf` + + wx.showLoading({ + title: '下载中...', + mask: true + }) + + try { + let remainingPoints = null + + // 只有首次下载才扣积分 + if (!alreadyDownloaded) { + const deductRes = await deductPoints(userId, this.data.id) + if (!deductRes.success) { + wx.hideLoading() + wx.showToast({ + title: deductRes.message || '扣除积分失败', + icon: 'none' + }) + return + } + remainingPoints = deductRes.data.remainingPoints + } + + // 下载文件 + wx.downloadFile({ + url: pdfUrl, + success: (res) => { + wx.hideLoading() + if (res.statusCode === 200) { + const tempFilePath = res.tempFilePath + + // 保存到本地 + const fs = wx.getFileSystemManager() + const savedPath = `${wx.env.USER_DATA_PATH}/${fileName}` + + fs.saveFile({ + tempFilePath: tempFilePath, + filePath: savedPath, + success: (saveRes) => { + this.setData({ downloadedPath: saveRes.savedFilePath }) + + // 保存下载记录 + this.saveDownloadRecord() + + // 根据是否是首次下载显示不同提示 + let content = '文件已保存,是否立即打开?' + if (!alreadyDownloaded && remainingPoints !== null) { + content = `文件已保存,消耗1积分,剩余${remainingPoints}积分。是否立即打开?` + } else if (alreadyDownloaded) { + content = '文件已保存(已购买,无需扣分)。是否立即打开?' + } + + wx.showModal({ + title: '下载成功', + content: content, + confirmText: '打开', + cancelText: '稍后', + success: (modalRes) => { + if (modalRes.confirm) { + wx.openDocument({ + filePath: saveRes.savedFilePath, + fileType: 'pdf', + showMenu: true + }) + } + } + }) + }, + fail: (err) => { + console.error('保存文件失败:', err) + // 如果保存失败,直接打开临时文件 + wx.showModal({ + title: '下载成功', + content: '是否立即打开?', + confirmText: '打开', + cancelText: '稍后', + success: (modalRes) => { + if (modalRes.confirm) { + wx.openDocument({ + filePath: tempFilePath, + fileType: 'pdf', + showMenu: true + }) + } + } + }) + } + }) + } + }, + fail: (err) => { + wx.hideLoading() + console.error('下载失败:', err) + wx.showToast({ + title: '下载失败', + icon: 'none' + }) + } + }) + } catch (error) { + wx.hideLoading() + console.error('下载失败:', error) + } + }, + + // 分享获取积分 + shareToGetPoints() { + wx.showToast({ + title: '点击右上角分享给好友', + icon: 'none', + duration: 2000 + }) + }, + + // 保存下载记录 + saveDownloadRecord() { + try { + const downloads = wx.getStorageSync('downloads') || [] + const record = { + id: this.data.id, + title: this.data.detail.title, + e_title: this.data.detail.e_title, + coverurl: this.data.detail.coverurl, + pdfurl: this.data.detail.pdfurl, + category_name: this.data.detail.category_name, + downloadTime: Date.now(), + filePath: this.data.downloadedPath + } + + // 检查是否已存在 + const existIndex = downloads.findIndex(d => d.id === record.id) + if (existIndex > -1) { + downloads[existIndex] = record + } else { + downloads.unshift(record) + } + + // 最多保存100条 + if (downloads.length > 100) { + downloads.pop() + } + + wx.setStorageSync('downloads', downloads) + } catch (error) { + console.error('保存下载记录失败:', error) + } + }, + + // 打印PDF + printPdf() { + const pdfUrl = DATA_BASE_URL + this.data.detail.pdfurl + + wx.showLoading({ + title: '准备打印...', + mask: true + }) + + // 先下载PDF + wx.downloadFile({ + url: pdfUrl, + success: (res) => { + wx.hideLoading() + if (res.statusCode === 200) { + // 微信小程序没有直接打印API,引导用户通过打开文档后使用系统打印 + wx.openDocument({ + filePath: res.tempFilePath, + fileType: 'pdf', + showMenu: true, + success: () => { + wx.showToast({ + title: '请点击右上角菜单进行打印', + icon: 'none', + duration: 3000 + }) + }, + fail: (err) => { + console.error('打开PDF失败:', err) + wx.showToast({ + title: '打开失败', + icon: 'none' + }) + } + }) + } + }, + fail: (err) => { + wx.hideLoading() + console.error('下载失败:', err) + wx.showToast({ + title: '加载失败', + icon: 'none' + }) + } + }) + }, + + // 跳转到其他详情 + goDetail(e) { + const id = e.currentTarget.dataset.id + wx.redirectTo({ + url: `/pages/detail/detail?id=${id}` + }) + } +})) diff --git a/pages/detail/detail.json b/pages/detail/detail.json new file mode 100644 index 0000000..38d4d33 --- /dev/null +++ b/pages/detail/detail.json @@ -0,0 +1,4 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "练习表详情" +} diff --git a/pages/detail/detail.wxml b/pages/detail/detail.wxml new file mode 100644 index 0000000..9538ad9 --- /dev/null +++ b/pages/detail/detail.wxml @@ -0,0 +1,59 @@ + + + + + + 点击图片预览大图 + + + + + + {{detail.category_name}} + + 浏览 {{detail.view_count || 0}} + 下载 {{detail.download_count || 0}} + + + {{detail.title}} + {{detail.e_title}} + {{detail.des}} + + + + 💡 + Tips: 打开PDF点右上角'···'选择你打印机打印 + + + + + + 下载PDF + + + + + + + 相关推荐 + + + + + + {{item.title}} + + + + + + + + diff --git a/pages/detail/detail.wxss b/pages/detail/detail.wxss new file mode 100644 index 0000000..c0a322e --- /dev/null +++ b/pages/detail/detail.wxss @@ -0,0 +1,213 @@ +/* 详情页面样式 */ +.container { + padding-bottom: 40rpx; + min-height: 100vh; + background: #f5f5f5; +} + +/* 封面预览 */ +.preview-section { + background: #fff; + padding: 30rpx; + text-align: center; +} + +.preview-image { + width: 100%; + height: 500rpx; + border-radius: 16rpx; + background: #f5f5f5; +} + +.preview-tip { + margin-top: 16rpx; + font-size: 24rpx; + color: #999; +} + +/* 基本信息 */ +.info-section { + background: #fff; + padding: 30rpx; + margin-top: 20rpx; +} + +.info-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20rpx; +} + +.category-tag { + display: inline-block; + padding: 8rpx 20rpx; + background: linear-gradient(135deg, #4CAF50, #8BC34A); + border-radius: 20rpx; + font-size: 24rpx; + color: #fff; +} + +.stats { + display: flex; + align-items: center; +} + +.stat-item { + font-size: 24rpx; + color: #999; + margin-left: 24rpx; +} + +.info-title { + display: block; + font-size: 36rpx; + font-weight: bold; + color: #333; + line-height: 1.4; + margin-bottom: 12rpx; +} + +.info-e-title { + display: block; + font-size: 28rpx; + color: #666; + margin-bottom: 20rpx; +} + +.info-desc { + display: block; + font-size: 26rpx; + color: #999; + line-height: 1.6; +} + +/* 操作按钮 */ +.action-section { + display: flex; + justify-content: space-between; + padding: 30rpx; + background: #fff; + margin-top: 20rpx; +} + +.action-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex: 1; + padding: 24rpx 0; + margin: 0 10rpx; + border-radius: 16rpx; + transition: all 0.3s; +} + +.action-btn:first-child { + margin-left: 0; +} + +.action-btn:last-child { + margin-right: 0; +} + +.preview-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.download-btn { + background: linear-gradient(135deg, #4CAF50, #8BC34A); +} + +.print-btn { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); +} + +.action-icon { + width: 48rpx; + height: 48rpx; + margin-bottom: 12rpx; +} + +.action-btn text { + font-size: 26rpx; + color: #fff; +} + +/* 相关推荐 */ +.recommend-section { + background: #fff; + padding: 30rpx 0 30rpx 30rpx; + margin-top: 20rpx; +} + +.section-header { + margin-bottom: 20rpx; +} + +.section-title { + font-size: 32rpx; + font-weight: bold; + color: #333; +} + +.recommend-scroll { + width: 100%; +} + +.recommend-list { + display: flex; + white-space: nowrap; +} + +.recommend-item { + flex-shrink: 0; + width: 200rpx; + margin-right: 20rpx; +} + +.recommend-item:last-child { + margin-right: 30rpx; +} + +.recommend-cover { + width: 200rpx; + height: 150rpx; + border-radius: 12rpx; + background: #f5f5f5; +} + +.recommend-title { + display: block; + margin-top: 12rpx; + font-size: 24rpx; + color: #333; + white-space: normal; +} + +/* 提示信息 */ +.tips-box { + display: flex; + align-items: center; + justify-content: center; + margin: 20rpx 30rpx; + padding: 16rpx 24rpx; + background: rgba(76, 175, 80, 0.1); + border-radius: 8rpx; +} + +.tips-icon { + font-size: 28rpx; + margin-right: 8rpx; +} + +.tips-text { + font-size: 28rpx; + color: #4CAF50; + line-height: 1.4; +} + +/* 底部安全区域 */ +.safe-bottom { + height: env(safe-area-inset-bottom); +} diff --git a/pages/download/download.js b/pages/download/download.js new file mode 100644 index 0000000..1dd9116 --- /dev/null +++ b/pages/download/download.js @@ -0,0 +1,150 @@ +import { injectPage } from '@jdmini/api' +const { DATA_BASE_URL } = require('../../utils/config.js') + +Page(injectPage({})({ + data: { + dataBaseUrl: DATA_BASE_URL, + downloads: [] + }, + + onShow() { + this.loadDownloads() + }, + + // 加载下载记录 + loadDownloads() { + try { + const downloads = wx.getStorageSync('downloads') || [] + + // 格式化时间 + const formattedDownloads = downloads.map(item => ({ + ...item, + downloadTimeStr: this.formatTime(item.downloadTime) + })) + + this.setData({ downloads: formattedDownloads }) + } catch (error) { + console.error('加载下载记录失败:', error) + } + }, + + // 格式化时间 + formatTime(timestamp) { + const date = new Date(timestamp) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + return `${year}-${month}-${day} ${hours}:${minutes}` + }, + + // 打开文件 + openFile(e) { + const index = e.currentTarget.dataset.index + const item = this.data.downloads[index] + + if (item.filePath) { + // 尝试打开已保存的文件 + wx.openDocument({ + filePath: item.filePath, + fileType: 'pdf', + showMenu: true, + fail: (err) => { + console.error('打开文件失败:', err) + // 文件可能被删除,重新下载 + this.redownload(item) + } + }) + } else { + // 没有本地文件,重新下载 + this.redownload(item) + } + }, + + // 重新下载 + redownload(item) { + const pdfUrl = DATA_BASE_URL + item.pdfurl + + wx.showLoading({ + title: '加载中...', + mask: true + }) + + wx.downloadFile({ + url: pdfUrl, + success: (res) => { + wx.hideLoading() + if (res.statusCode === 200) { + wx.openDocument({ + filePath: res.tempFilePath, + fileType: 'pdf', + showMenu: true + }) + } + }, + fail: () => { + wx.hideLoading() + wx.showToast({ + title: '加载失败', + icon: 'none' + }) + } + }) + }, + + // 删除下载记录 + deleteItem(e) { + const index = e.currentTarget.dataset.index + const item = this.data.downloads[index] + + wx.showModal({ + title: '提示', + content: '确定删除此下载记录吗?', + success: (res) => { + if (res.confirm) { + // 删除本地文件 + if (item.filePath) { + try { + const fs = wx.getFileSystemManager() + fs.unlinkSync(item.filePath) + } catch (error) { + console.log('删除文件失败或文件不存在') + } + } + + // 更新记录 + const downloads = this.data.downloads.filter((_, i) => i !== index) + this.setData({ downloads }) + + // 更新存储 + const storageData = downloads.map(d => { + const { downloadTimeStr, ...rest } = d + return rest + }) + wx.setStorageSync('downloads', storageData) + + wx.showToast({ + title: '已删除', + icon: 'success' + }) + } + } + }) + }, + + // 跳转详情 + goDetail(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/detail/detail?id=${id}` + }) + }, + + // 去浏览 + goHome() { + wx.switchTab({ + url: '/pages/index/index' + }) + } +})) diff --git a/pages/download/download.json b/pages/download/download.json new file mode 100644 index 0000000..cf513a3 --- /dev/null +++ b/pages/download/download.json @@ -0,0 +1,4 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "我的下载" +} diff --git a/pages/download/download.wxml b/pages/download/download.wxml new file mode 100644 index 0000000..a097708 --- /dev/null +++ b/pages/download/download.wxml @@ -0,0 +1,36 @@ + + + + + + + + + {{item.title}} + {{item.category_name}} + 下载时间: {{item.downloadTimeStr}} + + + + + 打开 + + + 删除 + + + + + + + + + 暂无下载记录 + 浏览练习表并下载PDF文件后,将在此处显示 + + 去浏览 + + + diff --git a/pages/download/download.wxss b/pages/download/download.wxss new file mode 100644 index 0000000..5a9a88e --- /dev/null +++ b/pages/download/download.wxss @@ -0,0 +1,129 @@ +/* 下载页面样式 */ +.container { + min-height: 100vh; + background: #f5f5f5; + padding-bottom: env(safe-area-inset-bottom); +} + +/* 下载列表 */ +.download-list { + padding: 20rpx; +} + +.download-item { + background: #fff; + border-radius: 16rpx; + margin-bottom: 20rpx; + overflow: hidden; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05); +} + +.item-content { + display: flex; + padding: 20rpx; +} + +.item-cover { + width: 160rpx; + height: 120rpx; + border-radius: 8rpx; + background: #f5f5f5; + flex-shrink: 0; +} + +.item-info { + flex: 1; + margin-left: 20rpx; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.item-title { + font-size: 28rpx; + color: #333; + line-height: 1.4; +} + +.item-category { + font-size: 24rpx; + color: #4CAF50; +} + +.item-time { + font-size: 22rpx; + color: #999; +} + +.item-actions { + display: flex; + border-top: 1rpx solid #f0f0f0; +} + +.action-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 20rpx 0; + border-right: 1rpx solid #f0f0f0; +} + +.action-btn:last-child { + border-right: none; +} + +.action-btn image { + width: 32rpx; + height: 32rpx; + margin-right: 8rpx; +} + +.action-btn text { + font-size: 26rpx; + color: #666; +} + +.action-btn.delete text { + color: #f56c6c; +} + +/* 空状态 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 160rpx 60rpx; +} + +.empty-img { + width: 240rpx; + height: 240rpx; + margin-bottom: 40rpx; + opacity: 0.6; +} + +.empty-title { + font-size: 32rpx; + color: #333; + margin-bottom: 16rpx; +} + +.empty-desc { + font-size: 26rpx; + color: #999; + text-align: center; + margin-bottom: 40rpx; +} + +.empty-btn { + padding: 20rpx 60rpx; + background: linear-gradient(135deg, #4CAF50, #8BC34A); + border-radius: 40rpx; +} + +.empty-btn text { + font-size: 28rpx; + color: #fff; +} diff --git a/pages/index/index.js b/pages/index/index.js new file mode 100644 index 0000000..b6fa105 --- /dev/null +++ b/pages/index/index.js @@ -0,0 +1,60 @@ +import { injectPage } from '@jdmini/api' +const { getHomeData } = require('../../utils/api.js') +const { DATA_BASE_URL } = require('../../utils/config.js') + +Page(injectPage({})({ + data: { + dataBaseUrl: DATA_BASE_URL, + // 分类数据(每个分类的第一条作为封面) + worksheets: [], + loading: false + }, + + onLoad() { + this.loadHomeData() + }, + + onPullDownRefresh() { + this.loadHomeData().finally(() => { + wx.stopPullDownRefresh() + }) + }, + + // 加载首页数据 + async loadHomeData() { + try { + this.setData({ loading: true }) + const res = await getHomeData() + if (res.success) { + this.setData({ + worksheets: res.data.worksheets || [], + loading: false + }) + } + } catch (error) { + console.error('加载首页数据失败:', error) + this.setData({ loading: false }) + wx.showToast({ + title: '加载失败', + icon: 'none' + }) + } + }, + + // 跳转搜索页 + goSearch() { + wx.navigateTo({ + url: '/pages/search/search' + }) + }, + + // 跳转到分类详情页面 + goCategory(e) { + const categoryId = e.currentTarget.dataset.id + const item = this.data.worksheets.find(w => w.category_id === categoryId) + const categoryName = item ? item.category_name : '' + wx.navigateTo({ + url: `/pages/category-detail/category-detail?id=${categoryId}&name=${encodeURIComponent(categoryName)}` + }) + } +})) diff --git a/pages/index/index.json b/pages/index/index.json new file mode 100644 index 0000000..287657f --- /dev/null +++ b/pages/index/index.json @@ -0,0 +1,5 @@ +{ + "usingComponents": {}, + "enablePullDownRefresh": true, + "backgroundTextStyle": "dark" +} diff --git a/pages/index/index.wxml b/pages/index/index.wxml new file mode 100644 index 0000000..aa488fa --- /dev/null +++ b/pages/index/index.wxml @@ -0,0 +1,48 @@ + + + + + + + 搜索练习表... + + + + + + + + + + + {{item.category_name}} + {{item.category_e_name}} + + + + + diff --git a/pages/index/index.wxss b/pages/index/index.wxss new file mode 100644 index 0000000..a7150c3 --- /dev/null +++ b/pages/index/index.wxss @@ -0,0 +1,130 @@ +/* 首页样式 */ +.container { + padding-bottom: 20rpx; +} + +/* 搜索栏 */ +.search-bar { + padding: 20rpx 30rpx; + background: #fff; +} + +.search-input { + display: flex; + align-items: center; + padding: 16rpx 24rpx; + background: #f5f5f5; + border-radius: 40rpx; +} + +.search-icon { + width: 36rpx; + height: 36rpx; + margin-right: 16rpx; +} + +.search-placeholder { + color: #999; + font-size: 28rpx; +} + +/* Banner轮播图 */ +.banner-section { + padding: 0 30rpx 20rpx; +} + +.banner-swiper { + height: 280rpx; + border-radius: 20rpx; + overflow: hidden; +} + +.banner-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 30rpx 40rpx; + height: 100%; + border-radius: 20rpx; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.banner-content { + flex: 1; +} + +.banner-title { + display: block; + font-size: 40rpx; + font-weight: bold; + color: #fff; + margin-bottom: 16rpx; +} + +.banner-desc { + display: block; + font-size: 26rpx; + color: rgba(255, 255, 255, 0.8); +} + +.banner-img { + width: 180rpx; + height: 180rpx; +} + +/* 分类列表 */ +.category-list-section { + display: flex; + flex-wrap: wrap; + padding: 20rpx; + gap: 20rpx; +} + +.category-card { + width: calc(50% - 20rpx); + background: #fff; + border-radius: 16rpx; + overflow: hidden; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); +} + +.category-cover { + position: relative; + width: 100%; + padding-top: 100%; + background: #f5f5f5; +} + +.category-cover .cover-img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.category-info { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 20rpx 16rpx; + background: rgba(76,175,80,0.85); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + text-align: center; +} + +.category-name { + display: block; + font-size: 30rpx; + font-weight: bold; + color: #fff; + margin-bottom: 6rpx; +} + +.category-e-name { + display: block; + font-size: 22rpx; + color: rgba(255,255,255,0.9); +} diff --git a/pages/login/login.js b/pages/login/login.js new file mode 100644 index 0000000..3564d01 --- /dev/null +++ b/pages/login/login.js @@ -0,0 +1,220 @@ +import { injectPage, gatewayHttpClient } from '@jdmini/api' +const api = require('../../utils/api.js') +const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0' + +Page(injectPage({})({ + data: { + avatarUrl: defaultAvatarUrl, + nickname: '', + isLoading: false, + loginSuccess: false, + btnText: '完成登录', + btnIcon: '✓', + canSubmit: false + }, + + onLoad(options) { + // 检查是否已登录 + const userId = wx.getStorageSync('userId'); + if (userId) { + // 已登录,检查是否有返回页面 + this.navigateBack(); + return; + } + + // 保存来源页面信息 + if (options.from) { + this.setData({ + fromPage: options.from + }); + } + + // 设置默认头像并检查提交状态 + this.checkCanSubmit(); + }, + + // 选择头像 + onChooseAvatar(e) { + const { avatarUrl } = e.detail; + this.setData({ + avatarUrl: avatarUrl + }, () => { + this.checkCanSubmit(); + }); + }, + + // 输入昵称 + onNicknameInput(e) { + this.setData({ + nickname: e.detail.value + }, () => { + this.checkCanSubmit(); + }); + }, + + // 检查是否可以提交 + checkCanSubmit() { + const { avatarUrl, nickname } = this.data; + // 检查是否选择了自定义头像(非默认头像)和填写了昵称 + const hasCustomAvatar = avatarUrl && avatarUrl !== defaultAvatarUrl; + const hasNickname = nickname && nickname.trim().length > 0; + const canSubmit = hasCustomAvatar && hasNickname; + this.setData({ + canSubmit: canSubmit + }); + }, + + // 处理登录 + async handleLogin() { + const { avatarUrl, nickname, isLoading } = this.data; + + if (isLoading) { + return; + } + + // 检查头像是否已授权(是否选择了非默认头像) + const hasCustomAvatar = avatarUrl && avatarUrl !== defaultAvatarUrl; + + // 检查昵称是否已填写 + const hasNickname = nickname && nickname.trim().length > 0; + + // 验证授权状态 + if (!hasCustomAvatar && !hasNickname) { + wx.showToast({ + title: '请先授权头像,再点授权昵称', + icon: 'none', + duration: 2500 + }); + return; + } + + if (!hasCustomAvatar) { + wx.showToast({ + title: '头像未授权', + icon: 'none', + duration: 2000 + }); + return; + } + + if (!hasNickname) { + wx.showToast({ + title: '昵称未授权', + icon: 'none', + duration: 2000 + }); + return; + } + + // 开始登录 + this.setData({ + isLoading: true, + btnText: '登录中...', + btnIcon: '⏳' + }); + + // 获取 app.js 中通过 waitLogin 已经获取的第三方登录信息 + const app = getApp(); + const openid = app.globalData.openid; + const wxUserInfo = app.globalData.wxUserInfo || wx.getStorageSync('jdwx-userinfo'); + + if (!openid && !wxUserInfo) { + this.handleLoginError('获取登录信息失败,请重启小程序'); + return; + } + //上传头像到图片服务 + + const JDavatarUrl= await gatewayHttpClient.uploadAvatar(avatarUrl) + + console.log(JDavatarUrl.data) + // 使用 openid 进行登录 + const finalOpenid = openid || wxUserInfo.openId; + this.performLogin(finalOpenid, nickname.trim(), JDavatarUrl.data); + }, + + // 执行登录请求 + performLogin(openid, nickname, avatarUrl) { + // 获取邀请人ID + const inviterId = wx.getStorageSync('inviterId') || getApp().globalData.inviterId || null + + // 调用后端登录接口 + api.userLogin(openid, nickname, avatarUrl, inviterId).then(result => { + // 清除邀请人ID(只在注册时使用一次) + wx.removeStorageSync('inviterId') + + // 保存用户信息 + wx.setStorageSync('userId', result.data.user.id); + wx.setStorageSync('token', result.data.token); + wx.setStorageSync('userInfo', { + nickname: result.data.user.nickname, + avatar: result.data.user.avatar, + points: result.data.user.points + }); + + // 更新按钮状态 + this.setData({ + isLoading: false, + loginSuccess: true, + btnText: '登录成功', + btnIcon: '✓' + }); + + // 延迟跳转 + setTimeout(() => { + this.navigateBack(); + }, 1000); + + }).catch(err => { + console.error('登录失败', err); + this.handleLoginError(err.message || '登录失败,请重试'); + }); + }, + + // 处理登录错误 + handleLoginError(message) { + wx.showToast({ + title: message, + icon: 'none' + }); + + this.setData({ + isLoading: false, + btnText: '完成登录', + btnIcon: '✓' + }); + }, + + // 导航返回 + navigateBack() { + // 检查是否有存储的返回信息 + const returnTo = wx.getStorageSync('returnTo'); + + if (returnTo) { + // 清除存储的返回信息 + wx.removeStorageSync('returnTo'); + + // 根据返回类型进行跳转 + if (returnTo.type === 'switchTab') { + wx.switchTab({ + url: returnTo.url + }); + } else if (returnTo.type === 'redirectTo') { + wx.redirectTo({ + url: returnTo.url + }); + } else { + wx.navigateTo({ + url: returnTo.url + }); + } + } else if (this.data.fromPage) { + // 使用URL参数中的来源页面 + wx.navigateBack(); + } else { + // 默认返回首页 + wx.switchTab({ + url: '/pages/index/index' + }); + } + } +})) diff --git a/pages/login/login.json b/pages/login/login.json new file mode 100644 index 0000000..40df7ae --- /dev/null +++ b/pages/login/login.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "登录", + "usingComponents": {} +} diff --git a/pages/login/login.wxml b/pages/login/login.wxml new file mode 100644 index 0000000..fd877fa --- /dev/null +++ b/pages/login/login.wxml @@ -0,0 +1,55 @@ + + + + + 微信登录授权 + 请授权获取您的头像和昵称信息 + + + + + + + + + + + + + + + + 授权中... + ✓ 已授权 + 立即授权 + + + + + 授权说明 + + + 获取您的公开信息(昵称、头像) + + + + 用于完善您的个人资料 + + + + 我们承诺保护您的隐私安全 + + + diff --git a/pages/login/login.wxss b/pages/login/login.wxss new file mode 100644 index 0000000..449b55c --- /dev/null +++ b/pages/login/login.wxss @@ -0,0 +1,218 @@ +page { + background-color: #f5f5f5; + min-height: 100vh; +} + +/* 主容器 */ +.container { + padding: 0; + min-height: 100vh; + background-color: #f5f5f5; + display: flex; + flex-direction: column; + align-items: center; +} + +/* 头部 */ +.header { + text-align: center; + padding: 80rpx 0 60rpx; + background-color: #f5f5f5; + width: 100%; +} + +.logo { + width: 160rpx; + height: 160rpx; + background: linear-gradient(135deg, #12b559 0%, #0aa750 100%); + border-radius: 32rpx; + margin: 0 auto 32rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 72rpx; + color: #fff; + box-shadow: 0 4rpx 16rpx rgba(18, 181, 89, 0.25); +} + +.title { + display: block; + font-size: 34rpx; + font-weight: 400; + color: #000; + margin-bottom: 16rpx; +} + +.subtitle { + display: block; + font-size: 26rpx; + color: #888; + line-height: 1.6; +} + +/* 用户信息卡片 */ +.user-card { + width: 620rpx; + background: #fff; + border-radius: 16rpx; + padding: 60rpx 0; + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 40rpx; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08); +} + +/* 头像选择器 */ +.avatar-picker { + background: none; + border: none; + padding: 0; + margin: 0 auto 24rpx; + line-height: normal; + display: block; + width: 172rpx; +} + +.avatar-picker::after { + border: none; +} + +.avatar-wrapper { + width: 160rpx; + height: 160rpx; + border-radius: 50%; + background: #f0f0f0; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + border: 6rpx solid #07c160; + position: relative; + margin: 0 auto; +} + +.avatar-wrapper.has-avatar { + border-color: #07c160; +} + +.avatar-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.avatar-icon { + font-size: 80rpx; + color: #c8c8c8; +} + +/* 昵称输入框(按钮样式) */ +.nickname-wrapper { + width: 100%; + display: flex; + justify-content: center; +} + +.nickname-btn-wrapper { + background: #f8f8f8; + border: 1rpx solid #e5e5e5; + border-radius: 8rpx; + padding: 20rpx 60rpx; + min-width: 400rpx; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s; +} + +.nickname-btn-wrapper:active { + background: #efefef; +} + +.nickname-btn-input { + width: 100%; + height: 40rpx; + font-size: 32rpx; + color: #000; + font-weight: 400; + text-align: center; + background: transparent; + border: none; +} + +.nickname-placeholder { + color: #999; + font-size: 32rpx; + text-align: center; +} + +/* 授权按钮 */ +.auth-btn { + width: 620rpx; + height: 88rpx; + background: #07c160; + border-radius: 8rpx; + border: none; + color: #fff; + font-size: 32rpx; + font-weight: 400; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 50rpx; + box-shadow: 0 2rpx 8rpx rgba(7, 193, 96, 0.3); + transition: all 0.3s; + cursor: pointer; +} + +.auth-btn:active { + opacity: 0.8; + transform: scale(0.98); +} + +.auth-btn.disabled { + background: #9ed99d; + color: rgba(255, 255, 255, 0.7); + box-shadow: none; + cursor: not-allowed; +} + +.auth-btn.disabled:active { + opacity: 1; + transform: scale(1); +} + +/* 授权说明 */ +.permissions { + width: 620rpx; + padding: 0; +} + +.permissions-title { + display: block; + font-size: 28rpx; + color: #000; + font-weight: 400; + margin-bottom: 24rpx; +} + +.permission-item { + display: flex; + align-items: flex-start; + margin-bottom: 14rpx; + font-size: 26rpx; + color: #666; + line-height: 1.8; +} + +.permission-item:last-child { + margin-bottom: 0; +} + +.dot { + margin-right: 12rpx; + color: #000; + font-size: 24rpx; + line-height: 1.8; +} diff --git a/pages/mine/mine.js b/pages/mine/mine.js new file mode 100644 index 0000000..e093fe5 --- /dev/null +++ b/pages/mine/mine.js @@ -0,0 +1,231 @@ +import { injectPage } from '@jdmini/api' +const { getUserInfo } = require('../../utils/api.js') + +const defaultAvatar = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0' + +Page(injectPage({})({ + data: { + isLoggedIn: false, + userInfo: {}, + defaultAvatar: defaultAvatar, + downloadCount: 0, + inviteCount: 0, + cacheSize: '0KB' + }, + + onShow() { + this.checkLoginStatus() + this.loadStats() + this.calculateCacheSize() + }, + + onShareAppMessage() { + const userId = wx.getStorageSync('userId') + let path = '/pages/index/index' + if (userId) { + path += `?inviter=${userId}` + } + return { + title: '儿童练习表 - 幼儿启蒙必备,快来一起学习吧!', + path: path + } + }, + + // 检查登录状态 + checkLoginStatus() { + const userId = wx.getStorageSync('userId') + const userInfo = wx.getStorageSync('userInfo') || {} + + if (userId) { + this.setData({ + isLoggedIn: true, + userInfo: userInfo + }) + // 刷新用户信息 + this.refreshUserInfo(userId) + } else { + this.setData({ + isLoggedIn: false, + userInfo: {} + }) + } + }, + + // 刷新用户信息 + async refreshUserInfo(userId) { + try { + const res = await getUserInfo(userId) + if (res.success) { + const userInfo = { + ...this.data.userInfo, + nickname: res.data.nickname, + avatar: res.data.avatar, + points: res.data.points + } + this.setData({ userInfo }) + wx.setStorageSync('userInfo', userInfo) + } + } catch (error) { + console.error('刷新用户信息失败:', error) + } + }, + + // 加载统计数据 + loadStats() { + try { + const downloads = wx.getStorageSync('downloads') || [] + // 邀请数暂时从本地存储获取,实际应该从服务端获取 + const inviteCount = wx.getStorageSync('inviteCount') || 0 + + this.setData({ + downloadCount: downloads.length, + inviteCount: inviteCount + }) + } catch (error) { + console.error('加载统计失败:', error) + } + }, + + // 计算缓存大小 + calculateCacheSize() { + try { + const res = wx.getStorageInfoSync() + const usedSize = res.currentSize // KB + let sizeStr = '' + + if (usedSize < 1024) { + sizeStr = usedSize + 'KB' + } else { + sizeStr = (usedSize / 1024).toFixed(2) + 'MB' + } + + this.setData({ cacheSize: sizeStr }) + } catch (error) { + console.error('计算缓存大小失败:', error) + } + }, + + // 跳转登录 + goLogin() { + wx.setStorageSync('returnTo', { + type: 'switchTab', + url: '/pages/mine/mine' + }) + wx.navigateTo({ + url: '/pages/login/login' + }) + }, + + // 跳转下载页面 + goDownloads() { + wx.switchTab({ + url: '/pages/download/download' + }) + }, + + // 跳转积分明细 + goPointsLog() { + if (!this.data.isLoggedIn) { + this.goLogin() + return + } + wx.showToast({ + title: '积分明细功能开发中', + icon: 'none' + }) + }, + + // 清除缓存 + clearCache() { + wx.showModal({ + title: '提示', + content: '确定清除所有缓存吗?这将删除下载记录(不会退出登录)。', + success: (res) => { + if (res.confirm) { + try { + // 保存登录信息 + const userId = wx.getStorageSync('userId') + const token = wx.getStorageSync('token') + const userInfo = wx.getStorageSync('userInfo') + + // 清除下载的文件 + const downloads = wx.getStorageSync('downloads') || [] + const fs = wx.getFileSystemManager() + + downloads.forEach(item => { + if (item.filePath) { + try { + fs.unlinkSync(item.filePath) + } catch (e) { + // 忽略文件不存在的错误 + } + } + }) + + // 清除存储 + wx.clearStorageSync() + + // 恢复登录信息 + if (userId) { + wx.setStorageSync('userId', userId) + wx.setStorageSync('token', token) + wx.setStorageSync('userInfo', userInfo) + } + + this.setData({ + downloadCount: 0, + inviteCount: 0, + cacheSize: '0KB' + }) + + wx.showToast({ + title: '清除成功', + icon: 'success' + }) + } catch (error) { + console.error('清除缓存失败:', error) + wx.showToast({ + title: '清除失败', + icon: 'none' + }) + } + } + } + }) + }, + + // 退出登录 + logout() { + wx.showModal({ + title: '提示', + content: '确定退出登录吗?', + success: (res) => { + if (res.confirm) { + wx.removeStorageSync('userId') + wx.removeStorageSync('token') + wx.removeStorageSync('userInfo') + + this.setData({ + isLoggedIn: false, + userInfo: {} + }) + + wx.showToast({ + title: '已退出登录', + icon: 'success' + }) + } + } + }) + }, + + // 关于我们 + about() { + wx.showModal({ + title: '关于儿童练习表', + content: '儿童练习表是一款专为幼儿设计的启蒙教育小程序,提供字母、数字、绘画、涂色等多种练习表,帮助孩子在快乐中学习成长。\n\n版本:v1.0.0', + showCancel: false, + confirmText: '确定' + }) + } +})) diff --git a/pages/mine/mine.json b/pages/mine/mine.json new file mode 100644 index 0000000..d8f6441 --- /dev/null +++ b/pages/mine/mine.json @@ -0,0 +1,4 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "我的" +} diff --git a/pages/mine/mine.wxml b/pages/mine/mine.wxml new file mode 100644 index 0000000..ab468fd --- /dev/null +++ b/pages/mine/mine.wxml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + {{userInfo.points || 0}} + 积分 + + + + {{downloadCount}} + 下载数 + + + + {{inviteCount}} + 邀请数 + + + + + + + + 📥 + 我的下载 + + + + + + + + + 🗑️ + 清除缓存 + + {{cacheSize}} + + + + + ℹ️ + 关于我们 + + v1.0.0 + + + + + + 🚪 + 退出登录 + + + + + + + diff --git a/pages/mine/mine.wxss b/pages/mine/mine.wxss new file mode 100644 index 0000000..c314b4a --- /dev/null +++ b/pages/mine/mine.wxss @@ -0,0 +1,186 @@ +/* 我的页面样式 */ +.container { + min-height: 100vh; + background: #f5f5f5; + padding-bottom: env(safe-area-inset-bottom); +} + +/* 用户信息 */ +.user-section { + background: linear-gradient(135deg, #4CAF50 0%, #8BC34A 100%); + padding: 60rpx 40rpx 80rpx; +} + +.user-section-unlogin { + padding-bottom: 80rpx; +} + +.user-info { + display: flex; + align-items: center; +} + +.user-avatar { + width: 120rpx; + height: 120rpx; + border-radius: 60rpx; + border: 4rpx solid rgba(255, 255, 255, 0.3); + background: #fff; +} + +.user-detail { + margin-left: 30rpx; + flex: 1; +} + +.user-name { + display: block; + font-size: 36rpx; + font-weight: bold; + color: #fff; + margin-bottom: 8rpx; +} + +.user-desc { + font-size: 26rpx; + color: rgba(255, 255, 255, 0.8); +} + +/* 积分显示 */ +.points-box { + display: flex; + align-items: center; + margin-top: 30rpx; + padding: 20rpx 24rpx; + background: rgba(255, 255, 255, 0.2); + border-radius: 12rpx; +} + +.points-label { + font-size: 26rpx; + color: rgba(255, 255, 255, 0.9); +} + +.points-value { + flex: 1; + text-align: right; + font-size: 36rpx; + font-weight: bold; + color: #fff; + margin-right: 10rpx; +} + +.points-arrow { + font-size: 32rpx; + color: rgba(255, 255, 255, 0.7); +} + +/* 统计信息 */ +.stats-section { + display: flex; + align-items: center; + justify-content: space-around; + background: #fff; + margin: -40rpx 30rpx 0; + padding: 30rpx 0; + border-radius: 16rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08); +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; +} + +.stat-num { + font-size: 40rpx; + font-weight: bold; + color: #4CAF50; +} + +.stat-label { + font-size: 24rpx; + color: #999; + margin-top: 8rpx; +} + +.stat-divider { + width: 1rpx; + height: 60rpx; + background: #eee; +} + +/* 功能列表 */ +.menu-section { + background: #fff; + margin: 30rpx; + border-radius: 16rpx; + overflow: hidden; +} + +.menu-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 30rpx; + border-bottom: 1rpx solid #f5f5f5; + background: #fff; +} + +.menu-item:last-child { + border-bottom: none; +} + +/* 分享按钮样式重置 */ +.menu-btn { + all: unset; + +} + +.menu-btn::after { + border: none; +} + +.menu-left { + display: flex; + align-items: center; +} + +.menu-icon { + width: 44rpx; + height: 44rpx; + margin-right: 20rpx; +} + +.menu-icon-emoji { + font-size: 40rpx; + margin-right: 20rpx; +} + +.menu-text { + font-size: 30rpx; + color: #333; +} + +.menu-arrow { + font-size: 32rpx; + color: #ccc; +} + +.menu-extra { + font-size: 26rpx; + color: #4CAF50; +} + +/* 版权信息 */ +.copyright { + text-align: center; + padding: 40rpx 0; +} + +.copyright text { + font-size: 24rpx; + color: #ccc; +} diff --git a/pages/search/search.js b/pages/search/search.js new file mode 100644 index 0000000..b285f9d --- /dev/null +++ b/pages/search/search.js @@ -0,0 +1,160 @@ +import { injectPage } from '@jdmini/api' +const { searchWorksheets } = require('../../utils/api.js') +const { DATA_BASE_URL } = require('../../utils/config.js') + +Page(injectPage({})({ + data: { + dataBaseUrl: DATA_BASE_URL, + keyword: '', + history: [], + hotKeywords: ['字母', '数字', '涂色', '迷宫', '折纸', '形状'], + worksheets: [], + page: 1, + pageSize: 20, + total: 0, + hasMore: true, + loading: false, + hasSearched: false + }, + + onLoad() { + this.loadHistory() + }, + + // 加载搜索历史 + loadHistory() { + try { + const history = wx.getStorageSync('searchHistory') || [] + this.setData({ history }) + } catch (error) { + console.error('加载搜索历史失败:', error) + } + }, + + // 保存搜索历史 + saveHistory(keyword) { + try { + let history = wx.getStorageSync('searchHistory') || [] + + // 去重 + history = history.filter(h => h !== keyword) + // 添加到开头 + history.unshift(keyword) + // 最多保存10条 + if (history.length > 10) { + history = history.slice(0, 10) + } + + wx.setStorageSync('searchHistory', history) + this.setData({ history }) + } catch (error) { + console.error('保存搜索历史失败:', error) + } + }, + + // 输入事件 + onInput(e) { + this.setData({ + keyword: e.detail.value + }) + }, + + // 清除关键词 + clearKeyword() { + this.setData({ + keyword: '', + hasSearched: false, + worksheets: [] + }) + }, + + // 执行搜索 + async doSearch() { + const keyword = this.data.keyword.trim() + if (!keyword) return + + // 保存搜索历史 + this.saveHistory(keyword) + + this.setData({ + page: 1, + hasMore: true, + worksheets: [], + hasSearched: true + }) + + await this.loadResults() + }, + + // 点击历史或热门关键词搜索 + searchHistory(e) { + const keyword = e.currentTarget.dataset.keyword + this.setData({ keyword }) + this.doSearch() + }, + + // 加载搜索结果 + async loadResults() { + if (this.data.loading) return + + try { + this.setData({ loading: true }) + + const res = await searchWorksheets({ + keyword: this.data.keyword, + page: this.data.page, + pageSize: this.data.pageSize + }) + + if (res.success) { + const newWorksheets = res.data.list || [] + this.setData({ + worksheets: this.data.page === 1 ? newWorksheets : [...this.data.worksheets, ...newWorksheets], + total: res.data.pagination?.total || 0, + hasMore: newWorksheets.length >= this.data.pageSize, + loading: false + }) + } + } catch (error) { + console.error('搜索失败:', error) + this.setData({ loading: false }) + } + }, + + // 加载更多 + async loadMore() { + if (this.data.loading || !this.data.hasMore) return + + this.setData({ + page: this.data.page + 1 + }) + await this.loadResults() + }, + + // 清除历史 + clearHistory() { + wx.showModal({ + title: '提示', + content: '确定清除搜索历史吗?', + success: (res) => { + if (res.confirm) { + wx.removeStorageSync('searchHistory') + this.setData({ history: [] }) + } + } + }) + }, + + // 返回 + goBack() { + wx.navigateBack() + }, + + // 跳转详情 + goDetail(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ + url: `/pages/detail/detail?id=${id}` + }) + } +})) diff --git a/pages/search/search.json b/pages/search/search.json new file mode 100644 index 0000000..ab78991 --- /dev/null +++ b/pages/search/search.json @@ -0,0 +1,4 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "搜索" +} diff --git a/pages/search/search.wxml b/pages/search/search.wxml new file mode 100644 index 0000000..beaaf63 --- /dev/null +++ b/pages/search/search.wxml @@ -0,0 +1,92 @@ + + + + + + + + + + 取消 + + + + + + 搜索历史 + + + + + {{item}} + + + + + + + + 热门搜索 + + + + {{item}} + + + + + + + + 找到 {{total}} 个相关结果 + + + + + + + {{item.title}} + {{item.e_title}} + {{item.category_name}} + + + + + + + 加载中... + 已加载全部 + 加载更多 + + + + + + 未找到相关练习表 + 换个关键词试试吧 + + + diff --git a/pages/search/search.wxss b/pages/search/search.wxss new file mode 100644 index 0000000..a3c3ee9 --- /dev/null +++ b/pages/search/search.wxss @@ -0,0 +1,192 @@ +/* 搜索页面样式 */ +.container { + min-height: 100vh; + background: #f5f5f5; +} + +/* 搜索栏 */ +.search-bar { + display: flex; + align-items: center; + padding: 20rpx 30rpx; + background: #fff; +} + +.search-input-wrap { + flex: 1; + display: flex; + align-items: center; + padding: 16rpx 24rpx; + background: #f5f5f5; + border-radius: 40rpx; +} + +.search-icon { + width: 36rpx; + height: 36rpx; + margin-right: 16rpx; + opacity: 0.5; +} + +.search-input { + flex: 1; + font-size: 28rpx; + color: #333; +} + +.clear-icon { + width: 32rpx; + height: 32rpx; + margin-left: 16rpx; + opacity: 0.5; +} + +.cancel-btn { + margin-left: 20rpx; + font-size: 28rpx; + color: #4CAF50; +} + +/* 搜索历史 */ +.history-section, +.hot-section { + background: #fff; + padding: 30rpx; + margin-top: 20rpx; +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20rpx; +} + +.section-title { + font-size: 30rpx; + font-weight: bold; + color: #333; +} + +.delete-icon { + width: 36rpx; + height: 36rpx; + opacity: 0.5; +} + +.history-tags, +.hot-tags { + display: flex; + flex-wrap: wrap; +} + +.history-tag, +.hot-tag { + padding: 12rpx 24rpx; + margin: 0 16rpx 16rpx 0; + background: #f5f5f5; + border-radius: 30rpx; + font-size: 26rpx; + color: #666; +} + +.hot-tag { + background: linear-gradient(135deg, rgba(76, 175, 80, 0.1), rgba(139, 195, 74, 0.1)); + color: #4CAF50; +} + +/* 搜索结果 */ +.result-section { + padding: 20rpx 30rpx; +} + +.result-header { + margin-bottom: 20rpx; +} + +.result-header text { + font-size: 26rpx; + color: #999; +} + +.worksheet-list { + background: #fff; + border-radius: 16rpx; + overflow: hidden; +} + +.worksheet-item { + display: flex; + padding: 20rpx; + border-bottom: 1rpx solid #f5f5f5; +} + +.worksheet-item:last-child { + border-bottom: none; +} + +.item-cover { + width: 180rpx; + height: 135rpx; + border-radius: 12rpx; + background: #f5f5f5; + flex-shrink: 0; +} + +.item-info { + flex: 1; + margin-left: 20rpx; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.item-title { + font-size: 28rpx; + color: #333; + line-height: 1.4; +} + +.item-e-title { + font-size: 24rpx; + color: #999; +} + +.item-category { + font-size: 22rpx; + color: #4CAF50; +} + +/* 加载状态 */ +.load-status { + text-align: center; + padding: 30rpx 0; + font-size: 26rpx; + color: #999; +} + +/* 无结果 */ +.empty-result { + display: flex; + flex-direction: column; + align-items: center; + padding: 100rpx 0; +} + +.empty-img { + width: 200rpx; + height: 200rpx; + margin-bottom: 30rpx; + opacity: 0.5; +} + +.empty-text { + font-size: 30rpx; + color: #333; + margin-bottom: 12rpx; +} + +.empty-tip { + font-size: 26rpx; + color: #999; +} 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..ea80ccd --- /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", + "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..d171331 --- /dev/null +++ b/utils/api.js @@ -0,0 +1,140 @@ +/** + * API 接口定义 + * 所有业务 API 接口的统一管理 + */ + +const { get, post } = require('./request.js') + +/** + * =============================== + * 首页相关接口 + * =============================== + */ + +/** + * 获取首页数据(分类 + 最新工作表) + */ +function getHomeData() { + return get('/api/home') +} + +/** + * 获取所有分类 + */ +function getCategories() { + return get('/api/categories') +} + +/** + * =============================== + * 工作表相关接口 + * =============================== + */ + +/** + * 获取工作表列表 + * @param {Object} params + * @param {Number} params.category_id - 分类ID(可选) + * @param {Number} params.page - 页码 + * @param {Number} params.pageSize - 每页数量 + * @param {String} params.keyword - 搜索关键词 + */ +function getWorksheets(params = {}) { + return get('/api/worksheets', params) +} + +/** + * 获取工作表详情 + * @param {Number} id - 工作表ID + */ +function getWorksheetDetail(id) { + return get(`/api/worksheets/${id}`) +} + +/** + * 记录下载次数 + * @param {Number} id - 工作表ID + */ +function recordDownload(id) { + return post(`/api/worksheets/${id}/download`) +} + +/** + * 搜索工作表 + * @param {Object} params + * @param {String} params.keyword - 搜索关键词 + * @param {Number} params.page - 页码 + * @param {Number} params.pageSize - 每页数量 + */ +function searchWorksheets(params = {}) { + return get('/api/search', params) +} + +/** + * =============================== + * 用户相关接口 + * =============================== + */ + +/** + * 用户登录/注册 + * @param {String} openid - 微信 openid + * @param {String} nickname - 昵称 + * @param {String} avatar - 头像URL + * @param {Number} inviter_id - 邀请人ID(可选) + */ +function userLogin(openid, nickname, avatar, inviter_id) { + return post('/api/user/login', { openid, nickname, avatar, inviter_id }) +} + +/** + * 获取用户信息 + * @param {Number} user_id - 用户ID + */ +function getUserInfo(user_id) { + return get('/api/user/info', { user_id }) +} + +/** + * 检查积分是否足够下载 + * @param {Number} user_id - 用户ID + */ +function checkPoints(user_id) { + return get('/api/user/check-points', { user_id }) +} + +/** + * 扣除积分(下载时调用) + * @param {Number} user_id - 用户ID + * @param {Number} worksheet_id - 工作表ID + */ +function deductPoints(user_id, worksheet_id) { + return post('/api/user/deduct-points', { user_id, worksheet_id }) +} + +/** + * 获取积分记录 + * @param {Number} user_id - 用户ID + * @param {Number} page - 页码 + * @param {Number} pageSize - 每页数量 + */ +function getPointsLog(user_id, page = 1, pageSize = 20) { + return get('/api/user/points-log', { user_id, page, pageSize }) +} + +module.exports = { + // 首页 + getHomeData, + getCategories, + // 工作表 + getWorksheets, + getWorksheetDetail, + recordDownload, + searchWorksheets, + // 用户 + userLogin, + getUserInfo, + checkPoints, + deductPoints, + getPointsLog +} 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..bb33170 --- /dev/null +++ b/utils/config.js @@ -0,0 +1,40 @@ +/** + * API 配置文件 + * 统一管理开发环境和生产环境的配置 + */ + +// 开发模式开关 +const IS_DEV = false + +// 开发环境配置 +const DEV_CONFIG = { + apiBase: 'http://localhost:3001', + timeout: 30000, + enableLog: true +} + +// 生产环境配置 +const PROD_CONFIG = { + apiBase: '/mp/jd-youerqimeng', // 幼儿启蒙模块 + timeout: 30000, + enableLog: false +} + +// 当前环境配置 +const CONFIG = IS_DEV ? DEV_CONFIG : PROD_CONFIG + +// 数据资源地址前缀 +const DATA_BASE_URL = 'https://pic.miniappapi.com/youerqimeng' + +module.exports = { + IS_DEV, + API_BASE: CONFIG.apiBase, + DATA_BASE_URL, + TIMEOUT: CONFIG.timeout, + ENABLE_LOG: CONFIG.enableLog, + + // 切换环境方法(用于调试) + switchEnv: (isDev) => { + return isDev ? DEV_CONFIG : PROD_CONFIG + } +} 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 +}