更新:增加“我的”相关功能
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user