first commit
|
|
@ -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: () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
}))
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
After Width: | Height: | Size: 779 B |
|
After Width: | Height: | Size: 566 B |
|
After Width: | Height: | Size: 619 B |
|
After Width: | Height: | Size: 531 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 811 B |
|
After Width: | Height: | Size: 821 B |
|
After Width: | Height: | Size: 725 B |
|
After Width: | Height: | Size: 550 B |
|
After Width: | Height: | Size: 504 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -0,0 +1 @@
|
|||
/d/code/youerqimeng-server/miniapp/images
|
||||
|
|
@ -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<void>;
|
||||
export function showPage(options: PageOptions | undefined, pageId: string): Promise<void>;
|
||||
export const checkTokenValid: () => boolean;
|
||||
/**
|
||||
* 确保登录完成
|
||||
* @param {Function} callback - 回调函数
|
||||
* @returns {void}
|
||||
*/
|
||||
export function onLoginReady(callback: (...args: any[]) => void): void;
|
||||
/**
|
||||
* 等待登录完成
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function waitLogin(): Promise<void>;
|
||||
export function login(): Promise<void>;
|
||||
export function fetchEchoData(): Promise<void>;
|
||||
export function trackVisit(): Promise<void>;
|
||||
}
|
||||
|
||||
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<Object>} 返回一个Promise对象
|
||||
*/
|
||||
request<T = any>(path: string, method?: WechatMiniprogram.RequestOption['method'], data?: Record<string, any>, options?: RequestOptions): Promise<ApiResponse<T>>;
|
||||
/**
|
||||
* 上传文件
|
||||
* @param {string} filePath 文件路径
|
||||
* @param {Object} data 数据, 默认{}
|
||||
* @param {'avatar' | 'file'} type 类型, 默认'file'
|
||||
* @returns {Promise<Object>} 返回一个Promise对象
|
||||
*/
|
||||
uploadFile<T = any>(filePath: string, data?: Record<string, any>, type?: 'avatar' | 'file'): Promise<ApiResponse<T>>;
|
||||
/**
|
||||
* 上传文件
|
||||
* @param {string} filePath 文件路径
|
||||
* @param {Object} data 数据, 默认{}
|
||||
* @param {'avatar' | 'file'} type 类型, 默认'file'
|
||||
* @returns {Promise<Object>} 返回一个Promise对象
|
||||
*/
|
||||
upload<T = any>(path: string, filePath: string, data?: Record<string, any>): Promise<ApiResponse<T>>;
|
||||
/**
|
||||
* 删除文件
|
||||
* @param {number} fileId 文件id
|
||||
* @returns {Promise<Object>} 返回一个Promise对象
|
||||
*/
|
||||
deleteFile(fileId: number): Promise<ApiResponse<null>>;
|
||||
/**
|
||||
* 上传头像
|
||||
* @param {string} filePath 文件路径
|
||||
* @returns {Promise<Object>} 返回一个Promise对象
|
||||
*/
|
||||
uploadAvatar<T = any>(filePath: string): Promise<ApiResponse<T>>;
|
||||
}
|
||||
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<void>;
|
||||
[key: string]: any;
|
||||
}
|
||||
interface PageConfig {
|
||||
onShow?: (...args: any[]) => void | Promise<void>;
|
||||
[key: string]: any;
|
||||
}
|
||||
interface ComponentConfig {
|
||||
methods?: {
|
||||
onLoad?: (...args: any[]) => void | Promise<void>;
|
||||
onShow?: (...args: any[]) => void | Promise<void>;
|
||||
[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<Record<AdData['appPage'], AdData['ads'][0]['adUnitId']>>;
|
||||
class AdManager {
|
||||
/**
|
||||
* 广告数据
|
||||
*/
|
||||
ads: Ads;
|
||||
/**
|
||||
* 友情链接数据
|
||||
*/
|
||||
link: LinkData[];
|
||||
/**
|
||||
* 友情链接顶部广告数据
|
||||
*/
|
||||
top: TopData | null;
|
||||
constructor();
|
||||
/**
|
||||
* 确保广告数据就绪
|
||||
* @param {Function} callback - 回调函数
|
||||
* @returns {void}
|
||||
*/
|
||||
onDataReady: (callback: (...args: any[]) => void) => void;
|
||||
/**
|
||||
* 等待广告数据加载完成
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
waitAdData: () => Promise<void>;
|
||||
/**
|
||||
* 初始化广告数据
|
||||
* @returns {void}
|
||||
*/
|
||||
init: () => void;
|
||||
/**
|
||||
* 创建并展示插屏广告
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
createAndShowInterstitialAd: () => Promise<void>;
|
||||
/**
|
||||
* 创建并展示激励视频广告
|
||||
* @param {any} context - 页面上下文
|
||||
* @param {string} [pageId] - 页面ID
|
||||
* @returns {Promise<boolean>} 是否完成播放
|
||||
*/
|
||||
createAndShowRewardedVideoAd: (context: any, pageId?: string) => Promise<boolean>;
|
||||
}
|
||||
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<string, string>;
|
||||
[key: string]: any;
|
||||
}
|
||||
export interface LoginData {
|
||||
appId: number;
|
||||
code: string;
|
||||
brand: string;
|
||||
model: string;
|
||||
platform: string;
|
||||
}
|
||||
export interface ApiResponse<T = any> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -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: {
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"component": true
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<view class="jdwx-ad-component">
|
||||
<ad wx:if="{{type === 'banner' && ads.banner}}" class="jdwx-ad-item" unit-id="{{ads.banner}}"></ad>
|
||||
<ad wx:if="{{type === 'video' && ads.video}}" class="jdwx-ad-item" ad-type="video" unit-id="{{ads.video}}"></ad>
|
||||
<ad-custom wx:if="{{type === 'custom' && ads.custom}}" class="jdwx-ad-item" unit-id="{{ads.custom}}"></ad-custom>
|
||||
</view>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.jdwx-ad-component {
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.jdwx-ad-item {
|
||||
bottom: 10rpx;
|
||||
}
|
||||
|
|
@ -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
|
||||
});
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"component": true
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<view class="jdwx-link-component">
|
||||
<view wx:if="{{top.appId}}" class="jdwx-applink-top" bindtap="gotopLink">
|
||||
<view><image src="{{top.appLogo}}" class="jdwx-applink-icon" /> </view>
|
||||
<view ><text class="jdwx-applink-top-linkname">{{top.linkName}}</text>
|
||||
<text class="jdwx-applink-top-text">{{top.appDsc}}</text> </view>
|
||||
</view>
|
||||
<view id="{{bindex}}" bindtap="goLink" wx:for="{{link}}" wx:for-index="bindex" wx:key="index" class="jdwx-applink-list">
|
||||
<image src="{{item.appLogo}}" class="jdwx-applink-icon" />
|
||||
<text class="jdwx-applink-text">{{item.linkName}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) => {
|
||||
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
|
@ -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<void>;
|
||||
export function showPage(options: PageOptions | undefined, pageId: string): Promise<void>;
|
||||
export const checkTokenValid: () => boolean;
|
||||
/**
|
||||
* 确保登录完成
|
||||
* @param {Function} callback - 回调函数
|
||||
* @returns {void}
|
||||
*/
|
||||
export function onLoginReady(callback: (...args: any[]) => void): void;
|
||||
/**
|
||||
* 等待登录完成
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function waitLogin(): Promise<void>;
|
||||
export function login(): Promise<void>;
|
||||
export function fetchEchoData(): Promise<void>;
|
||||
export function trackVisit(): Promise<void>;
|
||||
}
|
||||
|
||||
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<Object>} 返回一个Promise对象
|
||||
*/
|
||||
request<T = any>(path: string, method?: WechatMiniprogram.RequestOption['method'], data?: Record<string, any>, options?: RequestOptions): Promise<ApiResponse<T>>;
|
||||
/**
|
||||
* 上传文件
|
||||
* @param {string} filePath 文件路径
|
||||
* @param {Object} data 数据, 默认{}
|
||||
* @param {'avatar' | 'file'} type 类型, 默认'file'
|
||||
* @returns {Promise<Object>} 返回一个Promise对象
|
||||
*/
|
||||
uploadFile<T = any>(filePath: string, data?: Record<string, any>, type?: 'avatar' | 'file'): Promise<ApiResponse<T>>;
|
||||
/**
|
||||
* 上传文件
|
||||
* @param {string} filePath 文件路径
|
||||
* @param {Object} data 数据, 默认{}
|
||||
* @param {'avatar' | 'file'} type 类型, 默认'file'
|
||||
* @returns {Promise<Object>} 返回一个Promise对象
|
||||
*/
|
||||
upload<T = any>(path: string, filePath: string, data?: Record<string, any>): Promise<ApiResponse<T>>;
|
||||
/**
|
||||
* 删除文件
|
||||
* @param {number} fileId 文件id
|
||||
* @returns {Promise<Object>} 返回一个Promise对象
|
||||
*/
|
||||
deleteFile(fileId: number): Promise<ApiResponse<null>>;
|
||||
/**
|
||||
* 上传头像
|
||||
* @param {string} filePath 文件路径
|
||||
* @returns {Promise<Object>} 返回一个Promise对象
|
||||
*/
|
||||
uploadAvatar<T = any>(filePath: string): Promise<ApiResponse<T>>;
|
||||
}
|
||||
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<void>;
|
||||
[key: string]: any;
|
||||
}
|
||||
interface PageConfig {
|
||||
onShow?: (...args: any[]) => void | Promise<void>;
|
||||
[key: string]: any;
|
||||
}
|
||||
interface ComponentConfig {
|
||||
methods?: {
|
||||
onLoad?: (...args: any[]) => void | Promise<void>;
|
||||
onShow?: (...args: any[]) => void | Promise<void>;
|
||||
[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<Record<AdData['appPage'], AdData['ads'][0]['adUnitId']>>;
|
||||
class AdManager {
|
||||
/**
|
||||
* 广告数据
|
||||
*/
|
||||
ads: Ads;
|
||||
/**
|
||||
* 友情链接数据
|
||||
*/
|
||||
link: LinkData[];
|
||||
/**
|
||||
* 友情链接顶部广告数据
|
||||
*/
|
||||
top: TopData | null;
|
||||
constructor();
|
||||
/**
|
||||
* 确保广告数据就绪
|
||||
* @param {Function} callback - 回调函数
|
||||
* @returns {void}
|
||||
*/
|
||||
onDataReady: (callback: (...args: any[]) => void) => void;
|
||||
/**
|
||||
* 等待广告数据加载完成
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
waitAdData: () => Promise<void>;
|
||||
/**
|
||||
* 初始化广告数据
|
||||
* @returns {void}
|
||||
*/
|
||||
init: () => void;
|
||||
/**
|
||||
* 创建并展示插屏广告
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
createAndShowInterstitialAd: () => Promise<void>;
|
||||
/**
|
||||
* 创建并展示激励视频广告
|
||||
* @param {any} context - 页面上下文
|
||||
* @param {string} [pageId] - 页面ID
|
||||
* @returns {Promise<boolean>} 是否完成播放
|
||||
*/
|
||||
createAndShowRewardedVideoAd: (context: any, pageId?: string) => Promise<boolean>;
|
||||
}
|
||||
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<string, string>;
|
||||
[key: string]: any;
|
||||
}
|
||||
export interface LoginData {
|
||||
appId: number;
|
||||
code: string;
|
||||
brand: string;
|
||||
model: string;
|
||||
platform: string;
|
||||
}
|
||||
export interface ApiResponse<T = any> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "@jdmini/api",
|
||||
"version": "1.0.10",
|
||||
"main": "miniprogram_dist/index.js",
|
||||
"files": [
|
||||
"miniprogram_dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"pub": "npm run build && npm publish --access public",
|
||||
"build:js": "tsc --project tsconfig_tsc.json"
|
||||
},
|
||||
"miniprogram": "miniprogram_dist",
|
||||
"author": "",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@types/wechat-miniprogram": "^3.4.8",
|
||||
"dts-bundle": "^0.7.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-cli": "^5.1.4"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
## 安装/更新
|
||||
|
||||
1、终端使用命令安装 npm 包
|
||||
|
||||
```bash
|
||||
npm install --save @jdmini/components@latest
|
||||
```
|
||||
|
||||
2、在小程序开发者工具中:菜单选择工具 -> 构建 npm
|
||||
|
||||
`注意:依赖@jdmini/api,请确保小程序项目已安装@jdmini/api`
|
||||
|
||||
## 使用
|
||||
|
||||
1、在页面的 json 文件中引入组件:
|
||||
|
||||
```json
|
||||
{
|
||||
"usingComponents": {
|
||||
"jdwx-ad": "@jdmini/components/jdwx-ad",
|
||||
"jdwx-link": "@jdmini/components/jdwx-link"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2、在页面的 wxml 文件中使用组件:
|
||||
|
||||
```html
|
||||
<jdwx-ad type="custom" />
|
||||
<jdwx-link />
|
||||
```
|
||||
BIN
node_modules/@jdmini/components/miniprogram_dist/icons/home-active.png
generated
vendored
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
BIN
node_modules/@jdmini/components/miniprogram_dist/icons/link-active.png
generated
vendored
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -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: {
|
||||
}
|
||||
})
|
||||
3
node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.json
generated
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"component": true
|
||||
}
|
||||
5
node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.wxml
generated
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<view class="jdwx-ad-component">
|
||||
<ad wx:if="{{type === 'banner' && ads.banner}}" class="jdwx-ad-item" unit-id="{{ads.banner}}"></ad>
|
||||
<ad wx:if="{{type === 'video' && ads.video}}" class="jdwx-ad-item" ad-type="video" unit-id="{{ads.video}}"></ad>
|
||||
<ad-custom wx:if="{{type === 'custom' && ads.custom}}" class="jdwx-ad-item" unit-id="{{ads.custom}}"></ad-custom>
|
||||
</view>
|
||||
7
node_modules/@jdmini/components/miniprogram_dist/jdwx-ad/index.wxss
generated
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.jdwx-ad-component {
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.jdwx-ad-item {
|
||||
bottom: 10rpx;
|
||||
}
|
||||
37
node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.js
generated
vendored
Normal file
|
|
@ -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
|
||||
});
|
||||
},
|
||||
}
|
||||
})
|
||||
3
node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.json
generated
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"component": true
|
||||
}
|
||||
11
node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.wxml
generated
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<view class="jdwx-link-component">
|
||||
<view wx:if="{{top.appId}}" class="jdwx-applink-top" bindtap="gotopLink">
|
||||
<view><image src="{{top.appLogo}}" class="jdwx-applink-icon" /> </view>
|
||||
<view ><text class="jdwx-applink-top-linkname">{{top.linkName}}</text>
|
||||
<text class="jdwx-applink-top-text">{{top.appDsc}}</text> </view>
|
||||
</view>
|
||||
<view id="{{bindex}}" bindtap="goLink" wx:for="{{link}}" wx:for-index="bindex" wx:key="index" class="jdwx-applink-list">
|
||||
<image src="{{item.appLogo}}" class="jdwx-applink-icon" />
|
||||
<text class="jdwx-applink-text">{{item.linkName}}</text>
|
||||
</view>
|
||||
</view>
|
||||
63
node_modules/@jdmini/components/miniprogram_dist/jdwx-link/index.wxss
generated
vendored
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"@jdmini/api": "^1.0.10",
|
||||
"@jdmini/components": "^1.0.6"
|
||||
}
|
||||
}
|
||||
|
|
@ -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}`
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"usingComponents": {},
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundTextStyle": "dark"
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<!-- 分类详情页面 -->
|
||||
<view class="container">
|
||||
<!-- 练习表网格 -->
|
||||
<view class="worksheet-grid">
|
||||
<view class="worksheet-item"
|
||||
wx:for="{{worksheets}}"
|
||||
wx:key="id"
|
||||
bindtap="goDetail"
|
||||
data-id="{{item.id}}">
|
||||
<view class="worksheet-cover">
|
||||
<image class="cover-img" src="{{dataBaseUrl}}{{item.coverurl}}" mode="aspectFill" lazy-load="{{true}}"></image>
|
||||
</view>
|
||||
<view class="worksheet-info">
|
||||
<text class="worksheet-title text-ellipsis-2">{{item.title}}</text>
|
||||
<text class="worksheet-e-title text-ellipsis">{{item.e_title}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="load-status" wx:if="{{worksheets.length > 0}}">
|
||||
<text wx:if="{{loading}}">加载中...</text>
|
||||
<text wx:elif="{{!hasMore}}">已加载全部</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-tip" wx:if="{{!loading && worksheets.length === 0}}">
|
||||
<text>暂无练习表</text>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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}`
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"usingComponents": {},
|
||||
"navigationBarTitleText": "分类",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundTextStyle": "dark"
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<!-- 分类页面 -->
|
||||
<view class="container">
|
||||
<!-- 左侧分类列表 -->
|
||||
<scroll-view class="category-sidebar" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view class="category-item {{currentCategory === item.id ? 'active' : ''}}"
|
||||
wx:for="{{categories}}"
|
||||
wx:key="id"
|
||||
bindtap="selectCategory"
|
||||
data-id="{{item.id}}">
|
||||
<view class="category-indicator" wx:if="{{currentCategory === item.id}}"></view>
|
||||
<text class="category-name">{{item.name}}</text>
|
||||
<text class="category-e-name">{{item.e_name}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<scroll-view class="content-area"
|
||||
scroll-y="{{true}}"
|
||||
enhanced="{{true}}"
|
||||
show-scrollbar="{{false}}"
|
||||
bindscrolltolower="loadMore">
|
||||
<!-- 分类标题 -->
|
||||
<view class="content-header">
|
||||
<text class="content-title">{{currentCategoryName}}</text>
|
||||
<text class="content-count">共{{total}}个练习表</text>
|
||||
</view>
|
||||
|
||||
<!-- 练习表网格 -->
|
||||
<view class="worksheet-grid">
|
||||
<view class="worksheet-item"
|
||||
wx:for="{{worksheets}}"
|
||||
wx:key="id"
|
||||
bindtap="goDetail"
|
||||
data-id="{{item.id}}">
|
||||
<view class="worksheet-cover">
|
||||
<image class="cover-img" src="{{dataBaseUrl}}{{item.coverurl}}" mode="aspectFill" lazy-load="{{true}}"></image>
|
||||
</view>
|
||||
<view class="worksheet-info">
|
||||
<text class="worksheet-title text-ellipsis-2">{{item.title}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="load-status">
|
||||
<text wx:if="{{loading}}">加载中...</text>
|
||||
<text wx:elif="{{!hasMore && worksheets.length > 0}}">已加载全部</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-tip" wx:if="{{!loading && worksheets.length === 0}}">
|
||||
|
||||
<text>暂无练习表</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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}`
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"usingComponents": {},
|
||||
"navigationBarTitleText": "练习表详情"
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<!-- 详情页面 -->
|
||||
<view class="container">
|
||||
<!-- 封面预览 -->
|
||||
<view class="preview-section">
|
||||
<image class="preview-image"
|
||||
src="{{dataBaseUrl}}{{detail.coverurl}}"
|
||||
mode="aspectFit"
|
||||
bindtap="previewImage"></image>
|
||||
<view class="preview-tip">点击图片预览大图</view>
|
||||
</view>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<view class="info-section">
|
||||
<view class="info-header">
|
||||
<view class="category-tag">{{detail.category_name}}</view>
|
||||
<view class="stats">
|
||||
<text class="stat-item">浏览 {{detail.view_count || 0}}</text>
|
||||
<text class="stat-item">下载 {{detail.download_count || 0}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="info-title">{{detail.title}}</text>
|
||||
<text class="info-e-title">{{detail.e_title}}</text>
|
||||
<text class="info-desc" wx:if="{{detail.des}}">{{detail.des}}</text>
|
||||
</view>
|
||||
|
||||
<view class="tips-box">
|
||||
<text class="tips-icon">💡</text>
|
||||
<text class="tips-text">Tips: 打开PDF点右上角'···'选择你打印机打印</text>
|
||||
</view>
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-section">
|
||||
|
||||
<view class="action-btn download-btn" bindtap="downloadPdf">
|
||||
<text>下载PDF</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 相关推荐 -->
|
||||
<view class="recommend-section" wx:if="{{recommended.length > 0}}">
|
||||
<view class="section-header">
|
||||
<text class="section-title">相关推荐</text>
|
||||
</view>
|
||||
<scroll-view class="recommend-scroll" scroll-x="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
|
||||
<view class="recommend-list">
|
||||
<view class="recommend-item"
|
||||
wx:for="{{recommended}}"
|
||||
wx:key="id"
|
||||
bindtap="goDetail"
|
||||
data-id="{{item.id}}">
|
||||
<image class="recommend-cover" src="{{dataBaseUrl}}{{item.coverurl}}" mode="aspectFill" lazy-load="{{true}}"></image>
|
||||
<text class="recommend-title text-ellipsis">{{item.title}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部安全区域 -->
|
||||
<view class="safe-bottom"></view>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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'
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"usingComponents": {},
|
||||
"navigationBarTitleText": "我的下载"
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<!-- 下载页面 -->
|
||||
<view class="container">
|
||||
<!-- 下载列表 -->
|
||||
<view class="download-list" wx:if="{{downloads.length > 0}}">
|
||||
<view class="download-item"
|
||||
wx:for="{{downloads}}"
|
||||
wx:key="id">
|
||||
<view class="item-content" bindtap="goDetail" data-id="{{item.id}}">
|
||||
<image class="item-cover" src="{{dataBaseUrl}}{{item.coverurl}}" mode="aspectFill"></image>
|
||||
<view class="item-info">
|
||||
<text class="item-title text-ellipsis-2">{{item.title}}</text>
|
||||
<text class="item-category">{{item.category_name}}</text>
|
||||
<text class="item-time">下载时间: {{item.downloadTimeStr}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-actions">
|
||||
<view class="action-btn" bindtap="openFile" data-index="{{index}}">
|
||||
<text>打开</text>
|
||||
</view>
|
||||
<view class="action-btn delete" bindtap="deleteItem" data-index="{{index}}">
|
||||
<text>删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" wx:else>
|
||||
<image class="empty-img" src="/images/icons/empty-download.png" mode="aspectFit"></image>
|
||||
<text class="empty-title">暂无下载记录</text>
|
||||
<text class="empty-desc">浏览练习表并下载PDF文件后,将在此处显示</text>
|
||||
<view class="empty-btn" bindtap="goHome">
|
||||
<text>去浏览</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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)}`
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"usingComponents": {},
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundTextStyle": "dark"
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<!-- 首页 -->
|
||||
<view class="container">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar" bindtap="goSearch">
|
||||
<view class="search-input">
|
||||
<image class="search-icon" src="/images/icons/search.png" mode="aspectFit"></image>
|
||||
<text class="search-placeholder">搜索练习表...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Banner轮播图
|
||||
<view class="banner-section">
|
||||
<swiper class="banner-swiper"
|
||||
indicator-dots="{{true}}"
|
||||
autoplay="{{true}}"
|
||||
interval="{{3000}}"
|
||||
circular="{{true}}"
|
||||
indicator-color="rgba(255,255,255,0.5)"
|
||||
indicator-active-color="#ffffff">
|
||||
<swiper-item wx:for="{{banners}}" wx:key="id">
|
||||
<view class="banner-item" style="background: {{item.bgColor}}">
|
||||
<view class="banner-content">
|
||||
<text class="banner-title">{{item.title}}</text>
|
||||
<text class="banner-desc">{{item.desc}}</text>
|
||||
</view>
|
||||
<image class="banner-img" src="{{item.image}}" mode="aspectFit"></image>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
-->
|
||||
<!-- 分类列表 -->
|
||||
<view class="category-list-section">
|
||||
<view class="category-card"
|
||||
wx:for="{{worksheets}}"
|
||||
wx:key="id"
|
||||
bindtap="goCategory"
|
||||
data-id="{{item.category_id}}">
|
||||
<view class="category-cover">
|
||||
<image class="cover-img" src="{{dataBaseUrl}}{{item.coverurl}}" mode="aspectFill" lazy-load="{{true}}"></image>
|
||||
<view class="category-info">
|
||||
<text class="category-name">{{item.category_name}}</text>
|
||||
<text class="category-e-name">{{item.category_e_name}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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'
|
||||
});
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"navigationBarTitleText": "登录",
|
||||
"usingComponents": {}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<view class="container">
|
||||
<!-- Logo和标题 -->
|
||||
<view class="header">
|
||||
<view class="logo">微</view>
|
||||
<text class="title">微信登录授权</text>
|
||||
<text class="subtitle">请授权获取您的头像和昵称信息</text>
|
||||
</view>
|
||||
|
||||
<!-- 用户信息卡片 -->
|
||||
<view class="user-card">
|
||||
<button class="avatar-picker" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
|
||||
<view class="avatar-wrapper {{avatarUrl ? 'has-avatar' : ''}}">
|
||||
<image wx:if="{{avatarUrl}}" class="avatar-image" src="{{avatarUrl}}" mode="aspectFill"></image>
|
||||
<text wx:else class="avatar-icon">👤</text>
|
||||
</view>
|
||||
</button>
|
||||
|
||||
<view class="nickname-wrapper">
|
||||
<view class="nickname-btn-wrapper">
|
||||
<input class="nickname-btn-input"
|
||||
type="nickname"
|
||||
placeholder="点击授权昵称"
|
||||
value="{{nickname}}"
|
||||
bindinput="onNicknameInput"
|
||||
maxlength="20"
|
||||
placeholder-class="nickname-placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 授权按钮 -->
|
||||
<view class="auth-btn {{canSubmit ? '' : 'disabled'}}"
|
||||
bindtap="handleLogin">
|
||||
<text wx:if="{{isLoading}}">授权中...</text>
|
||||
<text wx:elif="{{loginSuccess}}">✓ 已授权</text>
|
||||
<text wx:else>立即授权</text>
|
||||
</view>
|
||||
|
||||
<!-- 授权说明 -->
|
||||
<view class="permissions">
|
||||
<view class="permissions-title">授权说明</view>
|
||||
<view class="permission-item">
|
||||
<text class="dot">•</text>
|
||||
<text>获取您的公开信息(昵称、头像)</text>
|
||||
</view>
|
||||
<view class="permission-item">
|
||||
<text class="dot">•</text>
|
||||
<text>用于完善您的个人资料</text>
|
||||
</view>
|
||||
<view class="permission-item">
|
||||
<text class="dot">•</text>
|
||||
<text>我们承诺保护您的隐私安全</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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: '确定'
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"usingComponents": {},
|
||||
"navigationBarTitleText": "我的"
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
<!-- 我的页面 -->
|
||||
<view class="container">
|
||||
<!-- 用户信息 - 已登录 -->
|
||||
<view class="user-section" wx:if="{{isLoggedIn}}">
|
||||
<view class="user-info">
|
||||
<image class="user-avatar" src="{{userInfo.avatar || defaultAvatar}}" mode="aspectFill"></image>
|
||||
<view class="user-detail">
|
||||
<text class="user-name">{{userInfo.nickname || '微信用户'}}</text>
|
||||
<text class="user-desc">陪伴孩子快乐学习</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 积分显示
|
||||
<view class="points-box" bindtap="goPointsLog">
|
||||
<text class="points-label">我的积分</text>
|
||||
<text class="points-value">{{userInfo.points || 0}}</text>
|
||||
<text class="points-arrow">›</text>
|
||||
</view>-->
|
||||
</view>
|
||||
|
||||
<!-- 用户信息 - 未登录 -->
|
||||
<view class="user-section user-section-unlogin" wx:else>
|
||||
<view class="user-info" bindtap="goLogin">
|
||||
<image class="user-avatar" src="{{defaultAvatar}}" mode="aspectFill"></image>
|
||||
<view class="user-detail">
|
||||
<text class="user-name">点击登录</text>
|
||||
<text class="user-desc">登录后可下载练习表</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<view class="stats-section">
|
||||
<view class="stat-item">
|
||||
<text class="stat-num">{{userInfo.points || 0}}</text>
|
||||
<text class="stat-label">积分</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-num">{{downloadCount}}</text>
|
||||
<text class="stat-label">下载数</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-num">{{inviteCount}}</text>
|
||||
<text class="stat-label">邀请数</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能列表 -->
|
||||
<view class="menu-section">
|
||||
<view class="menu-item" bindtap="goDownloads">
|
||||
<view class="menu-left">
|
||||
<text class="menu-icon-emoji">📥</text>
|
||||
<text class="menu-text">我的下载</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<!--
|
||||
<view class="menu-item" bindtap="goPointsLog">
|
||||
<view class="menu-left">
|
||||
<text class="menu-icon-emoji">💰</text>
|
||||
<text class="menu-text">积分明细</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
-->
|
||||
<button class="menu-btn" open-type="share">
|
||||
<view class="menu-item">
|
||||
<view class="menu-left">
|
||||
<text class="menu-icon-emoji">🎁</text>
|
||||
<text class="menu-text">邀请好友得积分</text>
|
||||
</view>
|
||||
<text class="menu-extra">+10积分/人</text>
|
||||
</view>
|
||||
</button>
|
||||
|
||||
<view class="menu-item" bindtap="clearCache">
|
||||
<view class="menu-left">
|
||||
<text class="menu-icon-emoji">🗑️</text>
|
||||
<text class="menu-text">清除缓存</text>
|
||||
</view>
|
||||
<text class="menu-extra">{{cacheSize}}</text>
|
||||
</view>
|
||||
|
||||
<view class="menu-item" bindtap="about">
|
||||
<view class="menu-left">
|
||||
<text class="menu-icon-emoji">ℹ️</text>
|
||||
<text class="menu-text">关于我们</text>
|
||||
</view>
|
||||
<text class="menu-extra">v1.0.0</text>
|
||||
</view>
|
||||
|
||||
<!-- 退出登录 -->
|
||||
<view class="menu-item" wx:if="{{isLoggedIn}}" bindtap="logout">
|
||||
<view class="menu-left">
|
||||
<text class="menu-icon-emoji">🚪</text>
|
||||
<text class="menu-text">退出登录</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 版权信息 -->
|
||||
</view>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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}`
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"usingComponents": {},
|
||||
"navigationBarTitleText": "搜索"
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
<!-- 搜索页面 -->
|
||||
<view class="container">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input-wrap">
|
||||
<image class="search-icon" src="/images/icons/search.png" mode="aspectFit"></image>
|
||||
<input class="search-input"
|
||||
type="text"
|
||||
placeholder="搜索练习表..."
|
||||
value="{{keyword}}"
|
||||
focus="{{true}}"
|
||||
confirm-type="search"
|
||||
bindinput="onInput"
|
||||
bindconfirm="doSearch" />
|
||||
<image class="clear-icon"
|
||||
wx:if="{{keyword}}"
|
||||
src="/images/icons/clear.png"
|
||||
mode="aspectFit"
|
||||
bindtap="clearKeyword"></image>
|
||||
</view>
|
||||
<text class="cancel-btn" bindtap="goBack">取消</text>
|
||||
</view>
|
||||
|
||||
<!-- 搜索历史 -->
|
||||
<view class="history-section" wx:if="{{!hasSearched && history.length > 0}}">
|
||||
<view class="section-header">
|
||||
<text class="section-title">搜索历史</text>
|
||||
<image class="delete-icon" src="/images/icons/delete.png" mode="aspectFit" bindtap="clearHistory"></image>
|
||||
</view>
|
||||
<view class="history-tags">
|
||||
<view class="history-tag"
|
||||
wx:for="{{history}}"
|
||||
wx:key="index"
|
||||
bindtap="searchHistory"
|
||||
data-keyword="{{item}}">
|
||||
{{item}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 热门搜索 -->
|
||||
<view class="hot-section" wx:if="{{!hasSearched}}">
|
||||
<view class="section-header">
|
||||
<text class="section-title">热门搜索</text>
|
||||
</view>
|
||||
<view class="hot-tags">
|
||||
<view class="hot-tag"
|
||||
wx:for="{{hotKeywords}}"
|
||||
wx:key="index"
|
||||
bindtap="searchHistory"
|
||||
data-keyword="{{item}}">
|
||||
{{item}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索结果 -->
|
||||
<view class="result-section" wx:if="{{hasSearched}}">
|
||||
<view class="result-header" wx:if="{{worksheets.length > 0}}">
|
||||
<text>找到 {{total}} 个相关结果</text>
|
||||
</view>
|
||||
|
||||
<view class="worksheet-list">
|
||||
<view class="worksheet-item"
|
||||
wx:for="{{worksheets}}"
|
||||
wx:key="id"
|
||||
bindtap="goDetail"
|
||||
data-id="{{item.id}}">
|
||||
<image class="item-cover" src="{{dataBaseUrl}}{{item.coverurl}}" mode="aspectFill"></image>
|
||||
<view class="item-info">
|
||||
<text class="item-title text-ellipsis-2">{{item.title}}</text>
|
||||
<text class="item-e-title text-ellipsis">{{item.e_title}}</text>
|
||||
<text class="item-category">{{item.category_name}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="load-status" wx:if="{{worksheets.length > 0}}">
|
||||
<text wx:if="{{loading}}">加载中...</text>
|
||||
<text wx:elif="{{!hasMore}}">已加载全部</text>
|
||||
<text wx:else bindtap="loadMore">加载更多</text>
|
||||
</view>
|
||||
|
||||
<!-- 无结果 -->
|
||||
<view class="empty-result" wx:if="{{!loading && worksheets.length === 0}}">
|
||||
<image class="empty-img" src="/images/icons/empty-search.png" mode="aspectFit"></image>
|
||||
<text class="empty-text">未找到相关练习表</text>
|
||||
<text class="empty-tip">换个关键词试试吧</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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": {}
|
||||
}
|
||||
|
|
@ -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": {}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
|
||||
"rules": [{
|
||||
"action": "allow",
|
||||
"page": "*"
|
||||
}]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||