更新:增加“我的”相关功能
This commit is contained in:
7
App.vue
7
App.vue
@@ -12,8 +12,9 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import "@/static/tailwind.css";
|
||||
<style lang="scss">
|
||||
@import "@/style/tailwind.css";
|
||||
@import "@/style/ui.scss";
|
||||
/* 覆盖 Tailwind 的默认 block 样式 */
|
||||
img, svg, video, canvas, audio, iframe, embed, object {
|
||||
display: inline-block;
|
||||
@@ -23,6 +24,6 @@ img, svg, video, canvas, audio, iframe, embed, object {
|
||||
}
|
||||
|
||||
button {
|
||||
margin-bottom: 15px;
|
||||
line-height: inherit;
|
||||
}
|
||||
</style>
|
||||
|
||||
217
api/modules/user.ts
Normal file
217
api/modules/user.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
// api/modules/user.ts
|
||||
import { mainClient } from '@/api/clients/main'
|
||||
import type { IApiResponse } from '@/api/types'
|
||||
import type {
|
||||
IUserInfo,
|
||||
IVipInfo,
|
||||
IOrder,
|
||||
IVipPackage,
|
||||
ITransaction,
|
||||
IFeedbackForm,
|
||||
IPageData
|
||||
} from '@/types/user'
|
||||
import { SERVICE_MAP } from '@/api/config'
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
export async function getUserInfo() {
|
||||
const res = await mainClient.request<IApiResponse<{ user: IUserInfo }>>({
|
||||
url: 'common/user/getUserInfo',
|
||||
method: 'POST'
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取VIP信息
|
||||
*/
|
||||
export async function getVipInfo() {
|
||||
const res = await mainClient.request<IApiResponse<{ vipInfo: IVipInfo }>>({
|
||||
url: 'bookAbroad/home/getVipInfo',
|
||||
method: 'POST'
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单列表
|
||||
* @param current 当前页码
|
||||
* @param limit 每页数量
|
||||
*/
|
||||
export async function getOrderList(current: number, limit: number) {
|
||||
const res = await mainClient.request<IApiResponse<{ orders: IPageData<IOrder> }>>({
|
||||
url: 'bookAbroad/home/getAbroadOrderList',
|
||||
method: 'POST',
|
||||
data: { current, limit }
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取VIP套餐列表
|
||||
*/
|
||||
export async function getVipPackages() {
|
||||
const res = await mainClient.request<IApiResponse<{ sysDictDatas: IVipPackage[] }>>({
|
||||
url: 'book/sysdictdata/getData',
|
||||
method: 'POST',
|
||||
data: { dictLabel: 'googleVip' }
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建订单
|
||||
* @param data 订单数据
|
||||
*/
|
||||
export async function createOrder(data: any) {
|
||||
const res = await mainClient.request<IApiResponse<{ orderSn: string }>>({
|
||||
url: 'bookAbroad/order/placeOrder',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取交易记录列表
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
export async function getTransactionList(userId: number) {
|
||||
const res = await mainClient.request<IApiResponse<{ transactionDetailsList: ITransaction[] }>>({
|
||||
url: 'common/transactionDetails/getTransactionDetailsList',
|
||||
method: 'POST',
|
||||
data: { userId }
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户基本信息
|
||||
* @param data 用户信息
|
||||
*/
|
||||
export async function updateUserInfo(data: Partial<IUserInfo>) {
|
||||
const res = await mainClient.request<IApiResponse>({
|
||||
url: 'book/user/update',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户邮箱
|
||||
* @param id 用户ID
|
||||
* @param email 邮箱地址
|
||||
* @param code 验证码
|
||||
*/
|
||||
export async function updateEmail(id: number, email: string, code: string) {
|
||||
const res = await mainClient.request<IApiResponse>({
|
||||
url: 'common/user/updateUserEmail',
|
||||
method: 'POST',
|
||||
data: { id, email, code }
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户密码
|
||||
* @param id 用户ID
|
||||
* @param password 新密码
|
||||
*/
|
||||
export async function updatePassword(id: number, password: string) {
|
||||
const res = await mainClient.request<IApiResponse>({
|
||||
url: 'common/user/setPasswordById',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
data: { id, password }
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮箱验证码
|
||||
* @param email 邮箱地址
|
||||
*/
|
||||
export async function sendEmailCode(email: string) {
|
||||
const res = await mainClient.request<IApiResponse>({
|
||||
url: 'common/user/getMailCaptcha',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
data: { email }
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片
|
||||
* @param filePath 本地文件路径
|
||||
* @returns 图片URL
|
||||
*/
|
||||
export function uploadImage(filePath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: `${SERVICE_MAP.MAIN}oss/fileoss`,
|
||||
filePath,
|
||||
name: 'file',
|
||||
success: (res) => {
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
if (data.url) {
|
||||
resolve(data.url)
|
||||
} else {
|
||||
reject(new Error('上传失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
},
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交问题反馈
|
||||
* @param data 反馈表单数据
|
||||
*/
|
||||
export async function submitFeedback(data: IFeedbackForm) {
|
||||
const res = await mainClient.request<IApiResponse>({
|
||||
url: 'common/sysFeedback/addSysFeedback',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Google支付
|
||||
* @param purchaseToken 购买令牌
|
||||
* @param orderSn 订单号
|
||||
* @param productId 产品ID
|
||||
*/
|
||||
export async function verifyGooglePay(purchaseToken: string, orderSn: string, productId: string) {
|
||||
const res = await mainClient.request<IApiResponse>({
|
||||
url: 'pay/googlepay/googleVerify',
|
||||
method: 'POST',
|
||||
data: { purchaseToken, orderSn, productId }
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证IAP支付
|
||||
* @param data 支付数据
|
||||
*/
|
||||
export async function verifyIAP(data: any) {
|
||||
const res = await mainClient.request<IApiResponse>({
|
||||
url: 'Ipa/veri',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
return res
|
||||
}
|
||||
40
components/nav-bar/nav-bar.vue
Normal file
40
components/nav-bar/nav-bar.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<view class="navbar">
|
||||
<view class="statusBar" :style="{height:getStatusBarHeight()+'px'}"></view>
|
||||
<view class="titleBar" :style="{height:getTitleBarHeight()+'px'}">
|
||||
<wd-navbar :title="title" :left-arrow="leftArrow" @click="handleClickLeft"></wd-navbar>
|
||||
</view>
|
||||
</view>
|
||||
<view :style="{height:getNavBarHeight() +'px'}"></view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { getStatusBarHeight, getTitleBarHeight, getNavBarHeight, getLeftIconLeft} from "@/utils/system"
|
||||
|
||||
defineProps({
|
||||
title:{
|
||||
type:String,
|
||||
default:""
|
||||
},
|
||||
leftArrow:{
|
||||
type:Boolean,
|
||||
default:true
|
||||
}
|
||||
})
|
||||
|
||||
const handleClickLeft = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.navbar{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background: var(--wot-navbar-background);
|
||||
z-index: 9;
|
||||
}
|
||||
</style>
|
||||
115
locale/en.json
115
locale/en.json
@@ -73,5 +73,120 @@
|
||||
"passwordStrengthMedium": "Medium password strength.",
|
||||
"passwordStrengthWeak": "please use a password consisting of at least two types: uppercase and lowercase letters, numbers, and symbols, with a length of 8 characters.",
|
||||
"passwordChanged": "Password changed successfully"
|
||||
},
|
||||
"common": {
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"submit": "Submit",
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"back": "Back",
|
||||
"loadMore": "Load More",
|
||||
"noMore": "No More Data",
|
||||
"noData": "No Data",
|
||||
"loading": "Loading...",
|
||||
"success": "Success",
|
||||
"failed": "Failed",
|
||||
"networkError": "Network Error",
|
||||
"pleaseInput": "Please Input",
|
||||
"pleaseSelect": "Please Select"
|
||||
},
|
||||
"user": {
|
||||
"title": "My",
|
||||
"myOrders": "My Orders",
|
||||
"myBooklist": "My Booklist",
|
||||
"profile": "Profile",
|
||||
"about": "About Us",
|
||||
"feedback": "Feedback",
|
||||
"settings": "Settings",
|
||||
"subscribe": "Subscribe",
|
||||
"myAccount": "My Account",
|
||||
"notSet": "Not Set",
|
||||
"clickToSet": "Set",
|
||||
"vip": "VIP Member",
|
||||
"vipExpireTime": "Expire Time",
|
||||
"nickname": "Nickname",
|
||||
"name": "Name",
|
||||
"email": "Email",
|
||||
"phone": "Phone",
|
||||
"age": "Age",
|
||||
"sex": "Gender",
|
||||
"male": "Male",
|
||||
"female": "Female",
|
||||
"avatar": "Avatar",
|
||||
"changeAvatar": "Change Avatar",
|
||||
"setAvatar": "Set Avatar",
|
||||
"updateSuccess": "Update Success",
|
||||
"updateFailed": "Update Failed",
|
||||
"paymentMethod": "Payment Method",
|
||||
"googlePay": "Google Pay",
|
||||
"applePay": "Apple Pay",
|
||||
"agreeText": "I have agreed",
|
||||
"agreement": "Recharge Agreement",
|
||||
"agreeFirst": "Please agree to the recharge agreement first",
|
||||
"orderSn": "Order Number",
|
||||
"createTime": "Create Time",
|
||||
"amount": "Amount",
|
||||
"paymentSuccess": "Payment Success",
|
||||
"paymentFailed": "Payment Failed",
|
||||
"paymentCanceled": "Payment Canceled",
|
||||
"transactionType": "Transaction Type",
|
||||
"recharge": "Recharge",
|
||||
"consume": "Consume",
|
||||
"remark": "Remark",
|
||||
"hotline": "Hotline",
|
||||
"customerEmail": "Customer Email",
|
||||
"wechat": "WeChat",
|
||||
"wechatTip": "Click on the image and long press to save it to your phone, or use WeChat to scan the QR code to add the customer service official WeChat.",
|
||||
"checkVersion": "Check Version",
|
||||
"version": "Version",
|
||||
"logoff": "Delete Account",
|
||||
"logout": "Logout",
|
||||
"logoffConfirm": "Are you sure you want to delete your account? This action cannot be undone",
|
||||
"logoffConfirmAgain": "The cancellation application has been submitted successfully, please contact customer service for follow-up operations: 022-24142321",
|
||||
"logoutConfirm": "Are you sure you want to logout?",
|
||||
"logoffSuccess": "Account Deleted",
|
||||
"logoutSuccess": "Logout Success",
|
||||
"copySuccess": "Copy Success",
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"appDescription": "An online ebook app offering a wide range of content, including medical literature, classical Chinese studies, literature, and ancient traditional Chinese medicine texts. It features 3D simulated page-flipping, eye-protection mode, and other advanced reading technologies for a comfortable reading experience. The app supports mixed text and image layouts, along with AI-powered voice narration for reading and listening. Some ebooks also have corresponding physical editions, providing users with more reading options.",
|
||||
"issueType": "Issue Type",
|
||||
"account": "Account",
|
||||
"description": "Description",
|
||||
"contactPhone": "Contact Phone",
|
||||
"screenshots": "Screenshots",
|
||||
"issueTypeAccount": "Account Issue",
|
||||
"issueTypePayment": "Payment Issue",
|
||||
"issueTypeOrder": "Order Related",
|
||||
"issueTypeContent": "Content Issue",
|
||||
"issueTypeSuggestion": "Feature Suggestion",
|
||||
"issueTypeBug": "Bug Report",
|
||||
"issueTypeOther": "Other",
|
||||
"feedbackSuccess": "Feedback Submitted",
|
||||
"feedbackFailed": "Feedback Failed",
|
||||
"pleaseInputDescription": "Please enter description",
|
||||
"pleaseInputPhone": "Please enter contact phone",
|
||||
"pleaseInputOrderSn": "Please enter order number",
|
||||
"uploadImageFailed": "Image upload failed",
|
||||
"maxImagesCount": "Maximum 4 images",
|
||||
"permissionDenied": "Permission Denied",
|
||||
"cameraPermission": "Camera permission required",
|
||||
"storagePermission": "Storage permission required",
|
||||
"phonePermission": "Phone permission required",
|
||||
"sendCodeSuccess": "Verification code sent",
|
||||
"sendCodeFailed": "Failed to send code",
|
||||
"countdown": "s to resend",
|
||||
"emailFormat": "Please enter valid email",
|
||||
"passwordFormat": "Invalid password format",
|
||||
"passwordNotMatch": "Passwords do not match",
|
||||
"pleaseInputCode": "Please enter verification code",
|
||||
"pleaseInputPassword": "Please enter password",
|
||||
"pleaseInputPasswordAgain": "Please enter password again",
|
||||
"monthCard": "Monthly",
|
||||
"seasonCard": "Quarterly",
|
||||
"yearCard": "Yearly",
|
||||
"days": "days",
|
||||
"selectPackage": "Please select a package"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,5 +74,120 @@
|
||||
"passwordStrengthMedium": "密码强度中等",
|
||||
"passwordStrengthWeak": "请使用至少包含大小写字母、数字、符号中的两种类型,长度为8个字符的密码",
|
||||
"passwordChanged": "密码修改成功"
|
||||
},
|
||||
"common": {
|
||||
"confirm": "确认",
|
||||
"cancel": "取消",
|
||||
"submit": "提交",
|
||||
"save": "保存",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"back": "返回",
|
||||
"loadMore": "加载更多",
|
||||
"noMore": "没有更多数据",
|
||||
"noData": "暂无数据",
|
||||
"loading": "加载中...",
|
||||
"success": "操作成功",
|
||||
"failed": "操作失败",
|
||||
"networkError": "网络连接失败",
|
||||
"pleaseInput": "请输入",
|
||||
"pleaseSelect": "请选择"
|
||||
},
|
||||
"user": {
|
||||
"title": "我的",
|
||||
"myOrders": "我的订单",
|
||||
"myBooklist": "我的书单",
|
||||
"profile": "个人资料",
|
||||
"about": "关于我们",
|
||||
"feedback": "问题反馈",
|
||||
"settings": "设置",
|
||||
"subscribe": "订阅",
|
||||
"myAccount": "我的账户",
|
||||
"notSet": "未设置",
|
||||
"clickToSet": "设置",
|
||||
"vip": "VIP会员",
|
||||
"vipExpireTime": "到期时间",
|
||||
"nickname": "昵称",
|
||||
"name": "姓名",
|
||||
"email": "邮箱",
|
||||
"phone": "手机号",
|
||||
"age": "年龄",
|
||||
"sex": "性别",
|
||||
"male": "男",
|
||||
"female": "女",
|
||||
"avatar": "头像",
|
||||
"changeAvatar": "更换头像",
|
||||
"setAvatar": "设置头像",
|
||||
"updateSuccess": "更新成功",
|
||||
"updateFailed": "更新失败",
|
||||
"paymentMethod": "支付方式",
|
||||
"googlePay": "Google Pay",
|
||||
"applePay": "Apple Pay",
|
||||
"agreeText": "我已同意",
|
||||
"agreement": "充值协议",
|
||||
"agreeFirst": "请先同意充值协议",
|
||||
"orderSn": "订单编号",
|
||||
"createTime": "创建时间",
|
||||
"amount": "金额",
|
||||
"paymentSuccess": "支付成功",
|
||||
"paymentFailed": "支付失败",
|
||||
"paymentCanceled": "支付已取消",
|
||||
"transactionType": "交易类型",
|
||||
"recharge": "充值",
|
||||
"consume": "消费",
|
||||
"remark": "备注",
|
||||
"hotline": "客服热线",
|
||||
"customerEmail": "客服邮箱",
|
||||
"wechat": "微信号",
|
||||
"wechatTip": "点击图片并长按保存到手机,或使用微信扫描二维码添加客服官方微信。",
|
||||
"checkVersion": "检查版本",
|
||||
"version": "版本",
|
||||
"logoff": "注销账户",
|
||||
"logout": "退出登录",
|
||||
"logoffConfirm": "确定要注销账户吗?此操作不可恢复",
|
||||
"logoffConfirmAgain": "注销申请已提交成功,请联系客服进行后续操作:022-24142321",
|
||||
"logoutConfirm": "确定要退出登录吗?",
|
||||
"logoffSuccess": "注销成功",
|
||||
"logoutSuccess": "退出成功",
|
||||
"copySuccess": "复制成功",
|
||||
"privacyPolicy": "隐私政策",
|
||||
"appDescription": "一款线上电子书APP,包含医学类、国学类、文学类、中医古籍等各种类型。3D仿真翻页、护眼模式等阅读技术,打造舒适阅读体验。图文混排,AI人声读书听书。部分电子书也有对应的纸质书,给予用户更多的阅读选择。",
|
||||
"issueType": "问题类型",
|
||||
"account": "账号",
|
||||
"description": "问题描述",
|
||||
"contactPhone": "联系电话",
|
||||
"screenshots": "截图",
|
||||
"issueTypeAccount": "账号问题",
|
||||
"issueTypePayment": "支付问题",
|
||||
"issueTypeOrder": "订单相关",
|
||||
"issueTypeContent": "内容问题",
|
||||
"issueTypeSuggestion": "功能建议",
|
||||
"issueTypeBug": "BUG反馈",
|
||||
"issueTypeOther": "其他",
|
||||
"feedbackSuccess": "反馈提交成功",
|
||||
"feedbackFailed": "反馈提交失败",
|
||||
"pleaseInputDescription": "请输入问题描述",
|
||||
"pleaseInputPhone": "请输入联系电话",
|
||||
"pleaseInputOrderSn": "请输入订单编号",
|
||||
"uploadImageFailed": "图片上传失败",
|
||||
"maxImagesCount": "最多上传4张图片",
|
||||
"permissionDenied": "权限被拒绝",
|
||||
"cameraPermission": "需要相机权限",
|
||||
"storagePermission": "需要存储权限",
|
||||
"phonePermission": "需要电话权限",
|
||||
"sendCodeSuccess": "验证码发送成功",
|
||||
"sendCodeFailed": "验证码发送失败",
|
||||
"countdown": "秒后重新发送",
|
||||
"emailFormat": "请输入正确的邮箱格式",
|
||||
"passwordFormat": "密码格式不正确",
|
||||
"passwordNotMatch": "两次密码不一致",
|
||||
"pleaseInputCode": "请输入验证码",
|
||||
"pleaseInputPassword": "请输入密码",
|
||||
"pleaseInputPasswordAgain": "请再次输入密码",
|
||||
"monthCard": "月卡",
|
||||
"seasonCard": "季卡",
|
||||
"yearCard": "年卡",
|
||||
"days": "天",
|
||||
"selectPackage": "请选择套餐"
|
||||
}
|
||||
}
|
||||
|
||||
50
pages.json
50
pages.json
@@ -25,6 +25,54 @@
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"navigationBarTextStyle": "black"
|
||||
}
|
||||
}, {
|
||||
"path": "pages/user/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "%user.title%",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}, {
|
||||
"path": "pages/user/order/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "%user.myOrders%",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}, {
|
||||
"path": "pages/user/wallet/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "%user.subscribe%",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}, {
|
||||
"path": "pages/user/wallet/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "%user.myAccount%",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}, {
|
||||
"path": "pages/user/profile/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "%user.profile%",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}, {
|
||||
"path": "pages/user/settings/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "%user.settings%",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}, {
|
||||
"path": "pages/user/about/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "%user.about%",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}, {
|
||||
"path": "pages/user/feedback/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "%user.feedback%",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
// "tabBar": {
|
||||
@@ -61,7 +109,7 @@
|
||||
"text": "%tabar.book%"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/my/my",
|
||||
"pagePath": "pages/user/index",
|
||||
"iconPath": "static/tab/icon4_n.png",
|
||||
"selectedIconPath": "static/tab/icon4_y.png",
|
||||
"text": "%tabar.user%"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="title bg-[red] text-left text-[#fff]">这是一个等待开发的首页</view>
|
||||
<view class="title bg-[red] text-center text-[#fff]">这是一个等待开发的首页</view>
|
||||
<view class="description bg-[red]">首页的内容是在线课程</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -99,9 +99,9 @@
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="loginHelp" v-if="submitClickNum > 0">
|
||||
<text>登录遇到问题?</text><text class="link" @click="onPageJump('/pages/user/workOrder?name=login')">去反馈</text>
|
||||
</view>
|
||||
<!-- <view class="loginHelp" v-if="submitClickNum > 0">
|
||||
<text>登录遇到问题?</text><text class="link" @click="onPageJump('/pages/user/feedback?name=login')">去反馈</text>
|
||||
</view> -->
|
||||
|
||||
<!-- 切换登录方式 -->
|
||||
<view class="qie-huan">
|
||||
@@ -240,7 +240,6 @@ const isCodeEmpty = () => {
|
||||
}
|
||||
// 邮箱格式验证
|
||||
const isEmailVerified = (email: string) => {
|
||||
console.log(email, validateEmail(email))
|
||||
if (!validateEmail(email)) {
|
||||
uni.showToast({
|
||||
title: t('login.emailError'),
|
||||
|
||||
239
pages/user/about/index.vue
Normal file
239
pages/user/about/index.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<view class="about-page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<nav-bar :title="$t('user.about')"></nav-bar>
|
||||
|
||||
<view class="about-content">
|
||||
<!-- 应用信息 -->
|
||||
<view class="app-info">
|
||||
<image
|
||||
src="/static/icon/home_icon_logo.jpg"
|
||||
mode="aspectFit"
|
||||
class="app-logo"
|
||||
/>
|
||||
<text v-if="appVersion" class="app-version">
|
||||
{{ $t('user.version') }} {{ appVersion }}
|
||||
</text>
|
||||
<!-- 应用简介 -->
|
||||
<view class="app-description">
|
||||
<text>{{ $t('user.appDescription') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 联系信息 -->
|
||||
<!-- <view class="contact-info">
|
||||
<view class="contact-item">
|
||||
<text class="label">{{ $t('user.hotline') }}</text>
|
||||
<view class="value-wrapper">
|
||||
<text class="value">022-24142321</text>
|
||||
<image
|
||||
src="/static/icon/tel.png"
|
||||
mode="aspectFit"
|
||||
class="tel-icon"
|
||||
@click="makePhoneCall"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view> -->
|
||||
<wd-cell-group border class="contact-info">
|
||||
<wd-cell :title="$t('user.hotline')" clickable @click="handlePhoneCall">
|
||||
022-24142321
|
||||
<img src="/static/icon/tel.png" width="23" height="23" />
|
||||
</wd-cell>
|
||||
<!-- <wd-cell :title="$t('user.wechat')" value="yilujiankangkefu" clickable @click="handlePhoneCall" /> -->
|
||||
</wd-cell-group>
|
||||
|
||||
<!-- 隐私政策链接 -->
|
||||
<view class="privacy-link">
|
||||
<text class="link-text" @click="openPrivacyPolicy">
|
||||
{{ $t('user.privacyPolicy') }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { makePhoneCall } from '@/utils/index'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 导航栏高度
|
||||
const statusBarHeight = ref(0)
|
||||
const navbarHeight = ref('44px')
|
||||
|
||||
// 应用版本信息
|
||||
const appVersion = ref('')
|
||||
|
||||
/**
|
||||
* 获取应用版本信息
|
||||
*/
|
||||
const getAppVersion = () => {
|
||||
// #ifdef APP-PLUS
|
||||
plus.runtime.getProperty(plus.runtime.appid, (info: any) => {
|
||||
appVersion.value = `${info.name} ${info.version}`
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-PLUS
|
||||
appVersion.value = '1.0.0'
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 拨打电话
|
||||
*/
|
||||
const handlePhoneCall = () => {
|
||||
makePhoneCall('022-24142321', t('user.hotline'), t)
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开隐私政策
|
||||
*/
|
||||
const openPrivacyPolicy = () => {
|
||||
// #ifdef APP-PLUS
|
||||
plus.runtime.openURL('https://www.amazinglimited.com/privacy.html')
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-PLUS
|
||||
uni.showToast({
|
||||
title: '请在APP中查看',
|
||||
icon: 'none'
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getAppVersion()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$theme-color: #54a966;
|
||||
|
||||
.about-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f7faf9;
|
||||
}
|
||||
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 44px;
|
||||
position: relative;
|
||||
|
||||
.navbar-left {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.about-content {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.app-info {
|
||||
background: #fff;
|
||||
border-radius: 15rpx;
|
||||
padding: 60rpx 40rpx;
|
||||
margin-bottom: 20rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.app-logo {
|
||||
width: 150rpx;
|
||||
height: 150rpx;
|
||||
margin: 0 auto 20rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
background: #fff;
|
||||
border-radius: 15rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx;
|
||||
border-top: 1rpx solid #e5e5e5;
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 30rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.value-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.value {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.tel-icon {
|
||||
width: 46rpx;
|
||||
height: 46rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-description {
|
||||
padding: 30rpx 30rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 40rpx;
|
||||
text-indent: 2em;
|
||||
display: block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.privacy-link {
|
||||
text-align: right;
|
||||
padding: 30rpx;
|
||||
|
||||
.link-text {
|
||||
font-size: 28rpx;
|
||||
color: $theme-color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
363
pages/user/feedback/index.vue
Normal file
363
pages/user/feedback/index.vue
Normal file
@@ -0,0 +1,363 @@
|
||||
<template>
|
||||
<view class="feedback-page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<nav-bar :title="$t('user.feedback')"></nav-bar>
|
||||
|
||||
<view class="feedback-content">
|
||||
<view class="feedback-form">
|
||||
<!-- 问题类型 -->
|
||||
<view class="form-item">
|
||||
<text class="label required">{{ $t('user.issueType') }}</text>
|
||||
<wd-select-picker
|
||||
v-model="form.type"
|
||||
type="radio"
|
||||
:columns="issueTypeOptions"
|
||||
:placeholder="$t('common.pleaseSelect')"
|
||||
:show-confirm="false"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 账号 -->
|
||||
<view class="form-item">
|
||||
<text class="label required">{{ $t('user.account') }}</text>
|
||||
<wd-input
|
||||
v-model="form.account"
|
||||
:placeholder="$t('user.account')"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 订单编号(条件显示) -->
|
||||
<view v-if="form.type === '3'" class="form-item">
|
||||
<text class="label required">{{ $t('user.orderSn') }}</text>
|
||||
<wd-input
|
||||
v-model="form.relation"
|
||||
type="number"
|
||||
:placeholder="$t('user.pleaseInputOrderSn')"
|
||||
@input="handleRelationInput"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 问题描述 -->
|
||||
<view class="form-item">
|
||||
<text class="label required">{{ $t('user.description') }}</text>
|
||||
<wd-textarea
|
||||
v-model="form.content"
|
||||
auto-height
|
||||
:placeholder="$t('user.pleaseInputDescription')"
|
||||
:maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 联系电话 -->
|
||||
<view class="form-item">
|
||||
<text class="label required">{{ $t('user.contactPhone') }}</text>
|
||||
<wd-input
|
||||
v-model="form.contactInformation"
|
||||
type="number"
|
||||
:placeholder="$t('user.pleaseInputPhone')"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 截图上传 -->
|
||||
<view class="form-item">
|
||||
<text class="label">{{ $t('user.screenshots') }}</text>
|
||||
<wd-upload
|
||||
:fileList="imageList"
|
||||
:limit="4"
|
||||
action="https://global.nuttyreading.com/oss/fileoss"
|
||||
@change="handleChangeImage"
|
||||
/>
|
||||
<text class="tip-text">{{ $t('user.maxImagesCount') }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-button">
|
||||
<wd-button
|
||||
type="success"
|
||||
block
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ $t('common.submit') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { submitFeedback } from '@/api/modules/user'
|
||||
import type { IFeedbackForm } from '@/types/user'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 问题类型选项
|
||||
const issueTypeOptions = computed(() => [
|
||||
{ label: t('user.issueTypeAccount'), value: '1' },
|
||||
{ label: t('user.issueTypePayment'), value: '2' },
|
||||
{ label: t('user.issueTypeOrder'), value: '3' },
|
||||
{ label: t('user.issueTypeContent'), value: '4' },
|
||||
{ label: t('user.issueTypeSuggestion'), value: '5' },
|
||||
{ label: t('user.issueTypeBug'), value: '6' },
|
||||
{ label: t('user.issueTypeOther'), value: '7' }
|
||||
])
|
||||
|
||||
// 表单数据
|
||||
const form = ref<IFeedbackForm>({
|
||||
type: '',
|
||||
account: userStore.userInfo.email || userStore.userInfo.phone || '',
|
||||
relation: '',
|
||||
content: '',
|
||||
contactInformation: '',
|
||||
image: ''
|
||||
})
|
||||
|
||||
// 图片列表
|
||||
const imageList = ref<any[]>([])
|
||||
|
||||
// 错误状态
|
||||
const relationError = ref(false)
|
||||
|
||||
/**
|
||||
* 处理订单编号输入
|
||||
*/
|
||||
const handleRelationInput = () => {
|
||||
relationError.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传前检查
|
||||
*/
|
||||
const beforeUpload = async (file: any) => {
|
||||
// TODO: 检查相机和存储权限
|
||||
|
||||
// 检查文件大小
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
uni.showToast({
|
||||
title: '图片大小不能超过5MB',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件变化
|
||||
*/
|
||||
const imageUrl = ref<any[]>([])
|
||||
const handleChangeImage = ({ fileList }: any) => {
|
||||
console.log('上传文件变化:', fileList)
|
||||
imageUrl.value = []
|
||||
fileList.forEach((file:any) => {
|
||||
imageUrl.value.push(JSON.parse(file.response).url)
|
||||
})
|
||||
console.log('图片列表:', imageUrl.value)
|
||||
// this.fileList = fileList
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单验证
|
||||
*/
|
||||
const validateForm = (): boolean => {
|
||||
// 验证问题类型
|
||||
if (!form.value.type) {
|
||||
uni.showToast({
|
||||
title: t('common.pleaseSelect') + t('user.issueType'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证账号
|
||||
if (!form.value.account) {
|
||||
uni.showToast({
|
||||
title: t('user.pleaseInput') + t('user.account'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证订单编号(当类型为订单相关时)
|
||||
if (form.value.type === '3') {
|
||||
if (!form.value.relation) {
|
||||
relationError.value = true
|
||||
uni.showToast({
|
||||
title: t('user.pleaseInputOrderSn'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 验证问题描述
|
||||
if (!form.value.content) {
|
||||
uni.showToast({
|
||||
title: t('user.pleaseInputDescription'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证联系电话
|
||||
if (!form.value.contactInformation) {
|
||||
uni.showToast({
|
||||
title: t('user.pleaseInputPhone'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交反馈
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
// 表单验证
|
||||
if (!validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading()
|
||||
|
||||
// 处理图片
|
||||
if (imageUrl.value.length > 0) {
|
||||
form.value.image = imageUrl.value.join(',')
|
||||
}
|
||||
|
||||
// 提交反馈
|
||||
await submitFeedback(form.value)
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
uni.showModal({
|
||||
title: t('global.tips'),
|
||||
content: t('user.feedbackSuccess'),
|
||||
showCancel: false,
|
||||
confirmText: t('common.confirm'),
|
||||
success: () => {
|
||||
// 清空表单
|
||||
imageList.value = []
|
||||
imageUrl.value = []
|
||||
form.value = {
|
||||
type: '',
|
||||
account: userStore.userInfo.email || userStore.userInfo.phone || '',
|
||||
relation: '',
|
||||
content: '',
|
||||
contactInformation: '',
|
||||
image: ''
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
uni.navigateBack({
|
||||
delta: 1
|
||||
})
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('提交反馈失败:', error)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: t('user.feedbackFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$theme-color: #54a966;
|
||||
|
||||
.feedback-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f7faf9;
|
||||
}
|
||||
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 44px;
|
||||
position: relative;
|
||||
|
||||
.navbar-left {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-content {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.feedback-form {
|
||||
background: #fff;
|
||||
border-radius: 15rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15rpx;
|
||||
|
||||
&.required::before {
|
||||
content: '*';
|
||||
color: red;
|
||||
margin-right: 5rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.error-text {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: red;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
margin-top: 50rpx;
|
||||
}
|
||||
</style>
|
||||
334
pages/user/index.vue
Normal file
334
pages/user/index.vue
Normal file
@@ -0,0 +1,334 @@
|
||||
<template>
|
||||
<view class="user-page">
|
||||
<!-- 设置图标 -->
|
||||
<view class="settings-icon" @click="goSettings">
|
||||
<wd-icon name="setting1" size="24px" color="#666" />
|
||||
<text>{{ $t('user.settings') }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 用户信息区域 -->
|
||||
<view class="user-info-section">
|
||||
<view class="user-info">
|
||||
<image
|
||||
:src="userInfo.avatar || defaultAvatar"
|
||||
class="avatar"
|
||||
@click="goProfile"
|
||||
/>
|
||||
<view class="user-details">
|
||||
<text class="nickname">{{ userInfo.nickname || $t('user.notSet') }}</text>
|
||||
<text v-if="userInfo.email" class="email">{{ userInfo.email }}</text>
|
||||
<text v-if="vipInfo.endTime && platform === 'ios'" class="vip-time">
|
||||
VIP {{ vipInfo.endTime.split(' ')[0] }} {{ $t('user.vipExpireTime') }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- VIP订阅卡片 -->
|
||||
<view class="vip-card-section">
|
||||
<view class="vip-card">
|
||||
<view v-if="vipInfo.id" class="vip-info">
|
||||
<text class="label">{{ $t('user.vip') }}</text>
|
||||
<text class="value">{{ vipInfo.endTime ? vipInfo.endTime.split(' ')[0] : '' }}</text>
|
||||
</view>
|
||||
<wd-button
|
||||
v-else
|
||||
type="success"
|
||||
size="small"
|
||||
@click="goSubscribe"
|
||||
>
|
||||
{{ $t('user.subscribe') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能菜单列表 -->
|
||||
<view class="menu-section">
|
||||
<view class="menu-list">
|
||||
<view
|
||||
v-for="item in menuItems"
|
||||
:key="item.id"
|
||||
class="menu-item"
|
||||
@click="handleMenuClick(item)"
|
||||
>
|
||||
<text class="menu-text">{{ item.name }}</text>
|
||||
<wd-icon name="arrow-right" size="16px" color="#aaa" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getUserInfo, getVipInfo } from '@/api/modules/user'
|
||||
import type { IVipInfo } from '@/types/user'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 默认头像
|
||||
const defaultAvatar = '/static/home_icon.png'
|
||||
|
||||
// 用户信息
|
||||
const userInfo = computed(() => userStore.userInfo)
|
||||
|
||||
// VIP信息
|
||||
const vipInfo = ref<IVipInfo>({
|
||||
id: 0,
|
||||
endTime: '',
|
||||
vipType: 0
|
||||
})
|
||||
|
||||
// 平台信息
|
||||
const platform = ref('')
|
||||
|
||||
// 菜单项
|
||||
const menuItems = computed(() => [
|
||||
{
|
||||
id: 1,
|
||||
name: t('user.myOrders'),
|
||||
url: '/pages/user/order/index',
|
||||
type: 'pageJump'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: t('user.myBooklist'),
|
||||
url: '/pages/book/index',
|
||||
type: 'switchTab'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: t('user.profile'),
|
||||
url: '/pages/user/profile/index',
|
||||
type: 'pageJump'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: t('user.about'),
|
||||
url: '/pages/user/about/index',
|
||||
type: 'pageJump'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: t('user.feedback'),
|
||||
url: '/pages/user/feedback/index',
|
||||
type: 'pageJump'
|
||||
}
|
||||
])
|
||||
|
||||
/**
|
||||
* 获取平台信息
|
||||
*/
|
||||
const getPlatform = () => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
platform.value = systemInfo.platform === 'android' ? 'android' : 'ios'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据
|
||||
*/
|
||||
const getData = async () => {
|
||||
try {
|
||||
// 获取用户信息
|
||||
const userRes = await getUserInfo()
|
||||
if (userRes.user) {
|
||||
userStore.setUserInfo(userRes.user)
|
||||
}
|
||||
|
||||
// 获取VIP信息
|
||||
const vipRes = await getVipInfo()
|
||||
if (vipRes.vipInfo) {
|
||||
vipInfo.value = vipRes.vipInfo
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到设置页面
|
||||
*/
|
||||
const goSettings = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/settings/index'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到个人资料页面
|
||||
*/
|
||||
const goProfile = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/profile/index'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到订阅页面
|
||||
*/
|
||||
const goSubscribe = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/wallet/index'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理菜单点击
|
||||
*/
|
||||
const handleMenuClick = (item: any) => {
|
||||
switch (item.type) {
|
||||
case 'pageJump':
|
||||
uni.navigateTo({
|
||||
url: item.url
|
||||
})
|
||||
break
|
||||
case 'switchTab':
|
||||
uni.switchTab({
|
||||
url: item.url
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getPlatform()
|
||||
getData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$theme-color: #54a966;
|
||||
|
||||
.user-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f7faf9;
|
||||
padding-top: 130rpx;
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
position: absolute;
|
||||
right: 40rpx;
|
||||
top: 60rpx;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
line-height: 1.2;
|
||||
|
||||
text {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info-section {
|
||||
padding: 0 30rpx;
|
||||
margin-bottom: 50rpx;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.avatar {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 100rpx;
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
flex: 1;
|
||||
|
||||
.nickname {
|
||||
display: block;
|
||||
font-size: 38rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.email {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.vip-time {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vip-card-section {
|
||||
padding: 0 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.vip-card {
|
||||
background: #fff;
|
||||
border-radius: 15rpx;
|
||||
padding: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.vip-info {
|
||||
text-align: center;
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
color: $theme-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-section {
|
||||
padding: 20rpx 20rpx 0;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
background: #fff;
|
||||
border-radius: 15rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #e0e0e0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
309
pages/user/order/index.vue
Normal file
309
pages/user/order/index.vue
Normal file
@@ -0,0 +1,309 @@
|
||||
<template>
|
||||
<view class="order-page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="custom-navbar" :style="{ height: navbarHeight }">
|
||||
<view class="navbar-content" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="navbar-left" @click="goBack">
|
||||
<wd-icon name="arrow-left" size="18px" color="#333" />
|
||||
</view>
|
||||
<text class="navbar-title">{{ $t('user.myOrders') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<scroll-view
|
||||
v-if="orderList.length > 0"
|
||||
scroll-y
|
||||
class="order-scroll"
|
||||
:style="{ paddingTop: navbarHeight }"
|
||||
@scrolltolower="loadMore"
|
||||
>
|
||||
<view class="order-list">
|
||||
<view
|
||||
v-for="order in orderList"
|
||||
:key="order.id"
|
||||
class="order-item"
|
||||
>
|
||||
<view class="order-header">
|
||||
<text class="book-name">{{ order.bookEntity.name }}</text>
|
||||
<view class="price-info">
|
||||
<image
|
||||
v-if="order.paymentMethod === '4'"
|
||||
src="/static/icon/coin.png"
|
||||
class="payment-icon"
|
||||
/>
|
||||
<image
|
||||
v-else
|
||||
src="/static/icon/currency.png"
|
||||
class="payment-icon"
|
||||
/>
|
||||
<text class="price">{{ order.orderMoney }}</text>
|
||||
<text v-if="order.paymentMethod === '5'" class="currency">NZD</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="order-sn">
|
||||
{{ $t('user.orderSn') }}
|
||||
{{ order.orderSn }}
|
||||
</text>
|
||||
<text class="create-time">{{ order.createTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载提示 -->
|
||||
<view class="load-tips">
|
||||
<wd-divider v-if="hasMore && showLoadTip">
|
||||
{{ $t('common.loadMore') }}
|
||||
</wd-divider>
|
||||
<wd-divider v-if="!hasMore && showLoadTip">
|
||||
{{ $t('common.noMore') }}
|
||||
</wd-divider>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else-if="!loading" class="empty-state">
|
||||
<text class="empty-text">{{ $t('common.noData') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getOrderList } from '@/api/modules/user'
|
||||
import type { IOrder } from '@/types/user'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 导航栏高度
|
||||
const statusBarHeight = ref(0)
|
||||
const navbarHeight = ref('44px')
|
||||
|
||||
// 订单列表
|
||||
const orderList = ref<IOrder[]>([])
|
||||
|
||||
// 分页信息
|
||||
const page = ref({
|
||||
current: 1,
|
||||
limit: 10
|
||||
})
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const hasMore = ref(true)
|
||||
const showLoadTip = ref(false)
|
||||
const total = ref(0)
|
||||
|
||||
/**
|
||||
* 获取导航栏高度
|
||||
*/
|
||||
const getNavbarHeight = () => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = systemInfo.statusBarHeight || 0
|
||||
|
||||
let navBarHeight = 44
|
||||
if (systemInfo.model.indexOf('iPhone') !== -1 && parseInt(systemInfo.model.slice(-2)) >= 11) {
|
||||
navBarHeight = 48
|
||||
}
|
||||
|
||||
const totalHeight = statusBarHeight.value + navBarHeight
|
||||
navbarHeight.value = totalHeight + 'px'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单列表
|
||||
*/
|
||||
const getData = async () => {
|
||||
if (loading.value || !hasMore.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
uni.showLoading({
|
||||
title: t('common.loading')
|
||||
})
|
||||
|
||||
const res = await getOrderList(page.value.current, page.value.limit)
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
if (res.orders && res.orders.records) {
|
||||
total.value = res.orders.total
|
||||
const records = res.orders.records
|
||||
|
||||
if (records.length > 0) {
|
||||
orderList.value = [...orderList.value, ...records]
|
||||
page.value.current += 1
|
||||
showLoadTip.value = true
|
||||
|
||||
// 判断是否还有更多数据
|
||||
if (orderList.value.length >= total.value || records.length < page.value.limit) {
|
||||
hasMore.value = false
|
||||
}
|
||||
} else {
|
||||
hasMore.value = false
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取订单列表失败:', error)
|
||||
uni.hideLoading()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载更多
|
||||
*/
|
||||
const loadMore = () => {
|
||||
if (!loading.value && hasMore.value) {
|
||||
getData()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
*/
|
||||
const goBack = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getNavbarHeight()
|
||||
getData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$theme-color: #54a966;
|
||||
|
||||
.order-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f7faf9;
|
||||
}
|
||||
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 44px;
|
||||
position: relative;
|
||||
|
||||
.navbar-left {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.order-scroll {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.order-list {
|
||||
padding: 0 20rpx 10rpx;
|
||||
}
|
||||
|
||||
.order-item {
|
||||
background: #fff;
|
||||
border-radius: 15rpx;
|
||||
padding: 30rpx;
|
||||
margin-top: 20rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.order-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.book-name {
|
||||
flex: 1;
|
||||
font-size: 34rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
max-width: 85%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.price-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.payment-icon {
|
||||
width: 38rpx;
|
||||
height: 38rpx;
|
||||
margin-right: 5rpx;
|
||||
}
|
||||
|
||||
.price {
|
||||
font-size: 36rpx;
|
||||
color: $theme-color;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.currency {
|
||||
font-size: 32rpx;
|
||||
color: $theme-color;
|
||||
font-weight: bold;
|
||||
margin-left: 5rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.order-sn {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
color: #666;
|
||||
line-height: 38rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.create-time {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
.load-tips {
|
||||
padding: 40rpx 0 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
padding-top: 100rpx;
|
||||
|
||||
.empty-text {
|
||||
font-size: 30rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
645
pages/user/profile/index.vue
Normal file
645
pages/user/profile/index.vue
Normal file
@@ -0,0 +1,645 @@
|
||||
<template>
|
||||
<view class="profile-page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<nav-bar :title="$t('user.profile')"></nav-bar>
|
||||
|
||||
<view class="profile-content">
|
||||
<!-- 头像区域 -->
|
||||
<view class="avatar-section">
|
||||
<image
|
||||
:src="userInfo.avatar || defaultAvatar"
|
||||
class="avatar"
|
||||
@click="editAvatar"
|
||||
/>
|
||||
<text class="avatar-text" @click="editAvatar">
|
||||
{{ userInfo.avatar ? $t('user.changeAvatar') : $t('user.setAvatar') }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 信息列表 -->
|
||||
<wd-cell-group border custom-class="info-list">
|
||||
<wd-cell
|
||||
v-for="field in fields"
|
||||
title-width="100rpx"
|
||||
:key="field.key"
|
||||
:title="field.label"
|
||||
clickable
|
||||
@click="editField(field)"
|
||||
>
|
||||
<view class="value-wrapper">
|
||||
<text v-if="userInfo[field.key]" class="value">
|
||||
{{ formatValue(field, userInfo[field.key]) }}
|
||||
</text>
|
||||
<text v-else class="placeholder">{{ $t('user.notSet') }}</text>
|
||||
<wd-icon
|
||||
v-if="userInfo[field.key]"
|
||||
name="edit-1"
|
||||
size="16px"
|
||||
color="#54a966"
|
||||
/>
|
||||
</view>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<wd-popup v-model="showEditModal" position="bottom">
|
||||
<view class="edit-modal">
|
||||
<text class="modal-title">{{ editTitle }}</text>
|
||||
|
||||
<!-- 昵称/姓名/年龄编辑 -->
|
||||
<view v-if="['nickname', 'name', 'age'].includes(currentField?.key || '')">
|
||||
<wd-input
|
||||
v-model="editValue"
|
||||
:placeholder="getPlaceholder(currentField?.key)"
|
||||
:type="currentField?.key === 'age' ? 'number' : 'text'"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 邮箱编辑 -->
|
||||
<view v-if="currentField?.key === 'email'">
|
||||
<wd-input
|
||||
v-model="editForm.email"
|
||||
:placeholder="$t('user.email')"
|
||||
type="email"
|
||||
/>
|
||||
<view class="code-input">
|
||||
<wd-input
|
||||
v-model="editForm.code"
|
||||
:placeholder="$t('login.codePlaceholder')"
|
||||
type="number"
|
||||
class="flex-1"
|
||||
/>
|
||||
<wd-button
|
||||
size="small"
|
||||
:disabled="codeDisabled"
|
||||
class="w-[100px]"
|
||||
@click="sendCode"
|
||||
>
|
||||
{{ codeText }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 密码编辑 -->
|
||||
<view v-if="currentField?.key === 'password'">
|
||||
<wd-input
|
||||
v-model="editForm.password"
|
||||
:placeholder="$t('user.pleaseInputPassword')"
|
||||
type="password"
|
||||
@input="checkPasswordStrength"
|
||||
/>
|
||||
<view class="password-note">
|
||||
<view v-if="passwordNote">{{ passwordNote }}</view>
|
||||
<view v-html="passwordStrength"></view>
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="editForm.confirmPassword"
|
||||
:placeholder="$t('user.pleaseInputPasswordAgain')"
|
||||
type="password"
|
||||
style="margin-top: 20rpx"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 性别编辑 -->
|
||||
<view v-if="currentField?.key === 'sex'">
|
||||
<wd-radio-group v-model="editValue" shape="button" class="text-center">
|
||||
<wd-radio v-for="item in sexOptions" :key="item.value" :value="item.value">{{ item.label }}</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
|
||||
<!-- 头像上传 -->
|
||||
<view v-if="currentField?.key === 'avatar'">
|
||||
<wd-upload
|
||||
:file-list="avatarList"
|
||||
:limit="1"
|
||||
action="https://global.nuttyreading.com/oss/fileoss"
|
||||
@change="handleChangeAvatar"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 确认按钮 -->
|
||||
<wd-button type="success" block :round="false" @click="handleSubmit" style="margin-top: 50rpx">
|
||||
{{ $t('common.confirm') }}
|
||||
</wd-button>
|
||||
<view class="cancel-btn" @click="closeModal">
|
||||
{{ $t('common.cancel') }}
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import {
|
||||
getUserInfo,
|
||||
updateUserInfo,
|
||||
updateEmail,
|
||||
updatePassword,
|
||||
uploadImage,
|
||||
sendEmailCode
|
||||
} from '@/api/modules/user'
|
||||
import { checkPasswordStrength as validatePassword } from '@/utils/validator'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 默认头像
|
||||
const defaultAvatar = '/static/home_icon.png'
|
||||
|
||||
// 字段列表
|
||||
const fields = computed(() => [
|
||||
{ key: 'nickname', label: t('user.nickname') },
|
||||
{ key: 'email', label: t('user.email') },
|
||||
{ key: 'password', label: t('login.password') },
|
||||
{ key: 'age', label: t('user.age') },
|
||||
{ key: 'sex', label: t('user.sex') },
|
||||
{ key: 'name', label: t('user.name') }
|
||||
])
|
||||
|
||||
// 性别选项
|
||||
const sexOptions = [
|
||||
{ label: t('user.male'), value: 1 },
|
||||
{ label: t('user.female'), value: 2 }
|
||||
]
|
||||
|
||||
// 编辑相关
|
||||
const showEditModal = ref(false)
|
||||
const currentField = ref<any>(null)
|
||||
const editTitle = ref('')
|
||||
const editValue = ref<any>('')
|
||||
const editForm = ref({
|
||||
email: '',
|
||||
code: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
// 验证码相关
|
||||
const codeDisabled = ref(false)
|
||||
const codeText = ref(t('login.getCode'))
|
||||
let codeTimer: any = null
|
||||
|
||||
// 密码强度
|
||||
const passwordNote = ref('')
|
||||
const passwordStrength = ref('')
|
||||
const passwordOk = ref(false)
|
||||
|
||||
// 头像上传
|
||||
const avatarList = ref<any[]>([])
|
||||
const avatarUrl = ref('')
|
||||
|
||||
|
||||
/**
|
||||
* 获取用户数据
|
||||
*/
|
||||
const userInfo = ref<any>({}) // 用户信息
|
||||
const getData = async () => {
|
||||
uni.showLoading()
|
||||
try {
|
||||
const res = await getUserInfo()
|
||||
uni.hideLoading()
|
||||
if (res.result) {
|
||||
userStore.setUserInfo(res.result)
|
||||
userInfo.value = res.result
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化显示值
|
||||
*/
|
||||
const formatValue = (field: any, value: any) => {
|
||||
if (field.key === 'sex') {
|
||||
return value === 1 ? t('user.male') : t('user.female')
|
||||
}
|
||||
if (field.key === 'password') {
|
||||
return '******'
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取输入框占位符
|
||||
*/
|
||||
const getPlaceholder = (key?: string) => {
|
||||
const placeholders: Record<string, string> = {
|
||||
nickname: t('common.pleaseInput') + t('user.nickname'),
|
||||
name: t('common.pleaseInput') + t('user.name'),
|
||||
age: t('common.pleaseInput') + t('user.age')
|
||||
}
|
||||
return placeholders[key || ''] || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑字段
|
||||
*/
|
||||
const editField = (field: any) => {
|
||||
currentField.value = field
|
||||
editTitle.value = (userInfo.value[field.key] ? t('common.edit') : t('user.clickToSet')) + field.label
|
||||
|
||||
// 重置表单
|
||||
editValue.value = ''
|
||||
editForm.value = {
|
||||
email: '',
|
||||
code: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
}
|
||||
passwordNote.value = ''
|
||||
passwordStrength.value = ''
|
||||
|
||||
// 设置初始值
|
||||
if (field.key === 'sex' && userInfo.value.sex) {
|
||||
editValue.value = userInfo.value.sex
|
||||
}
|
||||
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑头像
|
||||
*/
|
||||
const editAvatar = () => {
|
||||
currentField.value = { key: 'avatar', label: t('user.avatar') }
|
||||
editTitle.value = userInfo.value.avatar ? t('user.changeAvatar') : t('user.setAvatar')
|
||||
avatarList.value = userInfo.value.avatar ? [{ url: userInfo.value.avatar }] : []
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传前检查
|
||||
*/
|
||||
const beforeUpload = async () => {
|
||||
// TODO: 检查相机和存储权限
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理头像上传变化
|
||||
*/
|
||||
const handleChangeAvatar = async(filelist: any) => {
|
||||
if (filelist.fileList.length > 0) {
|
||||
avatarUrl.value = JSON.parse(filelist.fileList[0].response).url
|
||||
} else {
|
||||
avatarUrl.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除头像
|
||||
*/
|
||||
const handleDeleteAvatar = () => {
|
||||
avatarList.value = []
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
*/
|
||||
const sendCode = async () => {
|
||||
if (!editForm.value.email) {
|
||||
uni.showToast({
|
||||
title: t('user.pleaseInput') + t('user.email'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await sendEmailCode(editForm.value.email)
|
||||
uni.showToast({
|
||||
title: t('user.sendCodeSuccess'),
|
||||
icon: 'none'
|
||||
})
|
||||
startCountdown()
|
||||
} catch (error) {
|
||||
console.error('发送验证码失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码倒计时
|
||||
*/
|
||||
const startCountdown = () => {
|
||||
if (codeTimer) {
|
||||
clearInterval(codeTimer)
|
||||
}
|
||||
|
||||
codeDisabled.value = true
|
||||
let countdown = 60
|
||||
codeText.value = `${countdown}S`
|
||||
|
||||
codeTimer = setInterval(() => {
|
||||
countdown--
|
||||
codeText.value = `${countdown}S`
|
||||
if (countdown <= 0) {
|
||||
clearInterval(codeTimer)
|
||||
codeText.value = t('login.getCode')
|
||||
codeDisabled.value = false
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查密码强度
|
||||
*/
|
||||
const checkPasswordStrength = () => {
|
||||
const password = editForm.value.password
|
||||
if (!password) {
|
||||
passwordNote.value = ''
|
||||
passwordStrength.value = ''
|
||||
passwordOk.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const strength = validatePassword(password)
|
||||
|
||||
if (strength === 'strong') {
|
||||
passwordStrength.value = `<span style="color:#18bc37">${t('forget.passwordStrengthStrong')}</span>`
|
||||
passwordNote.value = ''
|
||||
passwordOk.value = true
|
||||
} else if (strength === 'medium') {
|
||||
passwordStrength.value = `<span style="color:#2979ff">${t('forget.passwordStrengthMedium')}</span>`
|
||||
passwordNote.value = t('forget.passwordStrengthWeak')
|
||||
passwordOk.value = true
|
||||
} else if (strength === 'weak') {
|
||||
passwordStrength.value = `<span style="color:#f3a73f">弱</span>`
|
||||
passwordNote.value = t('forget.passwordStrengthWeak')
|
||||
passwordOk.value = false
|
||||
} else {
|
||||
passwordStrength.value = ''
|
||||
passwordNote.value = t('forget.passwordStrengthWeak')
|
||||
passwordOk.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交修改
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
const key = currentField.value?.key
|
||||
|
||||
try {
|
||||
uni.showLoading()
|
||||
|
||||
// 构建更新数据对象
|
||||
let updateData: any = Object.assign({}, userInfo.value)
|
||||
|
||||
switch (key) {
|
||||
case 'email':
|
||||
// 更新邮箱
|
||||
if (!editForm.value.email || !editForm.value.code) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: t('user.pleaseInputCode'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
await updateEmail(userInfo.value.id, editForm.value.email, editForm.value.code)
|
||||
break
|
||||
|
||||
case 'password':
|
||||
// 更新密码
|
||||
if (!passwordOk.value) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: passwordNote.value,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (editForm.value.password !== editForm.value.confirmPassword) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: t('user.passwordNotMatch'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
await updatePassword(userInfo.value.id, editForm.value.password)
|
||||
break
|
||||
|
||||
case 'avatar':
|
||||
// 更新头像
|
||||
console.log('avatarUrl.value:', avatarUrl.value)
|
||||
if (!avatarUrl.value) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: t('common.pleaseSelect') + t('user.avatar'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是新上传的图片,需要先上传
|
||||
updateData.avatar = avatarUrl.value
|
||||
await updateUserInfo(updateData)
|
||||
break
|
||||
|
||||
case 'sex':
|
||||
// 更新性别
|
||||
const sexValue = editValue.value === 2 ? 0 : editValue.value
|
||||
updateData.sex = sexValue
|
||||
await updateUserInfo(updateData)
|
||||
break
|
||||
|
||||
default:
|
||||
// 更新其他字段
|
||||
if (!editValue.value) {
|
||||
uni.showToast({
|
||||
title: getPlaceholder(key),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
updateData[key] = editValue.value
|
||||
await updateUserInfo(updateData)
|
||||
break
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: t('user.updateSuccess'),
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
closeModal()
|
||||
|
||||
// 刷新数据
|
||||
setTimeout(() => {
|
||||
getData()
|
||||
}, 500)
|
||||
} catch (error) {
|
||||
console.error('更新失败:', error)
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
*/
|
||||
const closeModal = () => {
|
||||
showEditModal.value = false
|
||||
currentField.value = null
|
||||
if (codeTimer) {
|
||||
clearInterval(codeTimer)
|
||||
codeDisabled.value = false
|
||||
codeText.value = t('login.getCode')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$theme-color: #54a966;
|
||||
|
||||
.profile-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f7faf9;
|
||||
}
|
||||
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 44px;
|
||||
position: relative;
|
||||
|
||||
.navbar-left {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profile-content {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.avatar-section {
|
||||
background: #fff;
|
||||
border-radius: 15rpx;
|
||||
padding: 40rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.avatar {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
border-radius: 180rpx;
|
||||
background-color: #f5f5f5;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 30rpx;
|
||||
color: $theme-color;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.info-list {
|
||||
background: #fff;
|
||||
border-radius: 15rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.value-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.value {
|
||||
color: #666;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #999;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-modal {
|
||||
padding: 60rpx 50rpx 80rpx;
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
margin-bottom: 50rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.code-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.password-note {
|
||||
margin-top: 10rpx;
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
|
||||
.radio-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #ededed;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
text {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #888;
|
||||
margin-top: 25rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
340
pages/user/settings/index.vue
Normal file
340
pages/user/settings/index.vue
Normal file
@@ -0,0 +1,340 @@
|
||||
<template>
|
||||
<view class="settings-page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<nav-bar :title="$t('user.settings')"></nav-bar>
|
||||
|
||||
<view class="settings-content">
|
||||
<!-- 设置列表 -->
|
||||
<view class="settings-list">
|
||||
<view
|
||||
v-for="item in settingItems"
|
||||
:key="item.id"
|
||||
class="setting-item"
|
||||
@click="handleSettingClick(item)"
|
||||
>
|
||||
<text class="label">{{ item.label }}</text>
|
||||
<view class="value-wrapper">
|
||||
<text class="value">{{ item.value }}</text>
|
||||
<wd-icon name="arrow-right" size="16px" color="#aaa" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-buttons">
|
||||
<wd-button type="info" block :round="false" custom-class="logout-btn" @click="handleLogoff">{{ $t('user.logoff') }}</wd-button>
|
||||
<wd-button type="error" block :round="false" @click="handleLogout">
|
||||
{{ $t('user.logout') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 微信二维码弹窗 -->
|
||||
<wd-popup v-model="showQrCode" position="bottom" :closeable="true">
|
||||
<view class="qrcode-modal">
|
||||
<text class="modal-title">{{ $t('user.wechatTip') }}</text>
|
||||
<image
|
||||
src="/static/qiyeWx.jpg"
|
||||
mode="aspectFit"
|
||||
class="qrcode-image"
|
||||
@click="previewQrCode"
|
||||
/>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<wd-message-box />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMessage } from '@/uni_modules/wot-design-uni'
|
||||
import { makePhoneCall, copyToClipboard } from '@/utils/index'
|
||||
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 导航栏高度
|
||||
const statusBarHeight = ref(0)
|
||||
const navbarHeight = ref('44px')
|
||||
|
||||
// 设置项列表
|
||||
const settingItems = computed(() => [
|
||||
{
|
||||
id: 1,
|
||||
label: t('user.hotline'),
|
||||
value: '022-24142321',
|
||||
type: 'tel'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: t('user.customerEmail'),
|
||||
value: 'appyilujiankang@sina.com',
|
||||
type: 'email'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: t('user.wechat'),
|
||||
value: '',
|
||||
type: 'wechat'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: t('user.checkVersion'),
|
||||
value: '',
|
||||
type: 'version'
|
||||
}
|
||||
])
|
||||
|
||||
// 弹窗状态
|
||||
const showQrCode = ref(false)
|
||||
|
||||
/**
|
||||
* 获取导航栏高度
|
||||
*/
|
||||
const getNavbarHeight = () => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = systemInfo.statusBarHeight || 0
|
||||
|
||||
let navBarHeight = 44
|
||||
if (systemInfo.model.indexOf('iPhone') !== -1 && parseInt(systemInfo.model.slice(-2)) >= 11) {
|
||||
navBarHeight = 48
|
||||
}
|
||||
|
||||
const totalHeight = statusBarHeight.value + navBarHeight
|
||||
navbarHeight.value = totalHeight + 'px'
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设置项点击
|
||||
*/
|
||||
const handleSettingClick = (item: any) => {
|
||||
switch (item.type) {
|
||||
case 'tel':
|
||||
handlePhoneCall(item.value, item.label)
|
||||
break
|
||||
case 'email':
|
||||
handleCopyEmail(item.value, item.label)
|
||||
break
|
||||
case 'wechat':
|
||||
showQrCode.value = true
|
||||
break
|
||||
case 'version':
|
||||
checkVersion()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拨打电话
|
||||
*/
|
||||
const handlePhoneCall = (phoneNumber: string, title: string) => {
|
||||
makePhoneCall(phoneNumber, title, t)
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制到剪贴板
|
||||
*/
|
||||
const handleCopyEmail = (content: string, title: string) => {
|
||||
copyToClipboard(content, title, t)
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览二维码
|
||||
*/
|
||||
const previewQrCode = () => {
|
||||
showQrCode.value = false
|
||||
uni.previewImage({
|
||||
urls: ['/static/qiyeWx.jpg']
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查版本更新
|
||||
*/
|
||||
const checkVersion = () => {
|
||||
// #ifdef APP-PLUS
|
||||
// TODO: 集成 uni-upgrade-center-app 插件
|
||||
uni.showToast({
|
||||
title: '当前已是最新版本',
|
||||
icon: 'none'
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-PLUS
|
||||
uni.showToast({
|
||||
title: '仅支持APP端检查更新',
|
||||
icon: 'none'
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销账户
|
||||
*/
|
||||
const handleLogoff = () => {
|
||||
message.confirm({
|
||||
title: t('global.tips'),
|
||||
msg: t('user.logoffConfirm'),
|
||||
}).then(() => {
|
||||
message.confirm({
|
||||
title: t('global.tips'),
|
||||
msg: t('user.logoffConfirmAgain'),
|
||||
}).then(() => {
|
||||
performLogout()
|
||||
}).catch(() => {
|
||||
// 取消注销账户
|
||||
})
|
||||
}).catch(() => {
|
||||
// 取消注销账户
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
const handleLogout = () => {
|
||||
message.confirm({
|
||||
title: t('global.tips'),
|
||||
msg: t('user.logoutConfirm'),
|
||||
}).then(() => {
|
||||
performLogout()
|
||||
}).catch(() => {
|
||||
// 取消退出登录
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行退出登录
|
||||
*/
|
||||
const performLogout = () => {
|
||||
// 清除用户信息
|
||||
userStore.logout()
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getNavbarHeight()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$theme-color: #54a966;
|
||||
|
||||
.settings-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f7faf9;
|
||||
}
|
||||
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 44px;
|
||||
position: relative;
|
||||
|
||||
.navbar-left {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.settings-list {
|
||||
background: #fff;
|
||||
border-radius: 15rpx;
|
||||
overflow: hidden;
|
||||
margin-bottom: 40rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #e0e0e0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.value-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
color: #b0b0b0;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
padding: 40rpx 30rpx 60rpx;
|
||||
|
||||
:deep(.logout-btn) {
|
||||
margin-bottom: 40rpx;
|
||||
background-color: #e5e5e5;
|
||||
color: #8a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
.qrcode-modal {
|
||||
padding: 40rpx;
|
||||
text-align: center;
|
||||
|
||||
.modal-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.qrcode-image {
|
||||
width: 400rpx;
|
||||
height: 400rpx;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
523
pages/user/wallet/index.vue
Normal file
523
pages/user/wallet/index.vue
Normal file
@@ -0,0 +1,523 @@
|
||||
<template>
|
||||
<view class="wallet-page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<nav-bar :title="$t('user.subscribe')"></nav-bar>
|
||||
|
||||
<view class="wallet-content">
|
||||
<!-- VIP套餐列表 -->
|
||||
<view class="package-section">
|
||||
<text class="section-title">{{ $t('user.subscribe') }}</text>
|
||||
<view class="package-list">
|
||||
<view
|
||||
v-for="(pkg, index) in packages"
|
||||
:key="pkg.id"
|
||||
:class="['package-item', { active: selectedIndex === index }]"
|
||||
@click="selectPackage(index)"
|
||||
>
|
||||
<view class="package-price">
|
||||
<image src="/static/icon/currency.png" class="currency-icon" />
|
||||
<text class="price">{{ pkg.dictType }}</text>
|
||||
<text class="unit">NZD</text>
|
||||
</view>
|
||||
<text class="package-duration">{{ getDuration(pkg.remark) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 支付方式 -->
|
||||
<view class="payment-section">
|
||||
<text class="section-title">{{ $t('user.paymentMethod') }}</text>
|
||||
<radio-group class="payment-group">
|
||||
<view class="payment-item">
|
||||
<radio :checked="true" color="#54a966" />
|
||||
<text>{{ paymentName }}</text>
|
||||
</view>
|
||||
</radio-group>
|
||||
</view>
|
||||
|
||||
<!-- 协议勾选 -->
|
||||
<view class="agreement-section">
|
||||
<view class="agreement-checkbox" @click="toggleAgreement">
|
||||
<view :class="['checkbox', { checked: agreed }]">
|
||||
<wd-icon v-if="agreed" name="check" size="16px" color="#fff" />
|
||||
</view>
|
||||
<text class="agreement-text">
|
||||
{{ $t('user.agreeText') }}
|
||||
<text class="link" @click.stop="showAgreementModal">{{ $t('user.agreement') }}</text>
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订阅按钮 -->
|
||||
<view class="subscribe-button">
|
||||
<wd-button
|
||||
type="success"
|
||||
block
|
||||
@click="handleSubscribe"
|
||||
>
|
||||
{{ $t('user.subscribe') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 协议弹窗 -->
|
||||
<wd-popup v-model="agreementShow" position="bottom" :closeable="true">
|
||||
<view class="agreement-modal">
|
||||
<view class="modal-title">{{ agreementData.title }}</view>
|
||||
<scroll-view scroll-y class="modal-content">
|
||||
<view v-html="agreementData.content"></view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getVipPackages, createOrder, verifyGooglePay, verifyIAP } from '@/api/modules/user'
|
||||
import { commonApi } from '@/api/modules/common'
|
||||
import type { IVipPackage } from '@/types/user'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 导航栏高度
|
||||
const statusBarHeight = ref(0)
|
||||
const navbarHeight = ref('44px')
|
||||
|
||||
// VIP套餐列表
|
||||
const packages = ref<IVipPackage[]>([])
|
||||
const selectedIndex = ref<number | null>(null)
|
||||
const selectedPackage = computed(() =>
|
||||
selectedIndex.value !== null ? packages.value[selectedIndex.value] : null
|
||||
)
|
||||
|
||||
// 平台和支付方式
|
||||
const platform = ref('')
|
||||
const paymentMethod = ref(5) // 3-iOS IAP, 5-Google Pay
|
||||
const paymentName = computed(() =>
|
||||
platform.value === 'android' ? 'Google Pay' : 'Apple Pay'
|
||||
)
|
||||
|
||||
// 协议相关
|
||||
const agreed = ref(false)
|
||||
const agreementShow = ref(false)
|
||||
const agreementData = ref({
|
||||
title: '',
|
||||
content: ''
|
||||
})
|
||||
|
||||
// 订单号
|
||||
const orderSn = ref('')
|
||||
|
||||
/**
|
||||
* 获取导航栏高度
|
||||
*/
|
||||
const getNavbarHeight = () => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = systemInfo.statusBarHeight || 0
|
||||
|
||||
let navBarHeight = 44
|
||||
if (systemInfo.model.indexOf('iPhone') !== -1 && parseInt(systemInfo.model.slice(-2)) >= 11) {
|
||||
navBarHeight = 48
|
||||
}
|
||||
|
||||
const totalHeight = statusBarHeight.value + navBarHeight
|
||||
navbarHeight.value = totalHeight + 'px'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台信息
|
||||
*/
|
||||
const getPlatform = () => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
platform.value = systemInfo.platform === 'android' ? 'android' : 'ios'
|
||||
paymentMethod.value = platform.value === 'android' ? 5 : 3
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取VIP套餐列表
|
||||
*/
|
||||
const getPackages = async () => {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: t('common.loading')
|
||||
})
|
||||
|
||||
const res = await getVipPackages()
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
if (res.sysDictDatas && res.sysDictDatas.length > 0) {
|
||||
packages.value = res.sysDictDatas
|
||||
|
||||
// 检查是否有缓存的选择
|
||||
const cachedIndex = uni.getStorageSync('selectVipCard')
|
||||
if (cachedIndex !== null && cachedIndex < packages.value.length) {
|
||||
selectedIndex.value = cachedIndex
|
||||
} else {
|
||||
selectedIndex.value = 0
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取套餐列表失败:', error)
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择套餐
|
||||
*/
|
||||
const selectPackage = (index: number) => {
|
||||
selectedIndex.value = index
|
||||
uni.setStorageSync('selectVipCard', index)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取时长文本
|
||||
*/
|
||||
const getDuration = (remark: string) => {
|
||||
const days = parseInt(remark)
|
||||
if (days === 30) {
|
||||
return t('user.monthCard')
|
||||
} else if (days === 90) {
|
||||
return t('user.seasonCard')
|
||||
} else if (days === 365) {
|
||||
return t('user.yearCard')
|
||||
}
|
||||
return `${days} ${t('user.days')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换协议勾选
|
||||
*/
|
||||
const toggleAgreement = () => {
|
||||
agreed.value = !agreed.value
|
||||
uni.setStorageSync('Agreements_agreed', agreed.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示协议弹窗
|
||||
*/
|
||||
const showAgreementModal = async () => {
|
||||
try {
|
||||
const res = await commonApi.getAgreement(113)
|
||||
if (res) {
|
||||
let content = res.content || ''
|
||||
content = content.replace(
|
||||
/<h5>/g,
|
||||
'<view style="font-weight: bold;font-size: 32rpx;margin-top: 20rpx;margin-bottom: 20rpx;">'
|
||||
)
|
||||
content = content.replace(/<\/h5>/g, '</view>')
|
||||
|
||||
agreementData.value = {
|
||||
title: res.title,
|
||||
content: content
|
||||
}
|
||||
agreementShow.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取协议失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅处理
|
||||
*/
|
||||
const handleSubscribe = async () => {
|
||||
// 验证是否勾选协议
|
||||
if (!agreed.value) {
|
||||
uni.showToast({
|
||||
title: t('user.agreeFirst'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证是否选择套餐
|
||||
if (selectedIndex.value === null || !selectedPackage.value) {
|
||||
uni.showToast({
|
||||
title: t('user.selectPackage'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: t('common.loading')
|
||||
})
|
||||
|
||||
// 创建订单
|
||||
const orderData = {
|
||||
paymentMethod: paymentMethod.value,
|
||||
orderMoney: selectedPackage.value.money,
|
||||
vipBuyConfigId: selectedPackage.value.priceTypeId,
|
||||
orderType: 'abroadVip',
|
||||
abroadVipId: selectedPackage.value.id
|
||||
}
|
||||
|
||||
const orderRes = await createOrder(orderData)
|
||||
|
||||
if (orderRes.orderSn) {
|
||||
orderSn.value = orderRes.orderSn
|
||||
|
||||
// 根据平台调起支付
|
||||
if (platform.value === 'android') {
|
||||
await initiateGooglePay()
|
||||
} else {
|
||||
await initiateIAP()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建订单失败:', error)
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Pay 支付(Android)
|
||||
*/
|
||||
const initiateGooglePay = async () => {
|
||||
// TODO: 集成 Google Pay SDK
|
||||
// 这里需要使用 sn-googlepay5 插件
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: 'Google Pay 功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* IAP 支付(iOS)
|
||||
*/
|
||||
const initiateIAP = async () => {
|
||||
// TODO: 集成 IAP SDK
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: 'IAP 功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
*/
|
||||
const goBack = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getNavbarHeight()
|
||||
getPlatform()
|
||||
getPackages()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清除缓存
|
||||
uni.removeStorageSync('selectVipCard')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$theme-color: #54a966;
|
||||
|
||||
.wallet-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f7faf9;
|
||||
}
|
||||
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 44px;
|
||||
position: relative;
|
||||
|
||||
.navbar-left {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wallet-content {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: block;
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.package-section {
|
||||
background: #fff;
|
||||
border-radius: 15rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.package-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: -10rpx;
|
||||
}
|
||||
|
||||
.package-item {
|
||||
width: calc(33.333% - 20rpx);
|
||||
margin: 10rpx;
|
||||
padding: 30rpx 0;
|
||||
border: 2rpx solid #ebebeb;
|
||||
border-radius: 10rpx;
|
||||
text-align: center;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
border-color: $theme-color;
|
||||
background: rgba(84, 169, 102, 0.1);
|
||||
}
|
||||
|
||||
.package-price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.currency-icon {
|
||||
width: 25rpx;
|
||||
height: 25rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.price {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
margin-left: 5rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.package-duration {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-section {
|
||||
background: #fff;
|
||||
border-radius: 15rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.payment-group {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.payment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10rpx 0;
|
||||
|
||||
text {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.agreement-section {
|
||||
padding: 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.agreement-checkbox {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
.checkbox {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border: 2rpx solid #ddd;
|
||||
border-radius: 4rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 15rpx;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2rpx;
|
||||
|
||||
&.checked {
|
||||
background-color: $theme-color;
|
||||
border-color: $theme-color;
|
||||
}
|
||||
}
|
||||
|
||||
.agreement-text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 40rpx;
|
||||
|
||||
.link {
|
||||
color: $theme-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subscribe-button {
|
||||
padding: 40rpx 30rpx 60rpx;
|
||||
}
|
||||
|
||||
.agreement-modal {
|
||||
padding: 40rpx 30rpx;
|
||||
max-height: 70vh;
|
||||
|
||||
.modal-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
max-height: 60vh;
|
||||
font-size: 28rpx;
|
||||
color: #555;
|
||||
line-height: 45rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
242
pages/user/wallet/list.vue
Normal file
242
pages/user/wallet/list.vue
Normal file
@@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<view class="transaction-page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="custom-navbar" :style="{ height: navbarHeight }">
|
||||
<view class="navbar-content" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="navbar-left" @click="goBack">
|
||||
<wd-icon name="arrow-left" size="18px" color="#333" />
|
||||
</view>
|
||||
<text class="navbar-title">{{ $t('user.myAccount') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 交易记录列表 -->
|
||||
<view
|
||||
v-if="transactionList.length > 0"
|
||||
class="transaction-list"
|
||||
:style="{ paddingTop: navbarHeight }"
|
||||
>
|
||||
<view
|
||||
v-for="item in transactionList"
|
||||
:key="item.id"
|
||||
class="transaction-item"
|
||||
>
|
||||
<view class="item-header">
|
||||
<text class="transaction-type">{{ item.orderType }}</text>
|
||||
<text
|
||||
:class="['amount', { positive: item.orderType === '充值' }]"
|
||||
>
|
||||
{{ item.orderType === '充值' ? '+' : '' }}{{ item.changeAmount }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="remark">{{ formatRemark(item.remark) }}</text>
|
||||
<text class="create-time">{{ item.createTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view
|
||||
v-else-if="!loading"
|
||||
class="empty-state"
|
||||
:style="{ paddingTop: navbarHeight }"
|
||||
>
|
||||
<text class="empty-text">{{ $t('common.noData') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getTransactionList } from '@/api/modules/user'
|
||||
import type { ITransaction } from '@/types/user'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 导航栏高度
|
||||
const statusBarHeight = ref(0)
|
||||
const navbarHeight = ref('44px')
|
||||
|
||||
// 交易记录列表
|
||||
const transactionList = ref<ITransaction[]>([])
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
/**
|
||||
* 获取导航栏高度
|
||||
*/
|
||||
const getNavbarHeight = () => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = systemInfo.statusBarHeight || 0
|
||||
|
||||
let navBarHeight = 44
|
||||
if (systemInfo.model.indexOf('iPhone') !== -1 && parseInt(systemInfo.model.slice(-2)) >= 11) {
|
||||
navBarHeight = 48
|
||||
}
|
||||
|
||||
const totalHeight = statusBarHeight.value + navBarHeight
|
||||
navbarHeight.value = totalHeight + 'px'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取交易记录
|
||||
*/
|
||||
const getData = async () => {
|
||||
if (!userStore.userInfo.id) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
uni.showLoading({
|
||||
title: t('common.loading')
|
||||
})
|
||||
|
||||
const res = await getTransactionList(userStore.userInfo.id)
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
if (res.transactionDetailsList && res.transactionDetailsList.length > 0) {
|
||||
transactionList.value = res.transactionDetailsList
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取交易记录失败:', error)
|
||||
uni.hideLoading()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化备注信息(换行显示订单编号)
|
||||
*/
|
||||
const formatRemark = (remark: string) => {
|
||||
if (remark.includes('Stripe充值:')) {
|
||||
return remark.replace('Stripe充值:', 'Stripe充值:\n')
|
||||
} else if (remark.includes('订单编号为 - ')) {
|
||||
return remark.replace('订单编号为 - ', '订单编号为 - \n')
|
||||
}
|
||||
return remark
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
*/
|
||||
const goBack = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getNavbarHeight()
|
||||
getData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$theme-color: #54a966;
|
||||
|
||||
.transaction-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f7faf9;
|
||||
}
|
||||
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 44px;
|
||||
position: relative;
|
||||
|
||||
.navbar-left {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.transaction-list {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.transaction-item {
|
||||
background: #fff;
|
||||
border-radius: 15rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.transaction-type {
|
||||
font-size: 34rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-size: 34rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
|
||||
&.positive {
|
||||
color: $theme-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remark {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
color: #666;
|
||||
line-height: 38rpx;
|
||||
margin-bottom: 10rpx;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.create-time {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
padding-top: 100rpx;
|
||||
|
||||
.empty-text {
|
||||
font-size: 30rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,6 @@
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
"Courier New", monospace;
|
||||
--color-red-500: oklch(63.7% 0.237 25.331);
|
||||
--color-emerald-600: oklch(59.6% 0.145 163.225);
|
||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--default-transition-duration: 150ms;
|
||||
@@ -187,9 +186,6 @@
|
||||
.sticky {
|
||||
position: sticky;
|
||||
}
|
||||
.isolate {
|
||||
isolation: isolate;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
@media (width >= 40rem) {
|
||||
@@ -211,9 +207,6 @@
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
.contents {
|
||||
display: contents;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
@@ -229,12 +222,15 @@
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
.list-item {
|
||||
display: list-item;
|
||||
}
|
||||
.table {
|
||||
display: table;
|
||||
}
|
||||
.w-\[100px\] {
|
||||
width: 100px;
|
||||
}
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
.flex-shrink {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
@@ -244,6 +240,9 @@
|
||||
.resize {
|
||||
resize: both;
|
||||
}
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.border {
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 1px;
|
||||
@@ -251,35 +250,25 @@
|
||||
.bg-\[red\] {
|
||||
background-color: red;
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
.text-\[\#fff\] {
|
||||
color: #fff;
|
||||
}
|
||||
.capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.lowercase {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.ordinal {
|
||||
--tw-ordinal: ordinal;
|
||||
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
||||
}
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.shadow {
|
||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.ring {
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
@@ -304,10 +293,6 @@
|
||||
--tw-ease: var(--ease-in-out);
|
||||
transition-timing-function: var(--ease-in-out);
|
||||
}
|
||||
.ease-out {
|
||||
--tw-ease: var(--ease-out);
|
||||
transition-timing-function: var(--ease-out);
|
||||
}
|
||||
.hover\:bg-red-500 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
|
||||
@@ -4,20 +4,22 @@ import { setAuthToken, clearAuthToken } from '@/utils/auth'
|
||||
import type { IUserInfo } from '@/types/user'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: (): IUserInfo => ({
|
||||
state: (): IUserInfo => (uni.getStorageSync('userInfo') || {
|
||||
id: 0,
|
||||
name: '',
|
||||
nickname: '',
|
||||
avatar: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
token: uni.getStorageSync('token') || '',
|
||||
token: '',
|
||||
age: '',
|
||||
tel: '',
|
||||
sex: '',
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isLoggedIn: (state: any) => Boolean(state.token),
|
||||
userInfo: (state: any) => {
|
||||
const { id, name, avatar, email, phone } = state
|
||||
return { id, name, avatar, email, phone }
|
||||
return state
|
||||
},
|
||||
},
|
||||
|
||||
@@ -54,7 +56,6 @@ export const useUserStore = defineStore('user', {
|
||||
|
||||
clearAuthToken()
|
||||
uni.removeStorageSync('userInfo')
|
||||
uni.reLaunch({ url: '/pages/user/login' })
|
||||
},
|
||||
|
||||
/** 从本地存储恢复用户信息 */
|
||||
|
||||
536
style/tailwind.css
Normal file
536
style/tailwind.css
Normal file
@@ -0,0 +1,536 @@
|
||||
/*! tailwindcss v4.1.16 | MIT License | https://tailwindcss.com */
|
||||
@layer properties;
|
||||
@layer theme, base, components, utilities;
|
||||
@layer theme {
|
||||
:root, :host {
|
||||
--font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
"Courier New", monospace;
|
||||
--color-red-500: oklch(63.7% 0.237 25.331);
|
||||
--color-emerald-600: oklch(59.6% 0.145 163.225);
|
||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--default-transition-duration: 150ms;
|
||||
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--default-font-family: var(--font-sans);
|
||||
--default-mono-font-family: var(--font-mono);
|
||||
}
|
||||
}
|
||||
@layer base {
|
||||
*, ::after, ::before, ::backdrop, ::file-selector-button {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0 solid;
|
||||
}
|
||||
html, :host {
|
||||
line-height: 1.5;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
tab-size: 4;
|
||||
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
|
||||
font-feature-settings: var(--default-font-feature-settings, normal);
|
||||
font-variation-settings: var(--default-font-variation-settings, normal);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
hr {
|
||||
height: 0;
|
||||
color: inherit;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
-webkit-text-decoration: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
b, strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
code, kbd, samp, pre {
|
||||
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
|
||||
font-feature-settings: var(--default-mono-font-feature-settings, normal);
|
||||
font-variation-settings: var(--default-mono-font-variation-settings, normal);
|
||||
font-size: 1em;
|
||||
}
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
sub, sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
table {
|
||||
text-indent: 0;
|
||||
border-color: inherit;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
ol, ul, menu {
|
||||
list-style: none;
|
||||
}
|
||||
img, svg, video, canvas, audio, iframe, embed, object {
|
||||
display: block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
img, video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
button, input, select, optgroup, textarea, ::file-selector-button {
|
||||
font: inherit;
|
||||
font-feature-settings: inherit;
|
||||
font-variation-settings: inherit;
|
||||
letter-spacing: inherit;
|
||||
color: inherit;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
opacity: 1;
|
||||
}
|
||||
:where(select:is([multiple], [size])) optgroup {
|
||||
font-weight: bolder;
|
||||
}
|
||||
:where(select:is([multiple], [size])) optgroup option {
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
::file-selector-button {
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
::placeholder {
|
||||
opacity: 1;
|
||||
}
|
||||
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
|
||||
::placeholder {
|
||||
color: currentcolor;
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
color: color-mix(in oklab, currentcolor 50%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
::-webkit-date-and-time-value {
|
||||
min-height: 1lh;
|
||||
text-align: inherit;
|
||||
}
|
||||
::-webkit-datetime-edit {
|
||||
display: inline-flex;
|
||||
}
|
||||
::-webkit-datetime-edit-fields-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
|
||||
padding-block: 0;
|
||||
}
|
||||
::-webkit-calendar-picker-indicator {
|
||||
line-height: 1;
|
||||
}
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
|
||||
appearance: button;
|
||||
}
|
||||
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
[hidden]:where(:not([hidden="until-found"])) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@layer utilities {
|
||||
.collapse {
|
||||
visibility: collapse;
|
||||
}
|
||||
.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
.absolute {
|
||||
position: absolute;
|
||||
}
|
||||
.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
.static {
|
||||
position: static;
|
||||
}
|
||||
.sticky {
|
||||
position: sticky;
|
||||
}
|
||||
.isolate {
|
||||
isolation: isolate;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
@media (width >= 40rem) {
|
||||
max-width: 40rem;
|
||||
}
|
||||
@media (width >= 48rem) {
|
||||
max-width: 48rem;
|
||||
}
|
||||
@media (width >= 64rem) {
|
||||
max-width: 64rem;
|
||||
}
|
||||
@media (width >= 80rem) {
|
||||
max-width: 80rem;
|
||||
}
|
||||
@media (width >= 96rem) {
|
||||
max-width: 96rem;
|
||||
}
|
||||
}
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
.contents {
|
||||
display: contents;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
.list-item {
|
||||
display: list-item;
|
||||
}
|
||||
.table {
|
||||
display: table;
|
||||
}
|
||||
.flex-shrink {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.transform {
|
||||
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
||||
}
|
||||
.resize {
|
||||
resize: both;
|
||||
}
|
||||
.border {
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 1px;
|
||||
}
|
||||
.bg-\[red\] {
|
||||
background-color: red;
|
||||
}
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
.text-\[\#fff\] {
|
||||
color: #fff;
|
||||
}
|
||||
.capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.lowercase {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.ordinal {
|
||||
--tw-ordinal: ordinal;
|
||||
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
||||
}
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.shadow {
|
||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.ring {
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.outline {
|
||||
outline-style: var(--tw-outline-style);
|
||||
outline-width: 1px;
|
||||
}
|
||||
.blur {
|
||||
--tw-blur: blur(8px);
|
||||
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
||||
}
|
||||
.filter {
|
||||
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
||||
}
|
||||
.transition {
|
||||
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
}
|
||||
.ease-in-out {
|
||||
--tw-ease: var(--ease-in-out);
|
||||
transition-timing-function: var(--ease-in-out);
|
||||
}
|
||||
.ease-out {
|
||||
--tw-ease: var(--ease-out);
|
||||
transition-timing-function: var(--ease-out);
|
||||
}
|
||||
.hover\:bg-red-500 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-red-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@property --tw-rotate-x {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-rotate-y {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-rotate-z {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-skew-x {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-skew-y {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-border-style {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: solid;
|
||||
}
|
||||
@property --tw-ordinal {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-slashed-zero {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-numeric-figure {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-numeric-spacing {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-numeric-fraction {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
@property --tw-shadow-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-shadow-alpha {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 100%;
|
||||
}
|
||||
@property --tw-inset-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
@property --tw-inset-shadow-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-inset-shadow-alpha {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 100%;
|
||||
}
|
||||
@property --tw-ring-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-ring-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
@property --tw-inset-ring-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-inset-ring-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
@property --tw-ring-inset {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-ring-offset-width {
|
||||
syntax: "<length>";
|
||||
inherits: false;
|
||||
initial-value: 0px;
|
||||
}
|
||||
@property --tw-ring-offset-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: #fff;
|
||||
}
|
||||
@property --tw-ring-offset-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
@property --tw-outline-style {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: solid;
|
||||
}
|
||||
@property --tw-blur {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-brightness {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-contrast {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-grayscale {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-hue-rotate {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-invert {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-opacity {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-saturate {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-sepia {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-drop-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-drop-shadow-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-drop-shadow-alpha {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 100%;
|
||||
}
|
||||
@property --tw-drop-shadow-size {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-ease {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@layer properties {
|
||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||
*, ::before, ::after, ::backdrop {
|
||||
--tw-rotate-x: initial;
|
||||
--tw-rotate-y: initial;
|
||||
--tw-rotate-z: initial;
|
||||
--tw-skew-x: initial;
|
||||
--tw-skew-y: initial;
|
||||
--tw-border-style: solid;
|
||||
--tw-ordinal: initial;
|
||||
--tw-slashed-zero: initial;
|
||||
--tw-numeric-figure: initial;
|
||||
--tw-numeric-spacing: initial;
|
||||
--tw-numeric-fraction: initial;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-color: initial;
|
||||
--tw-shadow-alpha: 100%;
|
||||
--tw-inset-shadow: 0 0 #0000;
|
||||
--tw-inset-shadow-color: initial;
|
||||
--tw-inset-shadow-alpha: 100%;
|
||||
--tw-ring-color: initial;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-inset-ring-color: initial;
|
||||
--tw-inset-ring-shadow: 0 0 #0000;
|
||||
--tw-ring-inset: initial;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-outline-style: solid;
|
||||
--tw-blur: initial;
|
||||
--tw-brightness: initial;
|
||||
--tw-contrast: initial;
|
||||
--tw-grayscale: initial;
|
||||
--tw-hue-rotate: initial;
|
||||
--tw-invert: initial;
|
||||
--tw-opacity: initial;
|
||||
--tw-saturate: initial;
|
||||
--tw-sepia: initial;
|
||||
--tw-drop-shadow: initial;
|
||||
--tw-drop-shadow-color: initial;
|
||||
--tw-drop-shadow-alpha: 100%;
|
||||
--tw-drop-shadow-size: initial;
|
||||
--tw-ease: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
137
style/ui.scss
Normal file
137
style/ui.scss
Normal file
@@ -0,0 +1,137 @@
|
||||
:root,
|
||||
page {
|
||||
/* 文字字号 */
|
||||
--wot-fs-title: 18px; // 标题字号/重要正文字号
|
||||
--wot-fs-content: 16px; // 普通正文
|
||||
--wot-fs-secondary: 14px; // 次要信息,注释/补充/正文
|
||||
|
||||
// 导航栏
|
||||
// --wot-navbar-background: #5355C8;
|
||||
// --wot-navbar-color: #fff;
|
||||
// --wot-navbar-hover-color: #4D4DB9;
|
||||
|
||||
// 底部tabbar
|
||||
// --wot-tabbar-height: 60px;
|
||||
// --wot-tabbar-item-title-font-size: 16px;
|
||||
|
||||
// cell
|
||||
--wot-cell-title-fs: 16px;
|
||||
|
||||
// dropdown
|
||||
// --wot-drop-menu-selected-color: #5355C8;
|
||||
|
||||
// collapse
|
||||
--wot-collapse-body-padding: 0px 20px 15px;
|
||||
}
|
||||
uni-textarea {
|
||||
height: 100px !important;
|
||||
}
|
||||
|
||||
// 表单
|
||||
.wd-input__body, .wd-picker__body {
|
||||
text-align: left;
|
||||
}
|
||||
.wd-input__error-message,
|
||||
.wd-picker__error-message,
|
||||
.wd-cell__error-message {
|
||||
text-align: right !important;
|
||||
}
|
||||
.wd-input.is-cell{
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px !important;
|
||||
}
|
||||
.wd-textarea.is-cell {
|
||||
display: block !important;
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px !important;
|
||||
}
|
||||
.wd-textarea.is-auto-height uni-textarea {
|
||||
height: auto !important;
|
||||
}
|
||||
// 错误样式
|
||||
.wd-input.is-error {
|
||||
&::after {
|
||||
background: red !important;
|
||||
}
|
||||
|
||||
.wd-input__icon {
|
||||
color: red !important;
|
||||
}
|
||||
}
|
||||
// 顶部对齐表单
|
||||
.label-position-top {
|
||||
.wd-input {
|
||||
display: block !important;
|
||||
}
|
||||
.wd-input__body {
|
||||
text-align: left !important;
|
||||
}
|
||||
.wd-input__value {
|
||||
border: 1px solid #eee;
|
||||
padding: 2px 10px;
|
||||
}
|
||||
.wd-input__error-message {
|
||||
text-align: left !important;
|
||||
}
|
||||
}
|
||||
|
||||
// cell 样式
|
||||
.wd-cell-group {
|
||||
padding: 0 8px;
|
||||
}
|
||||
.wd-cell {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
.wd-cell .wd-cell__wrapper {
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px !important;
|
||||
}
|
||||
.wd-cell__arrow-right {
|
||||
margin-right: -5px;
|
||||
}
|
||||
.wd-picker.is-border .wd-picker__cell::after,
|
||||
.wd-input.is-cell.is-border::after,
|
||||
.wd-textarea.is-cell.is-border::after{
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
// picker
|
||||
.wd-picker__cell {
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px !important;
|
||||
}
|
||||
.wd-picker__arrow {
|
||||
font-size: 18px !important;
|
||||
margin-right: -5px;
|
||||
}
|
||||
|
||||
// popup
|
||||
// .wd-popup {
|
||||
// z-index: 9999 !important;
|
||||
// }
|
||||
// .wd-overlay {
|
||||
// z-index: 9998 !important;
|
||||
// }
|
||||
|
||||
// uni-ui form
|
||||
// .uni-forms-item {
|
||||
// margin-bottom: 10px !important;
|
||||
// }
|
||||
// .uni-forms-item__label {
|
||||
// height: auto !important;
|
||||
// padding-bottom: 0 !important;
|
||||
// font-size: var(--wot-fs-content) !important;
|
||||
// }
|
||||
// .uni-easyinput__content-input {
|
||||
// height: 30px !important;
|
||||
// }
|
||||
// .uni-easyinput__placeholder-class {
|
||||
// font-size: var(--wot-fs-content) !important;
|
||||
// }
|
||||
|
||||
// // tag
|
||||
// .wd-tag {
|
||||
// font-size: var(--wot-fs-secondary) !important;
|
||||
// border-radius: 4px !important;
|
||||
// }
|
||||
@@ -57,3 +57,79 @@ export interface IForgetPasswordForm {
|
||||
password: string
|
||||
confirmPassword: string
|
||||
}
|
||||
|
||||
/**
|
||||
* VIP信息接口
|
||||
*/
|
||||
export interface IVipInfo {
|
||||
id: number
|
||||
endTime: string
|
||||
vipType: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单接口
|
||||
*/
|
||||
export interface IOrder {
|
||||
id: number
|
||||
orderSn: string
|
||||
bookEntity: {
|
||||
id: number
|
||||
name: string
|
||||
images: string
|
||||
}
|
||||
orderMoney: number
|
||||
paymentMethod: string // '4'-虚拟货币, '5'-真实货币
|
||||
createTime: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* VIP套餐接口
|
||||
*/
|
||||
export interface IVipPackage {
|
||||
id: number
|
||||
dictType: string // 价格
|
||||
dictValue: string // 产品ID
|
||||
money: number
|
||||
priceTypeId: number
|
||||
remark: string // 时长(天数)
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 交易记录接口
|
||||
*/
|
||||
export interface ITransaction {
|
||||
id: number
|
||||
orderType: string // '充值' | '消费'
|
||||
changeAmount: number
|
||||
remark: string
|
||||
createTime: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 反馈表单接口
|
||||
*/
|
||||
export interface IFeedbackForm {
|
||||
type: string // 问题类型
|
||||
account: string // 账号
|
||||
relation?: string // 订单编号(可选)
|
||||
content: string // 问题描述
|
||||
contactInformation: string // 联系电话
|
||||
image?: string // 截图(多张用逗号分隔)
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页数据接口
|
||||
*/
|
||||
export interface IPageData<T> {
|
||||
records: T[]
|
||||
total: number
|
||||
size: number
|
||||
current: number
|
||||
pages: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -1,5 +1,51 @@
|
||||
/**
|
||||
* 页面跳转
|
||||
*/
|
||||
export const onPageJump = (path: string) => {
|
||||
uni.navigateTo({
|
||||
url: path
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 拨打电话
|
||||
*/
|
||||
export const makePhoneCall = (phoneNumber: string, title: string, t: Function) => {
|
||||
uni.showModal({
|
||||
title: title,
|
||||
content: phoneNumber,
|
||||
confirmText: t('common.confirm'),
|
||||
cancelText: t('common.cancel'),
|
||||
success: (res: any) => {
|
||||
if (res.confirm) {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: phoneNumber,
|
||||
success: () => {
|
||||
console.log('拨打电话成功')
|
||||
},
|
||||
fail: (error: any) => {
|
||||
console.error('拨打电话失败:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制到剪贴板
|
||||
*/
|
||||
export const copyToClipboard = (content: string, title: string, t: Function) => {
|
||||
uni.setClipboardData({
|
||||
data: content,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: title + t('user.copySuccess'),
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('复制失败:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
54
utils/system.ts
Normal file
54
utils/system.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
interface SystemInfo {
|
||||
statusBarHeight?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface MenuButtonRect {
|
||||
top: number;
|
||||
height: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface LeftIcon {
|
||||
left: string;
|
||||
width: string;
|
||||
}
|
||||
|
||||
const SYSTEM_INFO: SystemInfo = uni.getSystemInfoSync();
|
||||
|
||||
export const getStatusBarHeight = (): number => SYSTEM_INFO.statusBarHeight || 0;
|
||||
|
||||
export const getTitleBarHeight = (): number => {
|
||||
if (uni.getMenuButtonBoundingClientRect) {
|
||||
const { top, height }: MenuButtonRect = uni.getMenuButtonBoundingClientRect();
|
||||
return height + (top - getStatusBarHeight()) * 2;
|
||||
} else {
|
||||
return 44;
|
||||
}
|
||||
}
|
||||
|
||||
export const getNavBarHeight = (): number => getStatusBarHeight() + getTitleBarHeight();
|
||||
|
||||
export const getLeftIconLeft = (): number => {
|
||||
// #ifdef MP-TOUTIAO
|
||||
const { leftIcon: { left, width } }: { leftIcon: LeftIcon } = tt.getCustomButtonBoundingClientRect();
|
||||
return left + parseInt(width);
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-TOUTIAO
|
||||
return 0;
|
||||
// #endif
|
||||
}
|
||||
|
||||
export const getNavbarHeight2 = () => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = systemInfo.statusBarHeight || 0
|
||||
|
||||
let navBarHeight = 44
|
||||
if (systemInfo.model.indexOf('iPhone') !== -1 && parseInt(systemInfo.model.slice(-2)) >= 11) {
|
||||
navBarHeight = 48
|
||||
}
|
||||
|
||||
const totalHeight = statusBarHeight.value + navBarHeight
|
||||
navbarHeight.value = totalHeight + 'px'
|
||||
}
|
||||
@@ -30,5 +30,10 @@ child_process.exec(
|
||||
})
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [uni()]
|
||||
plugins: [uni()],
|
||||
define: {
|
||||
__VUE_I18N_FULL_INSTALL__: true,
|
||||
__VUE_I18N_LEGACY_API__: false,
|
||||
__INTLIFY_PROD_DEVTOOLS__: false
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user