feat :init
This commit is contained in:
246
miniprogram/app.js
Normal file
246
miniprogram/app.js
Normal file
@@ -0,0 +1,246 @@
|
||||
// app.js
|
||||
App({
|
||||
globalData: {
|
||||
userInfo: null,
|
||||
token: '',
|
||||
memberInfo: null,
|
||||
systemInfo: null,
|
||||
apiBaseUrl: 'https://api.xiaozhi.com', // 生产环境API地址
|
||||
// apiBaseUrl: 'http://localhost:8091', // 开发环境API地址
|
||||
version: '1.0.0'
|
||||
},
|
||||
|
||||
onLaunch() {
|
||||
console.log('小智AI会员系统启动')
|
||||
|
||||
// 初始化系统信息
|
||||
this.initSystemInfo()
|
||||
|
||||
// 检查登录状态
|
||||
this.checkLoginStatus()
|
||||
|
||||
// 检查更新
|
||||
this.checkForUpdate()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
console.log('小程序显示')
|
||||
},
|
||||
|
||||
onHide() {
|
||||
console.log('小程序隐藏')
|
||||
},
|
||||
|
||||
onError(msg) {
|
||||
console.error('小程序错误:', msg)
|
||||
// 错误上报
|
||||
this.reportError(msg)
|
||||
},
|
||||
|
||||
// 初始化系统信息
|
||||
initSystemInfo() {
|
||||
try {
|
||||
const systemInfo = wx.getSystemInfoSync()
|
||||
this.globalData.systemInfo = systemInfo
|
||||
console.log('系统信息:', systemInfo)
|
||||
} catch (error) {
|
||||
console.error('获取系统信息失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
checkLoginStatus() {
|
||||
const token = wx.getStorageSync('token')
|
||||
const userInfo = wx.getStorageSync('userInfo')
|
||||
|
||||
if (token && userInfo) {
|
||||
this.globalData.token = token
|
||||
this.globalData.userInfo = userInfo
|
||||
console.log('用户已登录:', userInfo.nickName)
|
||||
|
||||
// 验证token有效性
|
||||
this.validateToken()
|
||||
} else {
|
||||
console.log('用户未登录')
|
||||
}
|
||||
},
|
||||
|
||||
// 验证token有效性
|
||||
async validateToken() {
|
||||
try {
|
||||
const api = require('./utils/api.js')
|
||||
const res = await api.getMemberInfo()
|
||||
|
||||
if (res.code === 200) {
|
||||
this.globalData.memberInfo = res.data
|
||||
console.log('Token有效,会员信息:', res.data)
|
||||
} else {
|
||||
console.log('Token无效,清除登录状态')
|
||||
this.clearLoginStatus()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('验证Token失败:', error)
|
||||
this.clearLoginStatus()
|
||||
}
|
||||
},
|
||||
|
||||
// 清除登录状态
|
||||
clearLoginStatus() {
|
||||
this.globalData.token = ''
|
||||
this.globalData.userInfo = null
|
||||
this.globalData.memberInfo = null
|
||||
|
||||
wx.removeStorageSync('token')
|
||||
wx.removeStorageSync('userInfo')
|
||||
wx.removeStorageSync('memberInfo')
|
||||
},
|
||||
|
||||
// 检查小程序更新
|
||||
checkForUpdate() {
|
||||
if (wx.canIUse('getUpdateManager')) {
|
||||
const updateManager = wx.getUpdateManager()
|
||||
|
||||
updateManager.onCheckForUpdate((res) => {
|
||||
console.log('检查更新结果:', res.hasUpdate)
|
||||
})
|
||||
|
||||
updateManager.onUpdateReady(() => {
|
||||
wx.showModal({
|
||||
title: '更新提示',
|
||||
content: '新版本已经准备好,是否重启应用?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
updateManager.applyUpdate()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
updateManager.onUpdateFailed(() => {
|
||||
wx.showToast({
|
||||
title: '更新失败',
|
||||
icon: 'error'
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 错误上报
|
||||
reportError(error) {
|
||||
// TODO: 实现错误上报逻辑
|
||||
console.log('上报错误:', error)
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
getUserInfo() {
|
||||
return this.globalData.userInfo
|
||||
},
|
||||
|
||||
// 设置用户信息
|
||||
setUserInfo(userInfo) {
|
||||
this.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
},
|
||||
|
||||
// 获取会员信息
|
||||
getMemberInfo() {
|
||||
return this.globalData.memberInfo
|
||||
},
|
||||
|
||||
// 设置会员信息
|
||||
setMemberInfo(memberInfo) {
|
||||
this.globalData.memberInfo = memberInfo
|
||||
wx.setStorageSync('memberInfo', memberInfo)
|
||||
},
|
||||
|
||||
// 获取Token
|
||||
getToken() {
|
||||
return this.globalData.token
|
||||
},
|
||||
|
||||
// 设置Token
|
||||
setToken(token) {
|
||||
this.globalData.token = token
|
||||
wx.setStorageSync('token', token)
|
||||
},
|
||||
|
||||
// 检查会员权限
|
||||
checkMemberPermission(benefitCode) {
|
||||
const memberInfo = this.globalData.memberInfo
|
||||
if (!memberInfo) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查会员状态
|
||||
if (memberInfo.memberStatus !== 'ACTIVE') {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if (memberInfo.endTime) {
|
||||
const endTime = new Date(memberInfo.endTime)
|
||||
const now = new Date()
|
||||
if (endTime < now) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查权益
|
||||
const benefits = memberInfo.benefits || []
|
||||
return benefits.some(benefit =>
|
||||
benefit.benefitCode === benefitCode && benefit.isEnabled
|
||||
)
|
||||
},
|
||||
|
||||
// 格式化会员等级文本
|
||||
formatMemberLevel(level) {
|
||||
const levelMap = {
|
||||
'FREE': '免费用户',
|
||||
'VIP': 'VIP会员',
|
||||
'SVIP': 'SVIP会员'
|
||||
}
|
||||
return levelMap[level] || '未知等级'
|
||||
},
|
||||
|
||||
// 格式化会员状态文本
|
||||
formatMemberStatus(status) {
|
||||
const statusMap = {
|
||||
'ACTIVE': '有效',
|
||||
'EXPIRED': '已过期',
|
||||
'SUSPENDED': '已暂停',
|
||||
'CANCELLED': '已取消'
|
||||
}
|
||||
return statusMap[status] || '未知状态'
|
||||
},
|
||||
|
||||
// 计算会员剩余天数
|
||||
calculateRemainingDays(endTime) {
|
||||
if (!endTime) {
|
||||
return null // 永久有效
|
||||
}
|
||||
|
||||
const end = new Date(endTime)
|
||||
const now = new Date()
|
||||
const diffTime = end - now
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
return diffDays > 0 ? diffDays : 0
|
||||
},
|
||||
|
||||
// 显示会员升级提示
|
||||
showUpgradePrompt(benefitName) {
|
||||
wx.showModal({
|
||||
title: '权限不足',
|
||||
content: `${benefitName}需要VIP会员权限,是否立即升级?`,
|
||||
confirmText: '立即升级',
|
||||
cancelText: '稍后再说',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.navigateTo({
|
||||
url: '/pages/member/upgrade/upgrade'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
92
miniprogram/app.json
Normal file
92
miniprogram/app.json
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/index/index",
|
||||
"pages/member/bind/bind",
|
||||
"pages/member/center/center",
|
||||
"pages/member/benefits/benefits",
|
||||
"pages/member/upgrade/upgrade",
|
||||
"pages/member/history/history",
|
||||
"pages/login/login",
|
||||
"pages/profile/profile"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
"navigationBarBackgroundColor": "#2C3E50",
|
||||
"navigationBarTitleText": "小智AI会员",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#F8F9FA",
|
||||
"enablePullDownRefresh": true,
|
||||
"onReachBottomDistance": 50
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#7A7E83",
|
||||
"selectedColor": "#3CC51F",
|
||||
"borderStyle": "black",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"iconPath": "images/tab/home.png",
|
||||
"selectedIconPath": "images/tab/home-active.png",
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/member/center/center",
|
||||
"iconPath": "images/tab/member.png",
|
||||
"selectedIconPath": "images/tab/member-active.png",
|
||||
"text": "会员中心"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/profile/profile",
|
||||
"iconPath": "images/tab/profile.png",
|
||||
"selectedIconPath": "images/tab/profile-active.png",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
},
|
||||
"networkTimeout": {
|
||||
"request": 10000,
|
||||
"downloadFile": 10000
|
||||
},
|
||||
"debug": false,
|
||||
"navigateToMiniProgramAppIdList": [],
|
||||
"permission": {
|
||||
"scope.userInfo": {
|
||||
"desc": "用于获取用户基本信息"
|
||||
}
|
||||
},
|
||||
"requiredBackgroundModes": [],
|
||||
"plugins": {},
|
||||
"preloadRule": {
|
||||
"pages/member/center/center": {
|
||||
"network": "all",
|
||||
"packages": ["member"]
|
||||
}
|
||||
},
|
||||
"subpackages": [
|
||||
{
|
||||
"root": "member",
|
||||
"name": "member",
|
||||
"pages": [
|
||||
"pages/bind/bind",
|
||||
"pages/center/center",
|
||||
"pages/benefits/benefits",
|
||||
"pages/upgrade/upgrade",
|
||||
"pages/history/history"
|
||||
]
|
||||
}
|
||||
],
|
||||
"lazyCodeLoading": "requiredComponents",
|
||||
"sitemapLocation": "sitemap.json",
|
||||
"style": "v2",
|
||||
"componentFramework": "glass-easel",
|
||||
"renderer": "webview",
|
||||
"rendererOptions": {
|
||||
"skyline": {
|
||||
"defaultDisplayBlock": true,
|
||||
"disableABTest": true
|
||||
}
|
||||
},
|
||||
"darkmode": false,
|
||||
"themeLocation": "theme.json"
|
||||
}
|
||||
482
miniprogram/app.wxss
Normal file
482
miniprogram/app.wxss
Normal file
@@ -0,0 +1,482 @@
|
||||
/**app.wxss**/
|
||||
/* 全局样式 */
|
||||
|
||||
/* 重置样式 */
|
||||
page {
|
||||
background-color: #F8F9FA;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* 容器样式 */
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.container-padding {
|
||||
padding: 40rpx 30rpx;
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #F0F0F0;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 20rpx 30rpx;
|
||||
background: #FAFAFA;
|
||||
border-top: 1rpx solid #F0F0F0;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24rpx 40rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #F8F9FA;
|
||||
color: #6C757D;
|
||||
border: 1rpx solid #DEE2E6;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 2rpx solid #667eea;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.btn-disabled {
|
||||
background: #E9ECEF !important;
|
||||
color: #ADB5BD !important;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.btn-large {
|
||||
padding: 32rpx 60rpx;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 16rpx 24rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
width: 100%;
|
||||
margin: 20rpx 0;
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
.form-group {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 24rpx 20rpx;
|
||||
border: 2rpx solid #E9ECEF;
|
||||
border-radius: 12rpx;
|
||||
font-size: 30rpx;
|
||||
background: #FFFFFF;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: #667eea;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-input-error {
|
||||
border-color: #DC3545;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
color: #DC3545;
|
||||
font-size: 24rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
/* 文本样式 */
|
||||
.text-primary { color: #667eea; }
|
||||
.text-success { color: #28a745; }
|
||||
.text-warning { color: #ffc107; }
|
||||
.text-danger { color: #dc3545; }
|
||||
.text-info { color: #17a2b8; }
|
||||
.text-muted { color: #6c757d; }
|
||||
.text-white { color: #ffffff; }
|
||||
|
||||
.text-xs { font-size: 20rpx; }
|
||||
.text-sm { font-size: 24rpx; }
|
||||
.text-base { font-size: 28rpx; }
|
||||
.text-lg { font-size: 32rpx; }
|
||||
.text-xl { font-size: 36rpx; }
|
||||
.text-2xl { font-size: 40rpx; }
|
||||
.text-3xl { font-size: 48rpx; }
|
||||
|
||||
.text-center { text-align: center; }
|
||||
.text-left { text-align: left; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
.font-bold { font-weight: bold; }
|
||||
.font-medium { font-weight: 500; }
|
||||
.font-normal { font-weight: normal; }
|
||||
|
||||
/* 布局样式 */
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.flex-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.flex-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.align-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.align-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 间距样式 */
|
||||
.m-0 { margin: 0; }
|
||||
.m-1 { margin: 10rpx; }
|
||||
.m-2 { margin: 20rpx; }
|
||||
.m-3 { margin: 30rpx; }
|
||||
.m-4 { margin: 40rpx; }
|
||||
|
||||
.mt-0 { margin-top: 0; }
|
||||
.mt-1 { margin-top: 10rpx; }
|
||||
.mt-2 { margin-top: 20rpx; }
|
||||
.mt-3 { margin-top: 30rpx; }
|
||||
.mt-4 { margin-top: 40rpx; }
|
||||
|
||||
.mb-0 { margin-bottom: 0; }
|
||||
.mb-1 { margin-bottom: 10rpx; }
|
||||
.mb-2 { margin-bottom: 20rpx; }
|
||||
.mb-3 { margin-bottom: 30rpx; }
|
||||
.mb-4 { margin-bottom: 40rpx; }
|
||||
|
||||
.ml-0 { margin-left: 0; }
|
||||
.ml-1 { margin-left: 10rpx; }
|
||||
.ml-2 { margin-left: 20rpx; }
|
||||
.ml-3 { margin-left: 30rpx; }
|
||||
.ml-4 { margin-left: 40rpx; }
|
||||
|
||||
.mr-0 { margin-right: 0; }
|
||||
.mr-1 { margin-right: 10rpx; }
|
||||
.mr-2 { margin-right: 20rpx; }
|
||||
.mr-3 { margin-right: 30rpx; }
|
||||
.mr-4 { margin-right: 40rpx; }
|
||||
|
||||
.p-0 { padding: 0; }
|
||||
.p-1 { padding: 10rpx; }
|
||||
.p-2 { padding: 20rpx; }
|
||||
.p-3 { padding: 30rpx; }
|
||||
.p-4 { padding: 40rpx; }
|
||||
|
||||
.pt-0 { padding-top: 0; }
|
||||
.pt-1 { padding-top: 10rpx; }
|
||||
.pt-2 { padding-top: 20rpx; }
|
||||
.pt-3 { padding-top: 30rpx; }
|
||||
.pt-4 { padding-top: 40rpx; }
|
||||
|
||||
.pb-0 { padding-bottom: 0; }
|
||||
.pb-1 { padding-bottom: 10rpx; }
|
||||
.pb-2 { padding-bottom: 20rpx; }
|
||||
.pb-3 { padding-bottom: 30rpx; }
|
||||
.pb-4 { padding-bottom: 40rpx; }
|
||||
|
||||
.pl-0 { padding-left: 0; }
|
||||
.pl-1 { padding-left: 10rpx; }
|
||||
.pl-2 { padding-left: 20rpx; }
|
||||
.pl-3 { padding-left: 30rpx; }
|
||||
.pl-4 { padding-left: 40rpx; }
|
||||
|
||||
.pr-0 { padding-right: 0; }
|
||||
.pr-1 { padding-right: 10rpx; }
|
||||
.pr-2 { padding-right: 20rpx; }
|
||||
.pr-3 { padding-right: 30rpx; }
|
||||
.pr-4 { padding-right: 40rpx; }
|
||||
|
||||
/* 会员等级样式 */
|
||||
.member-free {
|
||||
color: #6C757D;
|
||||
}
|
||||
|
||||
.member-vip {
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
.member-svip {
|
||||
color: #FF6B35;
|
||||
}
|
||||
|
||||
.member-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.member-badge.free {
|
||||
background: #F8F9FA;
|
||||
color: #6C757D;
|
||||
}
|
||||
|
||||
.member-badge.vip {
|
||||
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.member-badge.svip {
|
||||
background: linear-gradient(135deg, #FF6B35, #F7931E);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* 状态样式 */
|
||||
.status-active {
|
||||
color: #28A745;
|
||||
}
|
||||
|
||||
.status-expired {
|
||||
color: #DC3545;
|
||||
}
|
||||
|
||||
.status-suspended {
|
||||
color: #FFC107;
|
||||
}
|
||||
|
||||
/* 加载样式 */
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
color: #6C757D;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 4rpx solid #F3F3F3;
|
||||
border-top: 4rpx solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx 40rpx;
|
||||
color: #6C757D;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-bottom: 20rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 24rpx;
|
||||
color: #ADB5BD;
|
||||
}
|
||||
|
||||
/* 分割线 */
|
||||
.divider {
|
||||
height: 1rpx;
|
||||
background: #E9ECEF;
|
||||
margin: 20rpx 0;
|
||||
}
|
||||
|
||||
.divider-thick {
|
||||
height: 20rpx;
|
||||
background: #F8F9FA;
|
||||
margin: 30rpx -30rpx;
|
||||
}
|
||||
|
||||
/* 阴影效果 */
|
||||
.shadow-sm {
|
||||
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.shadow-lg {
|
||||
box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* 圆角 */
|
||||
.rounded-sm { border-radius: 4rpx; }
|
||||
.rounded { border-radius: 8rpx; }
|
||||
.rounded-lg { border-radius: 12rpx; }
|
||||
.rounded-xl { border-radius: 16rpx; }
|
||||
.rounded-full { border-radius: 50%; }
|
||||
|
||||
/* 背景色 */
|
||||
.bg-white { background-color: #FFFFFF; }
|
||||
.bg-gray { background-color: #F8F9FA; }
|
||||
.bg-primary { background-color: #667eea; }
|
||||
.bg-success { background-color: #28a745; }
|
||||
.bg-warning { background-color: #ffc107; }
|
||||
.bg-danger { background-color: #dc3545; }
|
||||
|
||||
/* 边框 */
|
||||
.border { border: 1rpx solid #E9ECEF; }
|
||||
.border-top { border-top: 1rpx solid #E9ECEF; }
|
||||
.border-bottom { border-bottom: 1rpx solid #E9ECEF; }
|
||||
.border-left { border-left: 1rpx solid #E9ECEF; }
|
||||
.border-right { border-right: 1rpx solid #E9ECEF; }
|
||||
|
||||
/* 隐藏/显示 */
|
||||
.hidden { display: none !important; }
|
||||
.visible { display: block !important; }
|
||||
|
||||
/* 透明度 */
|
||||
.opacity-0 { opacity: 0; }
|
||||
.opacity-25 { opacity: 0.25; }
|
||||
.opacity-50 { opacity: 0.5; }
|
||||
.opacity-75 { opacity: 0.75; }
|
||||
.opacity-100 { opacity: 1; }
|
||||
|
||||
/* 动画 */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
.slide-up {
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
481
miniprogram/pages/index/index.js
Normal file
481
miniprogram/pages/index/index.js
Normal file
@@ -0,0 +1,481 @@
|
||||
// pages/index/index.js
|
||||
const app = getApp();
|
||||
const { memberAPI } = require('../../utils/api');
|
||||
const {
|
||||
formatRelativeTime,
|
||||
getMemberLevelText,
|
||||
calculateMemberExpire,
|
||||
showError,
|
||||
showSuccess,
|
||||
debounce
|
||||
} = require('../../utils/util');
|
||||
|
||||
Page({
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
// 用户信息
|
||||
userInfo: {},
|
||||
isLoggedIn: false,
|
||||
|
||||
// 会员信息
|
||||
memberInfo: {},
|
||||
|
||||
// 快捷权益
|
||||
quickBenefits: [],
|
||||
|
||||
// 功能菜单
|
||||
menuItems: [
|
||||
{
|
||||
id: 'member-center',
|
||||
name: '会员中心',
|
||||
icon: '/images/menu/member.png',
|
||||
path: '/pages/member/center/center'
|
||||
},
|
||||
{
|
||||
id: 'member-bind',
|
||||
name: '绑定会员',
|
||||
icon: '/images/menu/bind.png',
|
||||
path: '/pages/member/bind/bind'
|
||||
},
|
||||
{
|
||||
id: 'benefits',
|
||||
name: '权益说明',
|
||||
icon: '/images/menu/benefits.png',
|
||||
path: '/pages/member/benefits/benefits'
|
||||
},
|
||||
{
|
||||
id: 'upgrade',
|
||||
name: '升级会员',
|
||||
icon: '/images/menu/upgrade.png',
|
||||
path: '/pages/member/upgrade/upgrade'
|
||||
},
|
||||
{
|
||||
id: 'history',
|
||||
name: '使用记录',
|
||||
icon: '/images/menu/history.png',
|
||||
path: '/pages/member/history/history'
|
||||
},
|
||||
{
|
||||
id: 'invite',
|
||||
name: '邀请好友',
|
||||
icon: '/images/menu/invite.png',
|
||||
path: '/pages/member/invite/invite'
|
||||
},
|
||||
{
|
||||
id: 'support',
|
||||
name: '客服支持',
|
||||
icon: '/images/menu/support.png',
|
||||
path: '/pages/support/support'
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
name: '设置',
|
||||
icon: '/images/menu/settings.png',
|
||||
path: '/pages/settings/settings'
|
||||
}
|
||||
],
|
||||
|
||||
// 最近活动
|
||||
recentActivities: [],
|
||||
|
||||
// 升级提示
|
||||
showUpgradePrompt: false,
|
||||
|
||||
// 权益弹窗
|
||||
showBenefitModal: false,
|
||||
selectedBenefit: {},
|
||||
|
||||
// 加载状态
|
||||
loading: false,
|
||||
refreshing: false
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
this.initPage();
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
this.refreshUserInfo();
|
||||
this.refreshMemberInfo();
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
this.refreshPage();
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
// 可以在这里加载更多数据
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: '小智AI会员 - 享受专属特权',
|
||||
path: '/pages/index/index',
|
||||
imageUrl: '/images/share-cover.png'
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化页面
|
||||
*/
|
||||
initPage() {
|
||||
this.setData({ loading: true });
|
||||
|
||||
// 检查登录状态
|
||||
this.checkLoginStatus();
|
||||
|
||||
// 初始化菜单徽章
|
||||
this.updateMenuBadges();
|
||||
|
||||
this.setData({ loading: false });
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查登录状态
|
||||
*/
|
||||
checkLoginStatus() {
|
||||
const isLoggedIn = app.checkLoginStatus();
|
||||
const userInfo = app.globalData.userInfo || {};
|
||||
|
||||
this.setData({
|
||||
isLoggedIn,
|
||||
userInfo
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新页面数据
|
||||
*/
|
||||
refreshPage: debounce(function() {
|
||||
this.setData({ refreshing: true });
|
||||
|
||||
Promise.all([
|
||||
this.refreshUserInfo(),
|
||||
this.refreshMemberInfo(),
|
||||
this.loadRecentActivities()
|
||||
]).finally(() => {
|
||||
this.setData({ refreshing: false });
|
||||
wx.stopPullDownRefresh();
|
||||
});
|
||||
}, 1000),
|
||||
|
||||
/**
|
||||
* 刷新用户信息
|
||||
*/
|
||||
async refreshUserInfo() {
|
||||
if (!this.data.isLoggedIn) return;
|
||||
|
||||
try {
|
||||
const result = await memberAPI.getUserInfo();
|
||||
if (result.code === 0) {
|
||||
const userInfo = result.data;
|
||||
app.globalData.userInfo = userInfo;
|
||||
this.setData({ userInfo });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新会员信息
|
||||
*/
|
||||
async refreshMemberInfo() {
|
||||
if (!this.data.isLoggedIn) return;
|
||||
|
||||
try {
|
||||
const result = await memberAPI.getMemberInfo();
|
||||
if (result.code === 0) {
|
||||
const memberData = result.data;
|
||||
const memberInfo = this.processMemberInfo(memberData);
|
||||
|
||||
app.globalData.memberInfo = memberInfo;
|
||||
this.setData({ memberInfo });
|
||||
|
||||
// 加载会员权益
|
||||
this.loadMemberBenefits();
|
||||
|
||||
// 检查是否显示升级提示
|
||||
this.checkUpgradePrompt(memberInfo);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取会员信息失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理会员信息
|
||||
*/
|
||||
processMemberInfo(memberData) {
|
||||
const { level, expireTime } = memberData;
|
||||
const levelText = getMemberLevelText(level);
|
||||
const expireInfo = calculateMemberExpire(expireTime);
|
||||
|
||||
return {
|
||||
...memberData,
|
||||
levelText,
|
||||
expireInfo
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载会员权益
|
||||
*/
|
||||
async loadMemberBenefits() {
|
||||
try {
|
||||
const result = await memberAPI.getMemberBenefits();
|
||||
if (result.code === 0) {
|
||||
const benefits = result.data || [];
|
||||
// 只显示前4个权益作为快捷入口
|
||||
const quickBenefits = benefits.slice(0, 4).map(benefit => ({
|
||||
...benefit,
|
||||
icon: benefit.icon || '/images/benefits/default.png'
|
||||
}));
|
||||
|
||||
this.setData({ quickBenefits });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取会员权益失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载最近活动
|
||||
*/
|
||||
async loadRecentActivities() {
|
||||
if (!this.data.isLoggedIn) return;
|
||||
|
||||
try {
|
||||
const result = await memberAPI.getBenefitUsage(1, 5);
|
||||
if (result.code === 0) {
|
||||
const activities = (result.data.list || []).map(activity => ({
|
||||
...activity,
|
||||
timeText: formatRelativeTime(activity.createTime),
|
||||
statusClass: this.getActivityStatusClass(activity.status),
|
||||
statusText: this.getActivityStatusText(activity.status),
|
||||
icon: activity.icon || '/images/activity/default.png'
|
||||
}));
|
||||
|
||||
this.setData({ recentActivities: activities });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取最近活动失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取活动状态样式类
|
||||
*/
|
||||
getActivityStatusClass(status) {
|
||||
const classMap = {
|
||||
'success': 'text-success',
|
||||
'failed': 'text-danger',
|
||||
'pending': 'text-warning'
|
||||
};
|
||||
return classMap[status] || 'text-muted';
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取活动状态文本
|
||||
*/
|
||||
getActivityStatusText(status) {
|
||||
const textMap = {
|
||||
'success': '成功',
|
||||
'failed': '失败',
|
||||
'pending': '处理中'
|
||||
};
|
||||
return textMap[status] || '未知';
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查升级提示
|
||||
*/
|
||||
checkUpgradePrompt(memberInfo) {
|
||||
const { level, expireInfo } = memberInfo;
|
||||
|
||||
// 普通用户或会员即将过期时显示升级提示
|
||||
const showPrompt = level === 'free' ||
|
||||
(expireInfo && !expireInfo.isExpired && expireInfo.daysLeft <= 7);
|
||||
|
||||
this.setData({ showUpgradePrompt: showPrompt });
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新菜单徽章
|
||||
*/
|
||||
updateMenuBadges() {
|
||||
const menuItems = this.data.menuItems.map(item => {
|
||||
// 这里可以根据实际需求添加徽章逻辑
|
||||
if (item.id === 'member-bind' && !this.data.isLoggedIn) {
|
||||
item.badge = 'NEW';
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
this.setData({ menuItems });
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理登录
|
||||
*/
|
||||
handleLogin() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/login/login'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理菜单点击
|
||||
*/
|
||||
handleMenuClick(e) {
|
||||
const { menu } = e.currentTarget.dataset;
|
||||
|
||||
// 检查是否需要登录
|
||||
if (!this.data.isLoggedIn && this.needLogin(menu.id)) {
|
||||
this.handleLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
// 特殊处理
|
||||
switch (menu.id) {
|
||||
case 'support':
|
||||
this.handleCustomerService();
|
||||
break;
|
||||
default:
|
||||
wx.navigateTo({
|
||||
url: menu.path
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查功能是否需要登录
|
||||
*/
|
||||
needLogin(menuId) {
|
||||
const loginRequiredMenus = [
|
||||
'member-center', 'member-bind', 'upgrade',
|
||||
'history', 'invite'
|
||||
];
|
||||
return loginRequiredMenus.includes(menuId);
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理客服
|
||||
*/
|
||||
handleCustomerService() {
|
||||
wx.makePhoneCall({
|
||||
phoneNumber: '400-123-4567',
|
||||
fail: () => {
|
||||
showError('拨打客服电话失败');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理权益点击
|
||||
*/
|
||||
handleBenefitClick(e) {
|
||||
const { benefit } = e.currentTarget.dataset;
|
||||
this.setData({
|
||||
selectedBenefit: benefit,
|
||||
showBenefitModal: true
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭权益弹窗
|
||||
*/
|
||||
closeBenefitModal() {
|
||||
this.setData({
|
||||
showBenefitModal: false,
|
||||
selectedBenefit: {}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 阻止事件冒泡
|
||||
*/
|
||||
stopPropagation() {
|
||||
// 阻止事件冒泡
|
||||
},
|
||||
|
||||
/**
|
||||
* 使用权益
|
||||
*/
|
||||
async useBenefit() {
|
||||
const { selectedBenefit } = this.data;
|
||||
|
||||
try {
|
||||
const result = await memberAPI.verifyBenefit(selectedBenefit.type);
|
||||
if (result.code === 0) {
|
||||
showSuccess('权益使用成功');
|
||||
this.closeBenefitModal();
|
||||
this.loadMemberBenefits(); // 刷新权益信息
|
||||
} else {
|
||||
showError(result.message || '权益使用失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('使用权益失败:', error);
|
||||
showError('权益使用失败');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 导航到会员中心
|
||||
*/
|
||||
navigateToCenter() {
|
||||
if (!this.data.isLoggedIn) {
|
||||
this.handleLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
wx.switchTab({
|
||||
url: '/pages/member/center/center'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 导航到升级页面
|
||||
*/
|
||||
navigateToUpgrade() {
|
||||
if (!this.data.isLoggedIn) {
|
||||
this.handleLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
wx.navigateTo({
|
||||
url: '/pages/member/upgrade/upgrade'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 导航到历史记录
|
||||
*/
|
||||
navigateToHistory() {
|
||||
if (!this.data.isLoggedIn) {
|
||||
this.handleLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
wx.navigateTo({
|
||||
url: '/pages/member/history/history'
|
||||
});
|
||||
}
|
||||
});
|
||||
6
miniprogram/pages/index/index.json
Normal file
6
miniprogram/pages/index/index.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"navigationBarTitleText": "小智AI会员",
|
||||
"enablePullDownRefresh": true,
|
||||
"onReachBottomDistance": 50,
|
||||
"backgroundTextStyle": "dark"
|
||||
}
|
||||
152
miniprogram/pages/index/index.wxml
Normal file
152
miniprogram/pages/index/index.wxml
Normal file
@@ -0,0 +1,152 @@
|
||||
<!--pages/index/index.wxml-->
|
||||
<view class="container">
|
||||
<!-- 顶部用户信息卡片 -->
|
||||
<view class="user-card card">
|
||||
<view class="user-info flex align-center">
|
||||
<image class="avatar" src="{{userInfo.avatarUrl || '/images/default-avatar.png'}}" mode="aspectFill"></image>
|
||||
<view class="user-details flex-1 ml-3">
|
||||
<view class="username text-lg font-bold">{{userInfo.nickName || '未登录'}}</view>
|
||||
<view class="member-status flex align-center mt-1">
|
||||
<view class="member-badge {{memberInfo.level || 'free'}}">
|
||||
{{memberInfo.levelText || '普通用户'}}
|
||||
</view>
|
||||
<view class="expire-info text-sm text-muted ml-2" wx:if="{{memberInfo.expireInfo}}">
|
||||
{{memberInfo.expireInfo.text}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="login-btn" wx:if="{{!isLoggedIn}}" bindtap="handleLogin">
|
||||
<text class="btn btn-primary btn-small">登录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 会员权益快捷入口 -->
|
||||
<view class="benefits-card card" wx:if="{{isLoggedIn}}">
|
||||
<view class="card-header">
|
||||
<view class="flex align-center justify-between">
|
||||
<text class="text-lg font-bold">会员权益</text>
|
||||
<text class="text-primary text-sm" bindtap="navigateToCenter">查看全部</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="benefits-grid">
|
||||
<view class="benefit-item" wx:for="{{quickBenefits}}" wx:key="id" bindtap="handleBenefitClick" data-benefit="{{item}}">
|
||||
<view class="benefit-icon">
|
||||
<image src="{{item.icon}}" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="benefit-name text-sm">{{item.name}}</view>
|
||||
<view class="benefit-status text-xs" wx:if="{{item.available}}">
|
||||
<text class="text-success">可用</text>
|
||||
</view>
|
||||
<view class="benefit-status text-xs" wx:else>
|
||||
<text class="text-muted">不可用</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能菜单 -->
|
||||
<view class="menu-card card">
|
||||
<view class="card-header">
|
||||
<text class="text-lg font-bold">功能菜单</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="menu-grid">
|
||||
<view class="menu-item" wx:for="{{menuItems}}" wx:key="id" bindtap="handleMenuClick" data-menu="{{item}}">
|
||||
<view class="menu-icon">
|
||||
<image src="{{item.icon}}" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="menu-name text-sm">{{item.name}}</view>
|
||||
<view class="menu-badge" wx:if="{{item.badge}}">
|
||||
<text class="text-xs">{{item.badge}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 会员升级提示 -->
|
||||
<view class="upgrade-card card" wx:if="{{showUpgradePrompt}}">
|
||||
<view class="upgrade-content">
|
||||
<view class="upgrade-icon">
|
||||
<image src="/images/upgrade-icon.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="upgrade-text">
|
||||
<view class="upgrade-title text-lg font-bold">升级会员,享受更多特权</view>
|
||||
<view class="upgrade-desc text-sm text-muted mt-1">
|
||||
解锁专属功能,提升使用体验
|
||||
</view>
|
||||
</view>
|
||||
<view class="upgrade-action">
|
||||
<text class="btn btn-primary btn-small" bindtap="navigateToUpgrade">立即升级</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近活动 -->
|
||||
<view class="activity-card card" wx:if="{{recentActivities.length > 0}}">
|
||||
<view class="card-header">
|
||||
<view class="flex align-center justify-between">
|
||||
<text class="text-lg font-bold">最近活动</text>
|
||||
<text class="text-primary text-sm" bindtap="navigateToHistory">查看更多</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="activity-list">
|
||||
<view class="activity-item" wx:for="{{recentActivities}}" wx:key="id">
|
||||
<view class="activity-icon">
|
||||
<image src="{{item.icon}}" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="activity-content flex-1">
|
||||
<view class="activity-title text-sm">{{item.title}}</view>
|
||||
<view class="activity-time text-xs text-muted">{{item.timeText}}</view>
|
||||
</view>
|
||||
<view class="activity-status">
|
||||
<text class="text-xs {{item.statusClass}}">{{item.statusText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态提示 -->
|
||||
<view class="empty-state" wx:if="{{!isLoggedIn}}">
|
||||
<image class="empty-icon" src="/images/empty-login.png" mode="aspectFit"></image>
|
||||
<view class="empty-text">登录后查看更多功能</view>
|
||||
<view class="empty-desc">享受专属会员权益和服务</view>
|
||||
<text class="btn btn-primary mt-3" bindtap="handleLogin">立即登录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 权益详情弹窗 -->
|
||||
<view class="benefit-modal" wx:if="{{showBenefitModal}}" bindtap="closeBenefitModal">
|
||||
<view class="modal-content" catchtap="stopPropagation">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{selectedBenefit.name}}</text>
|
||||
<view class="modal-close" bindtap="closeBenefitModal">
|
||||
<image src="/images/close-icon.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="benefit-detail">
|
||||
<image class="benefit-large-icon" src="{{selectedBenefit.icon}}" mode="aspectFit"></image>
|
||||
<view class="benefit-description">{{selectedBenefit.description}}</view>
|
||||
<view class="benefit-usage" wx:if="{{selectedBenefit.usage}}">
|
||||
<text class="text-sm text-muted">使用次数:{{selectedBenefit.usage.used}}/{{selectedBenefit.usage.total}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<text class="btn btn-secondary" bindtap="closeBenefitModal">关闭</text>
|
||||
<text class="btn btn-primary ml-2" wx:if="{{selectedBenefit.available}}" bindtap="useBenefit">立即使用</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading" wx:if="{{loading}}">
|
||||
<view class="loading-spinner"></view>
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
428
miniprogram/pages/index/index.wxss
Normal file
428
miniprogram/pages/index/index.wxss
Normal file
@@ -0,0 +1,428 @@
|
||||
/* pages/index/index.wxss */
|
||||
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
background: #F8F9FA;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 用户信息卡片 */
|
||||
.user-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #FFFFFF;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
padding: 40rpx 30rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.user-details {
|
||||
margin-left: 30rpx;
|
||||
}
|
||||
|
||||
.username {
|
||||
color: #FFFFFF;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.member-status {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.member-badge {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.member-badge.free {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.member-badge.vip {
|
||||
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.member-badge.svip {
|
||||
background: linear-gradient(135deg, #FF6B35, #F7931E);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.expire-info {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
/* 会员权益卡片 */
|
||||
.benefits-card {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.benefits-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 30rpx;
|
||||
}
|
||||
|
||||
.benefit-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 20rpx 10rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #F8F9FA;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.benefit-item:active {
|
||||
background: #E9ECEF;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.benefit-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.benefit-icon image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.benefit-name {
|
||||
margin-bottom: 8rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.benefit-status {
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
/* 功能菜单卡片 */
|
||||
.menu-card {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.menu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 30rpx;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 30rpx 20rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menu-item:active {
|
||||
background: #F8F9FA;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.menu-icon image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.menu-name {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.menu-badge {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
background: #FF4757;
|
||||
color: #FFFFFF;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 10rpx;
|
||||
min-width: 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 升级提示卡片 */
|
||||
.upgrade-card {
|
||||
margin-bottom: 30rpx;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.upgrade-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.upgrade-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.upgrade-icon image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.upgrade-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.upgrade-title {
|
||||
color: #FFFFFF;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.upgrade-desc {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.upgrade-action {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
/* 最近活动卡片 */
|
||||
.activity-card {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background: #F8F9FA;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.activity-icon image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
color: #6C757D;
|
||||
}
|
||||
|
||||
.activity-status {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin-bottom: 30rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
color: #333333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 28rpx;
|
||||
color: #6C757D;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
/* 权益详情弹窗 */
|
||||
.benefit-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #FFFFFF;
|
||||
border-radius: 20rpx;
|
||||
margin: 40rpx;
|
||||
max-width: 600rpx;
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #E9ECEF;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
.modal-close image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 30rpx;
|
||||
max-height: 400rpx;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.benefit-detail {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.benefit-large-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.benefit-description {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.benefit-usage {
|
||||
padding: 16rpx 20rpx;
|
||||
background: #F8F9FA;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 20rpx 30rpx;
|
||||
border-top: 1rpx solid #E9ECEF;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx 40rpx;
|
||||
color: #6C757D;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 4rpx solid #F3F3F3;
|
||||
border-top: 4rpx solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
@media (max-width: 400px) {
|
||||
.benefits-grid,
|
||||
.menu-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 320px) {
|
||||
.benefits-grid,
|
||||
.menu-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
margin-left: 0;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
margin-left: 0;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
}
|
||||
271
miniprogram/pages/member/bind/bind.wxml
Normal file
271
miniprogram/pages/member/bind/bind.wxml
Normal file
@@ -0,0 +1,271 @@
|
||||
<!--pages/member/bind/bind.wxml-->
|
||||
<view class="container">
|
||||
<!-- 页面标题 -->
|
||||
<view class="page-header">
|
||||
<view class="header-content">
|
||||
<image class="header-icon" src="/images/bind/member-icon.png" mode="aspectFit"></image>
|
||||
<view class="header-text">
|
||||
<view class="header-title">会员绑定</view>
|
||||
<view class="header-desc">绑定会员账号,享受专属权益</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 当前状态卡片 -->
|
||||
<view class="status-card card">
|
||||
<view class="status-content">
|
||||
<view class="status-icon">
|
||||
<image src="{{statusInfo.icon}}" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="status-text">
|
||||
<view class="status-title">{{statusInfo.title}}</view>
|
||||
<view class="status-desc">{{statusInfo.desc}}</view>
|
||||
</view>
|
||||
<view class="status-badge {{statusInfo.badgeClass}}" wx:if="{{statusInfo.badge}}">
|
||||
{{statusInfo.badge}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 绑定表单 -->
|
||||
<view class="bind-form card" wx:if="{{!memberInfo.isBound}}">
|
||||
<view class="card-header">
|
||||
<text class="text-lg font-bold">绑定会员账号</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<form bindsubmit="handleSubmit">
|
||||
<!-- 手机号输入 -->
|
||||
<view class="form-group">
|
||||
<view class="form-label">手机号码</view>
|
||||
<view class="input-group">
|
||||
<input
|
||||
class="form-input"
|
||||
type="number"
|
||||
placeholder="请输入手机号码"
|
||||
value="{{formData.phone}}"
|
||||
bindinput="onPhoneInput"
|
||||
maxlength="11"
|
||||
/>
|
||||
<view class="input-suffix" wx:if="{{formData.phone && isValidPhone}}">
|
||||
<image src="/images/icons/check-green.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-error" wx:if="{{errors.phone}}">{{errors.phone}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 验证码输入 -->
|
||||
<view class="form-group">
|
||||
<view class="form-label">验证码</view>
|
||||
<view class="input-group">
|
||||
<input
|
||||
class="form-input flex-1"
|
||||
type="number"
|
||||
placeholder="请输入验证码"
|
||||
value="{{formData.verifyCode}}"
|
||||
bindinput="onVerifyCodeInput"
|
||||
maxlength="6"
|
||||
/>
|
||||
<button
|
||||
class="verify-btn {{canSendCode ? 'active' : 'disabled'}}"
|
||||
bindtap="sendVerifyCode"
|
||||
disabled="{{!canSendCode}}"
|
||||
>
|
||||
{{verifyCodeText}}
|
||||
</button>
|
||||
</view>
|
||||
<view class="form-error" wx:if="{{errors.verifyCode}}">{{errors.verifyCode}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 会员卡号输入 -->
|
||||
<view class="form-group">
|
||||
<view class="form-label">会员卡号</view>
|
||||
<view class="input-group">
|
||||
<input
|
||||
class="form-input"
|
||||
type="text"
|
||||
placeholder="请输入会员卡号"
|
||||
value="{{formData.memberCard}}"
|
||||
bindinput="onMemberCardInput"
|
||||
/>
|
||||
<view class="input-suffix" bindtap="scanMemberCard">
|
||||
<image src="/images/icons/scan.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-error" wx:if="{{errors.memberCard}}">{{errors.memberCard}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 密码输入 -->
|
||||
<view class="form-group">
|
||||
<view class="form-label">会员密码</view>
|
||||
<view class="input-group">
|
||||
<input
|
||||
class="form-input"
|
||||
type="{{showPassword ? 'text' : 'password'}}"
|
||||
placeholder="请输入会员密码"
|
||||
value="{{formData.password}}"
|
||||
bindinput="onPasswordInput"
|
||||
/>
|
||||
<view class="input-suffix" bindtap="togglePassword">
|
||||
<image src="/images/icons/{{showPassword ? 'eye-off' : 'eye'}}.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-error" wx:if="{{errors.password}}">{{errors.password}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 协议同意 -->
|
||||
<view class="agreement-group">
|
||||
<label class="agreement-item">
|
||||
<checkbox
|
||||
value="agree"
|
||||
checked="{{agreeTerms}}"
|
||||
bindchange="onAgreeChange"
|
||||
/>
|
||||
<text class="agreement-text">
|
||||
我已阅读并同意
|
||||
<text class="link" bindtap="showTerms">《会员服务协议》</text>
|
||||
和
|
||||
<text class="link" bindtap="showPrivacy">《隐私政策》</text>
|
||||
</text>
|
||||
</label>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<button
|
||||
class="btn btn-primary btn-block btn-large {{canSubmit ? '' : 'btn-disabled'}}"
|
||||
form-type="submit"
|
||||
disabled="{{!canSubmit || submitting}}"
|
||||
>
|
||||
{{submitting ? '绑定中...' : '立即绑定'}}
|
||||
</button>
|
||||
</form>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已绑定状态 -->
|
||||
<view class="bound-info card" wx:if="{{memberInfo.isBound}}">
|
||||
<view class="card-header">
|
||||
<text class="text-lg font-bold">会员信息</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="member-details">
|
||||
<view class="detail-item">
|
||||
<view class="detail-label">会员等级</view>
|
||||
<view class="detail-value">
|
||||
<view class="member-badge {{memberInfo.level}}">{{memberInfo.levelText}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<view class="detail-label">会员卡号</view>
|
||||
<view class="detail-value">{{memberInfo.memberCard}}</view>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<view class="detail-label">绑定手机</view>
|
||||
<view class="detail-value">{{memberInfo.phone}}</view>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<view class="detail-label">绑定时间</view>
|
||||
<view class="detail-value">{{memberInfo.bindTimeText}}</view>
|
||||
</view>
|
||||
<view class="detail-item" wx:if="{{memberInfo.expireTime}}">
|
||||
<view class="detail-label">到期时间</view>
|
||||
<view class="detail-value {{memberInfo.expireInfo.isExpired ? 'text-danger' : 'text-success'}}">
|
||||
{{memberInfo.expireInfo.text}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-buttons">
|
||||
<button class="btn btn-outline btn-block" bindtap="showUnbindConfirm">
|
||||
解除绑定
|
||||
</button>
|
||||
<button class="btn btn-primary btn-block mt-2" bindtap="navigateToCenter">
|
||||
查看权益
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 绑定说明 -->
|
||||
<view class="help-card card">
|
||||
<view class="card-header">
|
||||
<text class="text-lg font-bold">绑定说明</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="help-list">
|
||||
<view class="help-item">
|
||||
<view class="help-icon">
|
||||
<image src="/images/icons/info.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="help-text">绑定后可享受会员专属权益和服务</view>
|
||||
</view>
|
||||
<view class="help-item">
|
||||
<view class="help-icon">
|
||||
<image src="/images/icons/security.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="help-text">您的个人信息将被严格保护</view>
|
||||
</view>
|
||||
<view class="help-item">
|
||||
<view class="help-icon">
|
||||
<image src="/images/icons/support.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="help-text">如有问题请联系客服:400-123-4567</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 解绑确认弹窗 -->
|
||||
<view class="unbind-modal" wx:if="{{showUnbindModal}}" bindtap="hideUnbindConfirm">
|
||||
<view class="modal-content" catchtap="stopPropagation">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">确认解除绑定</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="unbind-warning">
|
||||
<image class="warning-icon" src="/images/icons/warning.png" mode="aspectFit"></image>
|
||||
<view class="warning-text">
|
||||
<view class="warning-title">解除绑定后将失去以下权益:</view>
|
||||
<view class="warning-list">
|
||||
<text>• 会员专属功能和服务</text>
|
||||
<text>• 积分和优惠券</text>
|
||||
<text>• 会员等级和特权</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="btn btn-secondary" bindtap="hideUnbindConfirm">取消</button>
|
||||
<button class="btn btn-danger ml-2" bindtap="confirmUnbind">确认解绑</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 协议弹窗 -->
|
||||
<view class="agreement-modal" wx:if="{{showAgreementModal}}" bindtap="hideAgreement">
|
||||
<view class="modal-content" catchtap="stopPropagation">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{agreementTitle}}</text>
|
||||
<view class="modal-close" bindtap="hideAgreement">
|
||||
<image src="/images/icons/close.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<scroll-view class="agreement-content" scroll-y="true">
|
||||
<rich-text nodes="{{agreementContent}}"></rich-text>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="btn btn-primary btn-block" bindtap="hideAgreement">我知道了</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-overlay" wx:if="{{loading}}">
|
||||
<view class="loading-content">
|
||||
<view class="loading-spinner"></view>
|
||||
<text>{{loadingText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
489
miniprogram/utils/api.js
Normal file
489
miniprogram/utils/api.js
Normal file
@@ -0,0 +1,489 @@
|
||||
/**
|
||||
* API服务工具
|
||||
* 统一处理网络请求、错误处理、token管理等
|
||||
*/
|
||||
|
||||
const app = getApp();
|
||||
|
||||
// API配置
|
||||
const API_CONFIG = {
|
||||
baseUrl: 'https://api.xiaozhi.com', // 生产环境API地址
|
||||
timeout: 10000,
|
||||
retryCount: 3,
|
||||
retryDelay: 1000
|
||||
};
|
||||
|
||||
// 请求状态码
|
||||
const STATUS_CODE = {
|
||||
SUCCESS: 200,
|
||||
UNAUTHORIZED: 401,
|
||||
FORBIDDEN: 403,
|
||||
NOT_FOUND: 404,
|
||||
SERVER_ERROR: 500,
|
||||
NETWORK_ERROR: -1
|
||||
};
|
||||
|
||||
// 错误消息映射
|
||||
const ERROR_MESSAGES = {
|
||||
[STATUS_CODE.UNAUTHORIZED]: '登录已过期,请重新登录',
|
||||
[STATUS_CODE.FORBIDDEN]: '没有访问权限',
|
||||
[STATUS_CODE.NOT_FOUND]: '请求的资源不存在',
|
||||
[STATUS_CODE.SERVER_ERROR]: '服务器内部错误',
|
||||
[STATUS_CODE.NETWORK_ERROR]: '网络连接失败,请检查网络设置'
|
||||
};
|
||||
|
||||
/**
|
||||
* 基础请求方法
|
||||
* @param {Object} options 请求配置
|
||||
* @returns {Promise} 请求结果
|
||||
*/
|
||||
function request(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const {
|
||||
url,
|
||||
method = 'GET',
|
||||
data = {},
|
||||
header = {},
|
||||
needAuth = true,
|
||||
showLoading = true,
|
||||
loadingText = '加载中...',
|
||||
retryCount = 0
|
||||
} = options;
|
||||
|
||||
// 显示加载提示
|
||||
if (showLoading) {
|
||||
wx.showLoading({
|
||||
title: loadingText,
|
||||
mask: true
|
||||
});
|
||||
}
|
||||
|
||||
// 构建请求头
|
||||
const requestHeader = {
|
||||
'Content-Type': 'application/json',
|
||||
...header
|
||||
};
|
||||
|
||||
// 添加认证token
|
||||
if (needAuth && app.globalData.token) {
|
||||
requestHeader['Authorization'] = `Bearer ${app.globalData.token}`;
|
||||
}
|
||||
|
||||
// 发起请求
|
||||
wx.request({
|
||||
url: `${API_CONFIG.baseUrl}${url}`,
|
||||
method,
|
||||
data,
|
||||
header: requestHeader,
|
||||
timeout: API_CONFIG.timeout,
|
||||
success: (res) => {
|
||||
if (showLoading) {
|
||||
wx.hideLoading();
|
||||
}
|
||||
|
||||
const { statusCode, data: responseData } = res;
|
||||
|
||||
// 请求成功
|
||||
if (statusCode === STATUS_CODE.SUCCESS) {
|
||||
if (responseData.code === 0) {
|
||||
resolve(responseData);
|
||||
} else {
|
||||
// 业务错误
|
||||
handleBusinessError(responseData, reject);
|
||||
}
|
||||
} else {
|
||||
// HTTP错误
|
||||
handleHttpError(statusCode, reject, options, retryCount);
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
if (showLoading) {
|
||||
wx.hideLoading();
|
||||
}
|
||||
|
||||
console.error('Request failed:', error);
|
||||
|
||||
// 网络错误重试
|
||||
if (retryCount < API_CONFIG.retryCount) {
|
||||
setTimeout(() => {
|
||||
request({
|
||||
...options,
|
||||
retryCount: retryCount + 1
|
||||
}).then(resolve).catch(reject);
|
||||
}, API_CONFIG.retryDelay);
|
||||
} else {
|
||||
handleNetworkError(error, reject);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务错误
|
||||
* @param {Object} responseData 响应数据
|
||||
* @param {Function} reject 拒绝函数
|
||||
*/
|
||||
function handleBusinessError(responseData, reject) {
|
||||
const { code, message } = responseData;
|
||||
|
||||
// 特殊错误码处理
|
||||
switch (code) {
|
||||
case 401:
|
||||
// token过期,清除登录状态
|
||||
app.clearLoginStatus();
|
||||
wx.showToast({
|
||||
title: '登录已过期',
|
||||
icon: 'none'
|
||||
});
|
||||
// 跳转到登录页
|
||||
wx.navigateTo({
|
||||
url: '/pages/login/login'
|
||||
});
|
||||
break;
|
||||
case 403:
|
||||
wx.showToast({
|
||||
title: '权限不足',
|
||||
icon: 'none'
|
||||
});
|
||||
break;
|
||||
default:
|
||||
wx.showToast({
|
||||
title: message || '操作失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
|
||||
reject({
|
||||
code,
|
||||
message,
|
||||
type: 'business'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理HTTP错误
|
||||
* @param {Number} statusCode 状态码
|
||||
* @param {Function} reject 拒绝函数
|
||||
* @param {Object} options 请求选项
|
||||
* @param {Number} retryCount 重试次数
|
||||
*/
|
||||
function handleHttpError(statusCode, reject, options, retryCount) {
|
||||
const message = ERROR_MESSAGES[statusCode] || '请求失败';
|
||||
|
||||
// 401错误特殊处理
|
||||
if (statusCode === STATUS_CODE.UNAUTHORIZED) {
|
||||
app.clearLoginStatus();
|
||||
wx.navigateTo({
|
||||
url: '/pages/login/login'
|
||||
});
|
||||
}
|
||||
|
||||
// 5xx错误重试
|
||||
if (statusCode >= 500 && retryCount < API_CONFIG.retryCount) {
|
||||
setTimeout(() => {
|
||||
request({
|
||||
...options,
|
||||
retryCount: retryCount + 1
|
||||
}).then(resolve).catch(reject);
|
||||
}, API_CONFIG.retryDelay);
|
||||
return;
|
||||
}
|
||||
|
||||
wx.showToast({
|
||||
title: message,
|
||||
icon: 'none'
|
||||
});
|
||||
|
||||
reject({
|
||||
code: statusCode,
|
||||
message,
|
||||
type: 'http'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理网络错误
|
||||
* @param {Object} error 错误对象
|
||||
* @param {Function} reject 拒绝函数
|
||||
*/
|
||||
function handleNetworkError(error, reject) {
|
||||
wx.showToast({
|
||||
title: '网络连接失败',
|
||||
icon: 'none'
|
||||
});
|
||||
|
||||
reject({
|
||||
code: STATUS_CODE.NETWORK_ERROR,
|
||||
message: '网络连接失败',
|
||||
type: 'network',
|
||||
error
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET请求
|
||||
* @param {String} url 请求地址
|
||||
* @param {Object} params 请求参数
|
||||
* @param {Object} options 其他选项
|
||||
* @returns {Promise} 请求结果
|
||||
*/
|
||||
function get(url, params = {}, options = {}) {
|
||||
const queryString = Object.keys(params)
|
||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
|
||||
.join('&');
|
||||
|
||||
const requestUrl = queryString ? `${url}?${queryString}` : url;
|
||||
|
||||
return request({
|
||||
url: requestUrl,
|
||||
method: 'GET',
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST请求
|
||||
* @param {String} url 请求地址
|
||||
* @param {Object} data 请求数据
|
||||
* @param {Object} options 其他选项
|
||||
* @returns {Promise} 请求结果
|
||||
*/
|
||||
function post(url, data = {}, options = {}) {
|
||||
return request({
|
||||
url,
|
||||
method: 'POST',
|
||||
data,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT请求
|
||||
* @param {String} url 请求地址
|
||||
* @param {Object} data 请求数据
|
||||
* @param {Object} options 其他选项
|
||||
* @returns {Promise} 请求结果
|
||||
*/
|
||||
function put(url, data = {}, options = {}) {
|
||||
return request({
|
||||
url,
|
||||
method: 'PUT',
|
||||
data,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE请求
|
||||
* @param {String} url 请求地址
|
||||
* @param {Object} options 其他选项
|
||||
* @returns {Promise} 请求结果
|
||||
*/
|
||||
function del(url, options = {}) {
|
||||
return request({
|
||||
url,
|
||||
method: 'DELETE',
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传
|
||||
* @param {String} url 上传地址
|
||||
* @param {String} filePath 文件路径
|
||||
* @param {Object} formData 表单数据
|
||||
* @param {Object} options 其他选项
|
||||
* @returns {Promise} 上传结果
|
||||
*/
|
||||
function upload(url, filePath, formData = {}, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const {
|
||||
name = 'file',
|
||||
header = {},
|
||||
needAuth = true,
|
||||
showLoading = true
|
||||
} = options;
|
||||
|
||||
if (showLoading) {
|
||||
wx.showLoading({
|
||||
title: '上传中...',
|
||||
mask: true
|
||||
});
|
||||
}
|
||||
|
||||
// 构建请求头
|
||||
const requestHeader = { ...header };
|
||||
if (needAuth && app.globalData.token) {
|
||||
requestHeader['Authorization'] = `Bearer ${app.globalData.token}`;
|
||||
}
|
||||
|
||||
wx.uploadFile({
|
||||
url: `${API_CONFIG.baseUrl}${url}`,
|
||||
filePath,
|
||||
name,
|
||||
formData,
|
||||
header: requestHeader,
|
||||
success: (res) => {
|
||||
if (showLoading) {
|
||||
wx.hideLoading();
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(res.data);
|
||||
if (data.code === 0) {
|
||||
resolve(data);
|
||||
} else {
|
||||
handleBusinessError(data, reject);
|
||||
}
|
||||
} catch (error) {
|
||||
reject({
|
||||
code: -1,
|
||||
message: '响应数据解析失败',
|
||||
type: 'parse',
|
||||
error
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
if (showLoading) {
|
||||
wx.hideLoading();
|
||||
}
|
||||
handleNetworkError(error, reject);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 会员相关API
|
||||
const memberAPI = {
|
||||
// 获取用户信息
|
||||
getUserInfo() {
|
||||
return get('/api/member/user/info');
|
||||
},
|
||||
|
||||
// 微信登录
|
||||
wxLogin(code) {
|
||||
return post('/api/member/auth/wx-login', { code }, { needAuth: false });
|
||||
},
|
||||
|
||||
// 获取会员信息
|
||||
getMemberInfo() {
|
||||
return get('/api/member/info');
|
||||
},
|
||||
|
||||
// 绑定会员
|
||||
bindMember(memberData) {
|
||||
return post('/api/member/bind', memberData);
|
||||
},
|
||||
|
||||
// 解绑会员
|
||||
unbindMember() {
|
||||
return post('/api/member/unbind');
|
||||
},
|
||||
|
||||
// 获取会员权益
|
||||
getMemberBenefits() {
|
||||
return get('/api/member/benefits');
|
||||
},
|
||||
|
||||
// 验证会员权益
|
||||
verifyBenefit(benefitType) {
|
||||
return post('/api/member/verify-benefit', { benefitType });
|
||||
},
|
||||
|
||||
// 获取会员等级配置
|
||||
getMemberLevels() {
|
||||
return get('/api/member/levels');
|
||||
},
|
||||
|
||||
// 升级会员
|
||||
upgradeMember(targetLevel) {
|
||||
return post('/api/member/upgrade', { targetLevel });
|
||||
},
|
||||
|
||||
// 获取绑定历史
|
||||
getBindHistory(page = 1, size = 10) {
|
||||
return get('/api/member/bind-history', { page, size });
|
||||
},
|
||||
|
||||
// 获取权益使用记录
|
||||
getBenefitUsage(page = 1, size = 10) {
|
||||
return get('/api/member/benefit-usage', { page, size });
|
||||
},
|
||||
|
||||
// 邀请好友
|
||||
inviteFriend(inviteCode) {
|
||||
return post('/api/member/invite', { inviteCode });
|
||||
},
|
||||
|
||||
// 获取邀请记录
|
||||
getInviteHistory(page = 1, size = 10) {
|
||||
return get('/api/member/invite-history', { page, size });
|
||||
}
|
||||
};
|
||||
|
||||
// 支付相关API
|
||||
const paymentAPI = {
|
||||
// 创建支付订单
|
||||
createOrder(orderData) {
|
||||
return post('/api/payment/create-order', orderData);
|
||||
},
|
||||
|
||||
// 查询订单状态
|
||||
queryOrder(orderId) {
|
||||
return get(`/api/payment/order/${orderId}`);
|
||||
},
|
||||
|
||||
// 取消订单
|
||||
cancelOrder(orderId) {
|
||||
return post(`/api/payment/order/${orderId}/cancel`);
|
||||
},
|
||||
|
||||
// 申请退款
|
||||
requestRefund(orderId, refundData) {
|
||||
return post(`/api/payment/order/${orderId}/refund`, refundData);
|
||||
},
|
||||
|
||||
// 查询退款状态
|
||||
queryRefund(refundId) {
|
||||
return get(`/api/payment/refund/${refundId}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 通用API
|
||||
const commonAPI = {
|
||||
// 获取系统配置
|
||||
getSystemConfig() {
|
||||
return get('/api/common/config', {}, { needAuth: false });
|
||||
},
|
||||
|
||||
// 上传图片
|
||||
uploadImage(filePath) {
|
||||
return upload('/api/common/upload/image', filePath);
|
||||
},
|
||||
|
||||
// 发送验证码
|
||||
sendVerifyCode(phone) {
|
||||
return post('/api/common/send-verify-code', { phone }, { needAuth: false });
|
||||
},
|
||||
|
||||
// 验证验证码
|
||||
verifyCode(phone, code) {
|
||||
return post('/api/common/verify-code', { phone, code }, { needAuth: false });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
request,
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
del: del,
|
||||
upload,
|
||||
memberAPI,
|
||||
paymentAPI,
|
||||
commonAPI,
|
||||
API_CONFIG,
|
||||
STATUS_CODE
|
||||
};
|
||||
543
miniprogram/utils/util.js
Normal file
543
miniprogram/utils/util.js
Normal file
@@ -0,0 +1,543 @@
|
||||
/**
|
||||
* 通用工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* @param {Date|String|Number} date 日期
|
||||
* @param {String} format 格式化字符串
|
||||
* @returns {String} 格式化后的时间字符串
|
||||
*/
|
||||
function formatTime(date, format = 'YYYY-MM-DD HH:mm:ss') {
|
||||
if (!date) return '';
|
||||
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const hour = String(d.getHours()).padStart(2, '0');
|
||||
const minute = String(d.getMinutes()).padStart(2, '0');
|
||||
const second = String(d.getSeconds()).padStart(2, '0');
|
||||
|
||||
return format
|
||||
.replace('YYYY', year)
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hour)
|
||||
.replace('mm', minute)
|
||||
.replace('ss', second);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化相对时间
|
||||
* @param {Date|String|Number} date 日期
|
||||
* @returns {String} 相对时间字符串
|
||||
*/
|
||||
function formatRelativeTime(date) {
|
||||
if (!date) return '';
|
||||
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - d.getTime();
|
||||
const seconds = Math.floor(diff / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (seconds < 60) {
|
||||
return '刚刚';
|
||||
} else if (minutes < 60) {
|
||||
return `${minutes}分钟前`;
|
||||
} else if (hours < 24) {
|
||||
return `${hours}小时前`;
|
||||
} else if (days < 7) {
|
||||
return `${days}天前`;
|
||||
} else {
|
||||
return formatTime(date, 'MM-DD');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
* @param {Function} func 要防抖的函数
|
||||
* @param {Number} delay 延迟时间
|
||||
* @returns {Function} 防抖后的函数
|
||||
*/
|
||||
function debounce(func, delay = 300) {
|
||||
let timer = null;
|
||||
return function (...args) {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
func.apply(this, args);
|
||||
}, delay);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
* @param {Function} func 要节流的函数
|
||||
* @param {Number} delay 延迟时间
|
||||
* @returns {Function} 节流后的函数
|
||||
*/
|
||||
function throttle(func, delay = 300) {
|
||||
let timer = null;
|
||||
return function (...args) {
|
||||
if (!timer) {
|
||||
timer = setTimeout(() => {
|
||||
func.apply(this, args);
|
||||
timer = null;
|
||||
}, delay);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 深拷贝
|
||||
* @param {Any} obj 要拷贝的对象
|
||||
* @returns {Any} 拷贝后的对象
|
||||
*/
|
||||
function deepClone(obj) {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (obj instanceof Date) {
|
||||
return new Date(obj.getTime());
|
||||
}
|
||||
|
||||
if (obj instanceof Array) {
|
||||
return obj.map(item => deepClone(item));
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
const cloned = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
cloned[key] = deepClone(obj[key]);
|
||||
}
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一ID
|
||||
* @param {String} prefix 前缀
|
||||
* @returns {String} 唯一ID
|
||||
*/
|
||||
function generateId(prefix = '') {
|
||||
const timestamp = Date.now().toString(36);
|
||||
const random = Math.random().toString(36).substr(2, 5);
|
||||
return `${prefix}${timestamp}${random}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证手机号
|
||||
* @param {String} phone 手机号
|
||||
* @returns {Boolean} 是否有效
|
||||
*/
|
||||
function validatePhone(phone) {
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
return phoneRegex.test(phone);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱
|
||||
* @param {String} email 邮箱
|
||||
* @returns {Boolean} 是否有效
|
||||
*/
|
||||
function validateEmail(email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证身份证号
|
||||
* @param {String} idCard 身份证号
|
||||
* @returns {Boolean} 是否有效
|
||||
*/
|
||||
function validateIdCard(idCard) {
|
||||
const idCardRegex = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
|
||||
return idCardRegex.test(idCard);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化金额
|
||||
* @param {Number} amount 金额
|
||||
* @param {Number} decimals 小数位数
|
||||
* @returns {String} 格式化后的金额
|
||||
*/
|
||||
function formatAmount(amount, decimals = 2) {
|
||||
if (isNaN(amount)) return '0.00';
|
||||
return Number(amount).toFixed(decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
* @param {Number} bytes 字节数
|
||||
* @returns {String} 格式化后的文件大小
|
||||
*/
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取URL参数
|
||||
* @param {String} url URL地址
|
||||
* @returns {Object} 参数对象
|
||||
*/
|
||||
function getUrlParams(url) {
|
||||
const params = {};
|
||||
const urlObj = new URL(url);
|
||||
|
||||
for (const [key, value] of urlObj.searchParams) {
|
||||
params[key] = value;
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建URL参数
|
||||
* @param {Object} params 参数对象
|
||||
* @returns {String} 参数字符串
|
||||
*/
|
||||
function buildUrlParams(params) {
|
||||
return Object.keys(params)
|
||||
.filter(key => params[key] !== undefined && params[key] !== null)
|
||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储数据到本地
|
||||
* @param {String} key 键名
|
||||
* @param {Any} data 数据
|
||||
* @param {Number} expire 过期时间(毫秒)
|
||||
*/
|
||||
function setStorage(key, data, expire = 0) {
|
||||
const item = {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
expire
|
||||
};
|
||||
|
||||
try {
|
||||
wx.setStorageSync(key, JSON.stringify(item));
|
||||
} catch (error) {
|
||||
console.error('Storage set error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地获取数据
|
||||
* @param {String} key 键名
|
||||
* @returns {Any} 数据
|
||||
*/
|
||||
function getStorage(key) {
|
||||
try {
|
||||
const item = wx.getStorageSync(key);
|
||||
if (!item) return null;
|
||||
|
||||
const parsed = JSON.parse(item);
|
||||
const { data, timestamp, expire } = parsed;
|
||||
|
||||
// 检查是否过期
|
||||
if (expire > 0 && Date.now() - timestamp > expire) {
|
||||
wx.removeStorageSync(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Storage get error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除本地存储数据
|
||||
* @param {String} key 键名
|
||||
*/
|
||||
function removeStorage(key) {
|
||||
try {
|
||||
wx.removeStorageSync(key);
|
||||
} catch (error) {
|
||||
console.error('Storage remove error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空本地存储
|
||||
*/
|
||||
function clearStorage() {
|
||||
try {
|
||||
wx.clearStorageSync();
|
||||
} catch (error) {
|
||||
console.error('Storage clear error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示成功提示
|
||||
* @param {String} title 提示内容
|
||||
* @param {Number} duration 显示时长
|
||||
*/
|
||||
function showSuccess(title, duration = 2000) {
|
||||
wx.showToast({
|
||||
title,
|
||||
icon: 'success',
|
||||
duration
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误提示
|
||||
* @param {String} title 提示内容
|
||||
* @param {Number} duration 显示时长
|
||||
*/
|
||||
function showError(title, duration = 2000) {
|
||||
wx.showToast({
|
||||
title,
|
||||
icon: 'error',
|
||||
duration
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示普通提示
|
||||
* @param {String} title 提示内容
|
||||
* @param {Number} duration 显示时长
|
||||
*/
|
||||
function showToast(title, duration = 2000) {
|
||||
wx.showToast({
|
||||
title,
|
||||
icon: 'none',
|
||||
duration
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示加载提示
|
||||
* @param {String} title 提示内容
|
||||
*/
|
||||
function showLoading(title = '加载中...') {
|
||||
wx.showLoading({
|
||||
title,
|
||||
mask: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏加载提示
|
||||
*/
|
||||
function hideLoading() {
|
||||
wx.hideLoading();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示确认对话框
|
||||
* @param {String} content 内容
|
||||
* @param {String} title 标题
|
||||
* @returns {Promise<Boolean>} 用户选择结果
|
||||
*/
|
||||
function showConfirm(content, title = '提示') {
|
||||
return new Promise((resolve) => {
|
||||
wx.showModal({
|
||||
title,
|
||||
content,
|
||||
success: (res) => {
|
||||
resolve(res.confirm);
|
||||
},
|
||||
fail: () => {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统信息
|
||||
* @returns {Object} 系统信息
|
||||
*/
|
||||
function getSystemInfo() {
|
||||
try {
|
||||
return wx.getSystemInfoSync();
|
||||
} catch (error) {
|
||||
console.error('Get system info error:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查网络状态
|
||||
* @returns {Promise<Object>} 网络状态
|
||||
*/
|
||||
function getNetworkType() {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.getNetworkType({
|
||||
success: resolve,
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制到剪贴板
|
||||
* @param {String} data 要复制的内容
|
||||
* @returns {Promise<Boolean>} 复制结果
|
||||
*/
|
||||
function copyToClipboard(data) {
|
||||
return new Promise((resolve) => {
|
||||
wx.setClipboardData({
|
||||
data,
|
||||
success: () => {
|
||||
showSuccess('复制成功');
|
||||
resolve(true);
|
||||
},
|
||||
fail: () => {
|
||||
showError('复制失败');
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览图片
|
||||
* @param {String} current 当前图片URL
|
||||
* @param {Array} urls 图片URL列表
|
||||
*/
|
||||
function previewImage(current, urls = []) {
|
||||
wx.previewImage({
|
||||
current,
|
||||
urls: urls.length > 0 ? urls : [current]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 拨打电话
|
||||
* @param {String} phoneNumber 电话号码
|
||||
*/
|
||||
function makePhoneCall(phoneNumber) {
|
||||
wx.makePhoneCall({
|
||||
phoneNumber,
|
||||
fail: () => {
|
||||
showError('拨打电话失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员等级文本
|
||||
* @param {String} level 会员等级
|
||||
* @returns {String} 等级文本
|
||||
*/
|
||||
function getMemberLevelText(level) {
|
||||
const levelMap = {
|
||||
'free': '普通用户',
|
||||
'vip': 'VIP会员',
|
||||
'svip': 'SVIP会员'
|
||||
};
|
||||
return levelMap[level] || '未知等级';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员等级颜色
|
||||
* @param {String} level 会员等级
|
||||
* @returns {String} 颜色值
|
||||
*/
|
||||
function getMemberLevelColor(level) {
|
||||
const colorMap = {
|
||||
'free': '#6C757D',
|
||||
'vip': '#FFD700',
|
||||
'svip': '#FF6B35'
|
||||
};
|
||||
return colorMap[level] || '#6C757D';
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算会员到期时间
|
||||
* @param {String} expireTime 到期时间
|
||||
* @returns {Object} 时间信息
|
||||
*/
|
||||
function calculateMemberExpire(expireTime) {
|
||||
if (!expireTime) {
|
||||
return {
|
||||
isExpired: true,
|
||||
daysLeft: 0,
|
||||
text: '已过期'
|
||||
};
|
||||
}
|
||||
|
||||
const expire = new Date(expireTime);
|
||||
const now = new Date();
|
||||
const diff = expire.getTime() - now.getTime();
|
||||
const daysLeft = Math.ceil(diff / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysLeft <= 0) {
|
||||
return {
|
||||
isExpired: true,
|
||||
daysLeft: 0,
|
||||
text: '已过期'
|
||||
};
|
||||
} else if (daysLeft <= 7) {
|
||||
return {
|
||||
isExpired: false,
|
||||
daysLeft,
|
||||
text: `${daysLeft}天后到期`
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
isExpired: false,
|
||||
daysLeft,
|
||||
text: formatTime(expireTime, 'YYYY-MM-DD') + ' 到期'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
formatTime,
|
||||
formatRelativeTime,
|
||||
debounce,
|
||||
throttle,
|
||||
deepClone,
|
||||
generateId,
|
||||
validatePhone,
|
||||
validateEmail,
|
||||
validateIdCard,
|
||||
formatAmount,
|
||||
formatFileSize,
|
||||
getUrlParams,
|
||||
buildUrlParams,
|
||||
setStorage,
|
||||
getStorage,
|
||||
removeStorage,
|
||||
clearStorage,
|
||||
showSuccess,
|
||||
showError,
|
||||
showToast,
|
||||
showLoading,
|
||||
hideLoading,
|
||||
showConfirm,
|
||||
getSystemInfo,
|
||||
getNetworkType,
|
||||
copyToClipboard,
|
||||
previewImage,
|
||||
makePhoneCall,
|
||||
getMemberLevelText,
|
||||
getMemberLevelColor,
|
||||
calculateMemberExpire
|
||||
};
|
||||
Reference in New Issue
Block a user