add coupon page

This commit is contained in:
Felix
2025-11-25 19:46:31 +08:00
parent 63010a509e
commit 2fa040560f
16 changed files with 407 additions and 122 deletions

View File

@@ -8,7 +8,8 @@
"pages/logs/logs", "pages/logs/logs",
"pages/history/history", "pages/history/history",
"pages/terms/terms", "pages/terms/terms",
"pages/privacy/privacy" "pages/privacy/privacy",
"pages/coupon/coupon"
], ],
"window": { "window": {
"navigationBarTextStyle": "black", "navigationBarTextStyle": "black",
@@ -17,15 +18,6 @@
"backgroundColor": "#ffffff", "backgroundColor": "#ffffff",
"backgroundTextStyle": "light" "backgroundTextStyle": "light"
}, },
"permission": {
"scope.camera": {
"desc": "需要使用相机拍照识别图片"
},
"scope.writePhotosAlbum": {
"desc": "需要访问相册选择图片"
}
},
"componentFramework": "glass-easel", "componentFramework": "glass-easel",
"sitemapLocation": "sitemap.json", "sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents", "lazyCodeLoading": "requiredComponents",

View File

@@ -12,6 +12,7 @@ App<IAppOption>({
onLaunch() { onLaunch() {
console.log('小程序启动') console.log('小程序启动')
wx.cloud.init()
// 初始化登录状态 // 初始化登录状态
this.initLoginStatus() this.initLoginStatus()

View File

@@ -0,0 +1,8 @@
{
"navigationBarTextStyle": "black",
"navigationBarBackgroundColor": "#ffffff",
"backgroundColor": "#f8f9fa",
"backgroundTextStyle": "light",
"usingComponents": {},
"navigationBarTitleText": "积分"
}

View File

@@ -0,0 +1,7 @@
// coupon.ts
Page({
data: {},
onLoad() {
console.log('Coupon page loaded');
},
});

View File

@@ -0,0 +1,24 @@
<!--coupon.wxml-->
<view class="coupon-container">
<view class='coupon_box'>
<view class='content'>
<view class='title'>积分</view>
<view class='how_much'>100</view>
</view>
<view class='btn'> ¥1.00 </view>
</view>
<view class='coupon_box'>
<view class='content'>
<view class='title'>积分</view>
<view class='how_much'>100</view>
</view>
<view class='btn'> ¥1.00 </view>
</view>
<view class='coupon_box'>
<view class='content'>
<view class='title'>积分</view>
<view class='how_much'>100</view>
</view>
<view class='btn'> ¥1.00 </view>
</view>
</view>

View File

@@ -0,0 +1,72 @@
/* coupon.less */
.coupon-container {
display: flex;
justify-content: flex-start;
align-items: flex-start;
align-content: start;
flex-wrap: wrap;
height: 100%;
padding: 0;
background-color: #f5f5f5;
}
.coupon_box{
background: linear-gradient(to right, #FF4B2B, #FF416C);
width: 40%;
border-radius: 12rpx;
text-align: center;
color: #fff;
font-family: 'Tahoma', sans-serif;
position: relative;
margin: 5% 5% 0 5%;
}
.coupon_box::before{
content: '';
position: absolute;
top: 200rpx;
background: #f5f5f5;
width: 20rpx;
height: 30rpx;
z-index: 1;
left: -1rpx;
border-radius: 0 30rpx 30rpx 0
}
.coupon_box::after{
content: '';
position: absolute;
top: 200rpx;
background: #f5f5f5;
width: 20rpx;
height: 30rpx;
z-index: 1;
right: -1rpx;
border-radius: 30rpx 0 0 30rpx
}
.title{
color: rgba(255,255,255,0.75);
font-weight: 600;
font-size: 32rpx
}
.how_much{
font-size: 80rpx;
text-shadow: 0 0 20rpx rgba(0,0,0,0.3); text-align: center
}
.content{
padding: 4rpx 0 32rpx 0;
border-bottom: 4rpx dashed rgba(0,0,0,0.15);
position: relative;
}
.content::before{
content: '100';
position: absolute;
color: rgba(255,255,255,0.15);
top: 0%;
left: 16rpx;
font-size: 144rpx;
font-weight: bold;
}

View File

@@ -67,9 +67,11 @@
maxcharacter="{{6}}" maxcharacter="{{6}}"
/> />
</t-dialog> </t-dialog>
<t-cell title="积分" hover note="{{points.balance}}"> <navigator url="/pages/coupon/coupon" class="cell-navigator">
<t-icon slot="left-icon" name="star" size="44rpx"></t-icon> <t-cell title="积分" hover note="{{points.balance}}">
</t-cell> <t-icon slot="left-icon" name="star" size="44rpx"></t-icon>
</t-cell>
</navigator>
<t-cell title="兑换码" hover arrow bindtap="showCouponDialog"> <t-cell title="兑换码" hover arrow bindtap="showCouponDialog">
<t-icon slot="left-icon" name="coupon" size="44rpx"></t-icon> <t-icon slot="left-icon" name="coupon" size="44rpx"></t-icon>
</t-cell> </t-cell>

View File

@@ -346,3 +346,17 @@
border-radius: 8rpx; border-radius: 8rpx;
margin: 20rpx; margin: 20rpx;
} }
/* Navigation cell styles */
.cell-navigator {
display: block;
text-decoration: none;
}
.cell-navigator .t-cell {
background: #ffffff;
}
.cell-navigator::after {
border: none;
}

View File

@@ -4,13 +4,23 @@ import {
ILoginResponse, ILoginResponse,
IApiResponse, IApiResponse,
IRecognitionResponse, IRecognitionResponse,
ExtendedWordDetail,
IAuditHistoryResponse, IAuditHistoryResponse,
IUserInfo, IUserInfo,
IDailySummaryResponse, IDailySummaryResponse,
YdWordDetail YdWordDetail
} from '../types/app'; } from '../types/app';
import { BASE_URL } from './config'; import { BASE_URL } from './config';
import { cloudConfig } from './cloud.config';
// 添加对 ICloud 命名空间的引用
declare namespace ICloud {
interface CallContainerResult {
data: any;
header: Record<string, string>;
statusCode: number;
errMsg: string;
}
}
// 音频缓存映射 // 音频缓存映射
const audioCache: Map<string, string> = new Map() const audioCache: Map<string, string> = new Map()
@@ -234,128 +244,191 @@ class ApiManager {
retryCount: number = 0 retryCount: number = 0
): Promise<IApiResponse<T>> { ): Promise<IApiResponse<T>> {
const maxRetries = 1 // 最多重试1次首次请求 + 1次重试 const maxRetries = 1 // 最多重试1次首次请求 + 1次重试
return new Promise(async (resolve, reject) => {
if (showLoading && retryCount === 0) {
// wx.showLoading({ title: '加载中...' })
}
console.log('发起请求:', { if (BASE_URL.includes('https://prod')){
url: `${BASE_URL}${url}`, return this.wx_request<T>(url, method, data, showLoading)
method, }
hasData: !!data, else {
hasHeaders: !!this.getHeaders(), return new Promise(async (resolve, reject) => {
retryCount if (showLoading && retryCount === 0) {
}) // wx.showLoading({ title: '加载中...' })
}
wx.request({ console.log('发起请求:', {
url: `${BASE_URL}${url}`, url: `${BASE_URL}${url}`,
method, method,
data, hasData: !!data,
header: this.getHeaders(), hasHeaders: !!this.getHeaders(),
success: async (res) => { retryCount
if (showLoading) { })
wx.hideLoading()
}
console.log('请求响应:', { wx.request({
statusCode: res.statusCode, url: `${BASE_URL}${url}`,
hasData: !!res.data, method,
retryCount, data,
success: true header: this.getHeaders(),
}) success: async (res) => {
if (showLoading) {
if (res.statusCode === 200) { wx.hideLoading()
const response = res.data as IApiResponse<T>
if (response.code === 0 || response.code === 200) {
resolve(response)
} else {
// 业务错误
const errorMsg = response.message || response.msg || '请求失败'
console.error('业务错误:', errorMsg, response)
wx.showToast({
title: errorMsg,
icon: 'none'
})
reject(new Error(errorMsg))
} }
} else if (res.statusCode === 401) {
console.warn('401认证失败尝试自动登录', { retryCount, maxRetries }) console.log('请求响应:', {
statusCode: res.statusCode,
// 如果还有重试机会,尝试自动登录并重试请求 hasData: !!res.data,
if (retryCount < maxRetries) { retryCount,
try { success: true
console.log('开始401自动登录重试') })
// 清理过期的认证数据 if (res.statusCode === 200) {
this.clearAuthData() const response = res.data as IApiResponse<T>
if (response.code === 0 || response.code === 200) {
// 进行智能登录 resolve(response)
const loginResult = await this.smartLogin(true) // 强制刷新token } else {
// 业务错误
if (loginResult) { const errorMsg = response.message || response.msg || '请求失败'
console.log('401自动登录成功重试请求') console.error('业务错误:', errorMsg, response)
// 登录成功后重试原请求 wx.showToast({
try { title: errorMsg,
const retryResult = await this.request<T>(url, method, data, false, retryCount + 1) icon: 'none'
resolve(retryResult) })
} catch (retryError) { reject(new Error(errorMsg))
reject(retryError) }
} else if (res.statusCode === 401) {
console.warn('401认证失败尝试自动登录', { retryCount, maxRetries })
// 如果还有重试机会,尝试自动登录并重试请求
if (retryCount < maxRetries) {
try {
console.log('开始401自动登录重试')
// 清理过期的认证数据
this.clearAuthData()
// 进行智能登录
const loginResult = await this.smartLogin(true) // 强制刷新token
if (loginResult) {
console.log('401自动登录成功重试请求')
// 登录成功后重试原请求
try {
const retryResult = await this.request<T>(url, method, data, false, retryCount + 1)
resolve(retryResult)
} catch (retryError) {
reject(retryError)
}
} else {
console.error('401自动登录失败')
this.handleTokenExpired()
reject(new Error('登录失败,请重新登录'))
} }
} else { } catch (loginError) {
console.error('401自动登录失败') console.error('401自动登录发生错误:', loginError)
this.handleTokenExpired() this.handleTokenExpired()
reject(new Error('登录失败,请重新登录')) reject(new Error('登录失败,请重新登录'))
} }
} catch (loginError) { } else {
console.error('401自动登录发生错误:', loginError) // 重试次数耗尽直接处理token过期
console.error('401重试次数耗尽跳转登录页')
this.handleTokenExpired() this.handleTokenExpired()
reject(new Error('登录失败,请重新登录')) reject(new Error('登录已过期'))
} }
} else if (res.statusCode === 400) {
const response = res.data as IApiResponse<T>
const errorMsg = response.msg || '请求失败'
console.error('400 错误:', errorMsg, response)
reject(new Error(errorMsg))
} else if (res.statusCode === 403) {
const response = res.data as IApiResponse<T>
const errorMsg = response.msg || '请求失败'
console.error('403 错误:', errorMsg, response)
reject(new Error(errorMsg))
} else if (res.statusCode === 404 ) {
const response = res.data as IApiResponse<T>
const errorMsg = response.msg || '请求失败'
console.error('404 错误:', errorMsg, response)
reject(new Error(errorMsg))
} else { } else {
// 重试次数耗尽直接处理token过期 console.error('HTTP错误:', res.statusCode, res.data)
console.error('401重试次数耗尽跳转登录页') wx.showToast({
this.handleTokenExpired() title: `网络请求失败 (${res.statusCode})`,
reject(new Error('登录已过期')) icon: 'none'
})
reject(new Error(`HTTP ${res.statusCode}`))
} }
} else if (res.statusCode === 400) { },
const response = res.data as IApiResponse<T> fail: (error) => {
const errorMsg = response.msg || '请求失败' if (showLoading) {
console.error('400 错误:', errorMsg, response) wx.hideLoading()
reject(new Error(errorMsg)) }
} else if (res.statusCode === 403) {
const response = res.data as IApiResponse<T> console.error('请求失败:', error)
const errorMsg = response.msg || '请求失败'
console.error('403 错误:', errorMsg, response)
reject(new Error(errorMsg))
} else if (res.statusCode === 404 ) {
const response = res.data as IApiResponse<T>
const errorMsg = response.msg || '请求失败'
console.error('404 错误:', errorMsg, response)
reject(new Error(errorMsg))
} else {
console.error('HTTP错误:', res.statusCode, res.data)
wx.showToast({ wx.showToast({
title: `网络请求失败 (${res.statusCode})`, title: '网络连接失败',
icon: 'none' icon: 'none'
}) })
reject(new Error(`HTTP ${res.statusCode}`)) reject(new Error('网络连接失败: ' + JSON.stringify(error)))
} }
})
})
}
}
// 微信云托管请求方法
private async wx_request<T>(
path: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
data?: any,
showLoading: boolean = true
): Promise<ICloud.CallContainerResult> {
return new Promise((resolve, reject) => {
if (showLoading) {
wx.showLoading({ title: '加载中...' });
}
console.log('发起微信云托管请求:', {
path,
method,
hasData: !!data
});
wx.cloud.callContainer({
config: {
env: cloudConfig.env // 使用配置文件中的环境ID
}, },
fail: (error) => { path: path.startsWith('/') ? path : `/${path}`, // 确保路径以 / 开头
method,
header: {
'X-WX-SERVICE': cloudConfig.service // 使用配置文件中的服务名称
},
data: data || {}, // 确保数据对象不为 undefined
success: (res: ICloud.CallContainerResult) => {
if (showLoading) { if (showLoading) {
wx.hideLoading() wx.hideLoading();
} }
console.error('请求失败:', error) console.log('微信云托管请求响应:', {
statusCode: res.statusCode,
hasData: !!res.data,
success: true
});
// 直接返回响应数据,由调用方处理业务逻辑
resolve(res.data);
},
fail: (error: any) => {
if (showLoading) {
wx.hideLoading();
}
console.error('微信云托管请求失败:', error);
wx.showToast({ wx.showToast({
title: '网络连接失败', title: '网络连接失败',
icon: 'none' icon: 'none'
}) });
reject(new Error('网络连接失败: ' + JSON.stringify(error))) reject(new Error('网络连接失败: ' + JSON.stringify(error)));
} }
}) });
}) });
} }
// 处理 token 过期 // 处理 token 过期
@@ -1658,3 +1731,13 @@ class ApiManager {
const apiManager = new ApiManager() const apiManager = new ApiManager()
export default apiManager export default apiManager
export { ApiManager } export { ApiManager }

View File

@@ -0,0 +1,45 @@
// 微信云托管配置文件
// 区分不同环境的配置
// 环境配置接口
export interface CloudConfig {
env: string;
service: string;
}
// 根据当前环境获取配置
// 在微信小程序中,可以通过 wx.getAccountInfoSync() 获取环境信息
const accountInfo = wx.getAccountInfoSync();
const envVersion = accountInfo.miniProgram.envVersion || 'release'; // develop, trial, release
// 环境配置映射
const envConfigMap: Record<string, CloudConfig> = {
// 开发环境
develop: {
env: 'prod-1g647ekk563f2652', // 开发环境云托管环境ID
service: 'prod' // 开发环境服务名称
},
// 体验版环境
trial: {
env: 'prod-1g647ekk563f2652', // 体验版云托管环境ID
service: 'prod' // 体验版服务名称
},
// 正式版环境
release: {
env: 'prod-1g647ekk563f2652', // 正式版云托管环境ID
service: 'prod' // 正式版服务名称
}
};
// 导出当前环境的配置
export const cloudConfig: CloudConfig = envConfigMap[envVersion] || envConfigMap['release'];
// 如果需要自定义配置,可以覆盖默认配置
export function setCloudConfig(config: Partial<CloudConfig>): void {
if (config.env !== undefined) {
cloudConfig.env = config.env;
}
if (config.service !== undefined) {
cloudConfig.service = config.service;
}
}

View File

@@ -2,6 +2,7 @@
// API 基础域名 // API 基础域名
export const BASE_URL = 'https://app.xhzone.cn' export const BASE_URL = 'https://app.xhzone.cn'
// export const BASE_URL = 'https://prod-201510-4-1385696640.sh.run.tcloudbase.com'
// 文件服务基础路径 // 文件服务基础路径
export const FILE_BASE_URL = `${BASE_URL}/api/v1/file` export const FILE_BASE_URL = `${BASE_URL}/api/v1/file`

12
package-lock.json generated
View File

@@ -1,14 +1,14 @@
{ {
"name": "miniprogram-ts-less-quickstart", "name": "miniprogram-ts-less-quickstart",
"version": "1.0.0", "version": "25.9.10",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "miniprogram-ts-less-quickstart", "name": "miniprogram-ts-less-quickstart",
"version": "1.0.0", "version": "25.9.10",
"dependencies": { "dependencies": {
"tdesign-miniprogram": "^1.10.1" "tdesign-miniprogram": "^1.11.0"
}, },
"devDependencies": { "devDependencies": {
"miniprogram-api-typings": "^2.8.3-1" "miniprogram-api-typings": "^2.8.3-1"
@@ -22,9 +22,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/tdesign-miniprogram": { "node_modules/tdesign-miniprogram": {
"version": "1.10.1", "version": "1.11.0",
"resolved": "https://registry.npmjs.org/tdesign-miniprogram/-/tdesign-miniprogram-1.10.1.tgz", "resolved": "https://registry.npmjs.org/tdesign-miniprogram/-/tdesign-miniprogram-1.11.0.tgz",
"integrity": "sha512-rKM3JYfJGB+R9G/yp6kGVN0Kc0hKqKoQnpoWm7OVrXNsYM0dlJXwTf+WsLkQtgUyfM20taqNaxXD852Bx7IseQ==", "integrity": "sha512-1Siv2HVrSVlFiQBQXEjBrCvvsC3NQUsGgiej2RDXOxXHAPH2nynLhJhK+wu+KptzXi5GV+XYr+PHV2Je8vgW3w==",
"license": "MIT" "license": "MIT"
} }
} }

View File

@@ -10,6 +10,6 @@
"miniprogram-api-typings": "^2.8.3-1" "miniprogram-api-typings": "^2.8.3-1"
}, },
"dependencies": { "dependencies": {
"tdesign-miniprogram": "^1.10.1" "tdesign-miniprogram": "^1.11.0"
} }
} }

