feat: 新增绑卡激活、旅游卡购买、工会购买、使用说明、常见问题、订单管理页面
- 新增 pages/card-bind 绑卡激活页(证件类型下拉、照片上传、表单校验) - 新增 pages/card-buy 超级玩家卡购买页(微信支付) - 新增 pages/union-buy 工会购买页 - 新增 pages/usage-guide 使用说明页 - 新增 pages/faq 常见问题页(购买类/预约类/绑定类折叠问答) - 新增 pages/order 订单列表页(Tab筛选/下拉刷新/状态标签) - 新增 pages/order 订单详情页(状态差异化展示/卡号复制) - 登录页背景改为白色、按钮统一主色、协议未同意提示 - 首页金刚区跳转对接(绑卡/预约/入园/FAQ) - 旅游卡页使用说明跳转、景区权益改为使用说明 - 我的页面菜单跳转对接(订单/使用说明/FAQ) - 景区列表新增卡种筛选、吸顶区动态高度适配 - 全局统一思源宋体、主色#3c9cff、rpx单位 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+297
-85
@@ -1,42 +1,99 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<image class="logo" src="/static/logo.png" mode="aspectFit" />
|
||||
<text class="app-name">湖南12301文旅专区</text>
|
||||
<text class="app-desc">年卡购买 · 景区预约 · 权益查看</text>
|
||||
<view class="login-page">
|
||||
<!-- Brand area -->
|
||||
<view class="brand" :class="{ 'brand--visible': showContent }">
|
||||
<image class="brand-logo" src="/static/logo.png" mode="aspectFit" />
|
||||
<text class="brand-name">爱上萍乡</text>
|
||||
<text class="brand-desc">赣湘之间,发现旅途美好</text>
|
||||
</view>
|
||||
|
||||
<view class="login-area">
|
||||
<button class="btn-wx-login"
|
||||
open-type="getPhoneNumber"
|
||||
@getphonenumber="handleGetPhoneNumber"
|
||||
:disabled="loading">
|
||||
{{ loading ? '登录中...' : '微信一键登录' }}
|
||||
<!-- Actions: 2 buttons -->
|
||||
<view class="actions" :class="{ 'actions--visible': showContent }">
|
||||
<!-- 未同意条款:普通按钮,点击提示 -->
|
||||
<button
|
||||
v-if="!agreed"
|
||||
class="btn-primary"
|
||||
:class="{ 'btn-primary--disabled': loading }"
|
||||
:disabled="loading"
|
||||
@tap="showToast('请先阅读并同意服务条款和隐私政策')"
|
||||
>
|
||||
<view class="btn-primary__inner">
|
||||
<view class="wx-icon" />
|
||||
<text class="btn-primary__text">微信一键登录</text>
|
||||
</view>
|
||||
</button>
|
||||
<!-- 已同意条款:微信授权按钮 -->
|
||||
<button
|
||||
v-else
|
||||
class="btn-primary"
|
||||
:class="{ 'btn-primary--disabled': loading }"
|
||||
open-type="getPhoneNumber"
|
||||
@getphonenumber="handleLogin"
|
||||
:disabled="loading"
|
||||
>
|
||||
<view v-if="!loading" class="btn-primary__inner">
|
||||
<view class="wx-icon" />
|
||||
<text class="btn-primary__text">微信一键登录</text>
|
||||
</view>
|
||||
<view v-else class="btn-primary__inner">
|
||||
<view class="loading-spinner" />
|
||||
<text class="btn-primary__text">登录中...</text>
|
||||
</view>
|
||||
</button>
|
||||
|
||||
<button class="btn-cancel" @tap="handleCancel">
|
||||
<text class="btn-cancel__text">取消</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="agreement">
|
||||
<text class="agreement-text">登录即表示同意</text>
|
||||
<text class="agreement-link">《用户协议》</text>
|
||||
<text class="agreement-text">和</text>
|
||||
<text class="agreement-link">《隐私政策》</text>
|
||||
<!-- Agreement: checkbox + links -->
|
||||
<view class="agreement" :class="{ 'agreement--visible': showContent }">
|
||||
<view
|
||||
class="checkbox"
|
||||
:class="{ 'checkbox--checked': agreed }"
|
||||
@tap="agreed = !agreed"
|
||||
>
|
||||
<view v-if="agreed" class="checkbox__check" />
|
||||
</view>
|
||||
<view class="agreement__text">
|
||||
<text class="agreement__label">我已阅读并同意</text>
|
||||
<text class="agreement__link" @tap="openAgreement">《服务条款》</text>
|
||||
<text class="agreement__link" @tap="openPrivacy">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useUserStore } from '../../store/user'
|
||||
import { showToast } from '../../utils'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const agreed = ref(false)
|
||||
const loading = ref(false)
|
||||
const showContent = ref(false)
|
||||
|
||||
/** 微信手机号授权回调 → 登录 */
|
||||
async function handleGetPhoneNumber(e: any) {
|
||||
// 用户拒绝授权手机号
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
showContent.value = true
|
||||
}, 80)
|
||||
})
|
||||
|
||||
function handleCancel() {
|
||||
uni.navigateBack({ fail: () => uni.switchTab({ url: '/pages/index/index' }) })
|
||||
}
|
||||
|
||||
function openAgreement() {
|
||||
uni.navigateTo({ url: '/pages/login/agreement' })
|
||||
}
|
||||
|
||||
function openPrivacy() {
|
||||
uni.navigateTo({ url: '/pages/login/privacy' })
|
||||
}
|
||||
|
||||
async function handleLogin(e: any) {
|
||||
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
|
||||
showToast('需要手机号授权才能登录')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -48,20 +105,18 @@ async function handleGetPhoneNumber(e: any) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取微信登录 code(与 phoneCode 是完全不同的凭证)
|
||||
const loginRes: any = await new Promise((resolve, reject) => {
|
||||
uni.login({ provider: 'weixin', success: resolve, fail: reject })
|
||||
})
|
||||
|
||||
// 防御性检查:loginCode 不能为空
|
||||
if (!loginRes || !loginRes.code) {
|
||||
showToast('微信登录失败,请重试')
|
||||
return
|
||||
}
|
||||
|
||||
await userStore.wxLogin({
|
||||
loginCode: loginRes.code, // wx.login() 返回的 code
|
||||
phoneCode: phoneCode // getPhoneNumber 返回的 code
|
||||
loginCode: loginRes.code,
|
||||
phoneCode: phoneCode
|
||||
})
|
||||
|
||||
showToast('登录成功', 'success')
|
||||
@@ -76,75 +131,232 @@ async function handleGetPhoneNumber(e: any) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
padding: 0 60rpx;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
}
|
||||
<style lang="scss" scoped>
|
||||
/* Lucide icons — data URI pattern (matches scenic-detail) */
|
||||
$icon-size: 40rpx;
|
||||
$icon-wechat: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7.9 20A9 9 0 1 0 4 16.1L2 22Z'/%3E%3C/svg%3E");
|
||||
$icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
|
||||
|
||||
.header {
|
||||
.login-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 160rpx;
|
||||
margin-bottom: 80rpx;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 44rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.app-desc {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.login-area {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-wx-login {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
line-height: 96rpx;
|
||||
background: #07C160;
|
||||
color: #fff;
|
||||
border-radius: 48rpx;
|
||||
font-size: 32rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-wx-login[disabled] {
|
||||
background: #9ED6B5;
|
||||
}
|
||||
|
||||
.agreement {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background: #FFFFFF;
|
||||
padding: 0 48rpx;
|
||||
gap: 48rpx;
|
||||
overflow-x: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ========== Brand ========== */
|
||||
.brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
transition: opacity 0.6s ease, transform 0.6s ease;
|
||||
}
|
||||
|
||||
.brand--visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-family: $font-family-display;
|
||||
font-size: 56rpx;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $uv-main-color;
|
||||
line-height: $line-height-tight;
|
||||
letter-spacing: 4rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.brand-desc {
|
||||
font-size: $font-size-base;
|
||||
color: $uv-tips-color;
|
||||
font-weight: $font-weight-normal;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
/* ========== Actions ========== */
|
||||
.actions {
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
transition: opacity 0.6s ease 0.1s, transform 0.6s ease 0.1s;
|
||||
}
|
||||
|
||||
.actions--visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
height: 92rpx;
|
||||
border-radius: $radius-xl;
|
||||
background: $uv-primary;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0 0 24rpx 0;
|
||||
line-height: normal;
|
||||
box-shadow: 0 6rpx 20rpx rgba(60, 156, 255, 0.28);
|
||||
transition: all $transition-base;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary--disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.btn-primary:active:not(.btn-primary--disabled) {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2rpx 8rpx rgba(60, 156, 255, 0.2);
|
||||
}
|
||||
|
||||
.btn-primary__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 92rpx;
|
||||
}
|
||||
|
||||
.btn-primary__text {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: #FFFFFF;
|
||||
margin-left: $spacing-2;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.wx-icon {
|
||||
width: $icon-size;
|
||||
height: $icon-size;
|
||||
background-image: $icon-wechat;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #FFFFFF;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
width: 100%;
|
||||
height: 92rpx;
|
||||
border-radius: $radius-xl;
|
||||
background: #FFFFFF;
|
||||
border: 2rpx solid $uv-border-color;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: 92rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
transition: all $transition-base;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-cancel:active {
|
||||
transform: scale(0.98);
|
||||
background: $uv-bg-color;
|
||||
}
|
||||
|
||||
.btn-cancel__text {
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-size: $font-size-lg;
|
||||
font-weight: $font-weight-medium;
|
||||
color: $uv-content-color;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ========== Agreement ========== */
|
||||
.agreement {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
transition: opacity 0.6s ease 0.2s, transform 0.6s ease 0.2s;
|
||||
}
|
||||
|
||||
.agreement--visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
flex-shrink: 0;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 3rpx solid $uv-border-color;
|
||||
border-radius: 10rpx;
|
||||
margin-right: $spacing-2;
|
||||
margin-top: 2rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all $transition-base;
|
||||
}
|
||||
|
||||
.checkbox--checked {
|
||||
background: $uv-primary;
|
||||
border-color: $uv-primary;
|
||||
}
|
||||
|
||||
.checkbox__check {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
background-image: $icon-check;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.agreement__text {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 40rpx 0 60rpx;
|
||||
align-items: center;
|
||||
line-height: $line-height-normal;
|
||||
}
|
||||
|
||||
.agreement-text {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
.agreement__label {
|
||||
font-size: $font-size-base;
|
||||
color: $uv-content-color;
|
||||
}
|
||||
|
||||
.agreement-link {
|
||||
font-size: 22rpx;
|
||||
color: #4A90D9;
|
||||
.agreement__link {
|
||||
font-size: $font-size-base;
|
||||
color: $uv-primary;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user