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:
jacobxu666
2026-06-26 00:32:21 +08:00
co-authored by Claude
parent ff524ac973
commit 5ac924a76c
17 changed files with 3201 additions and 113 deletions
+297 -85
View File
@@ -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>