View File

@@ -51,5 +51,5 @@
"ignore": [], "ignore": [],
"include": [] "include": []
}, },
"appid": "wxe739c0e6fb02eda8" "appid": "wxd6917f77eb2723fb"
} }

View File

@@ -18,10 +18,17 @@
"checkInvalidKey": true, "checkInvalidKey": true,
"ignoreDevUnusedFiles": true "ignoreDevUnusedFiles": true
}, },
"libVersion": "3.9.3", "libVersion": "2.33.0",
"condition": { "condition": {
"miniprogram": { "miniprogram": {
"list": [ "list": [
{
"name": "pages/coupon/coupon",
"pathName": "pages/coupon/coupon",
"query": "",
"scene": null,
"launchMode": "default"
},
{ {
"name": "", "name": "",
"pathName": "pages/result/result", "pathName": "pages/result/result",

View File

@@ -121,6 +121,12 @@ interface WxCloud {
CloudID: ICloud.ICloudIDConstructor CloudID: ICloud.ICloudIDConstructor
CDN: ICloud.ICDNConstructor CDN: ICloud.ICDNConstructor
// Add callContainer method for WeChat Cloud Hosting
callContainer(param: ICloud.CallContainerParam): void
callContainer(
param: RQ<ICloud.CallContainerParam>
): Promise<ICloud.CallContainerResult>
} }
declare namespace ICloud { declare namespace ICloud {
@@ -228,6 +234,29 @@ declare namespace ICloud {
(options: string | ArrayBuffer | ICDNFilePathSpec): CDN (options: string | ArrayBuffer | ICDNFilePathSpec): CDN
} }
// === end === // === end ===
// === API: callContainer ===
interface CallContainerResult extends IAPISuccessParam {
data: any
header: Record<string, string>
statusCode: number
errMsg: string
}
interface CallContainerParam extends ICloudAPIParam<CallContainerResult> {
config?: {
env?: string
}
path: string
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'TRACE' | 'CONNECT'
header?: Record<string, string>
data?: any
dataType?: string
responseType?: string
timeout?: number
}
// === end ===
} }
// === Database === // === Database ===