补全其他文件

This commit is contained in:
ty200947752 2025-12-15 11:43:43 +08:00
parent 5298ed1acf
commit dcb3f8cc41
3 changed files with 528 additions and 0 deletions

323
pages/register/register.vue Normal file
View File

@ -0,0 +1,323 @@
<template>
<view class="register-container">
<!-- 手机号 -->
<view class="input-row">
<text class="label">手机号</text>
<input
type="number"
v-model="phone"
class="input-field"
placeholder="请输入手机号"
/>
</view>
<!-- 密码 -->
<view class="input-row">
<text class="label">密码</text>
<input
type="password"
v-model="pwd"
class="input-field"
placeholder="请输入密码"
/>
</view>
<!-- 确认密码 -->
<view class="input-row">
<text class="label">确认密码</text>
<input
type="password"
v-model="confirmPwd"
class="input-field"
placeholder="请再次输入密码"
/>
</view>
<!-- 手机验证码 -->
<view class="input-row">
<text class="label">验证码</text>
<view class="captcha-container">
<input
type="text"
v-model="userCaptcha"
class="input-field captcha-input"
placeholder="请输入验证码"
/>
<button
class="captcha-btn"
:class="{ disabled: countdown > 0 || !isPhoneValidForCaptcha }"
@click="sendSmsCode"
:disabled="countdown > 0 || !isPhoneValidForCaptcha"
>
{{ countdown > 0 ? `${countdown}秒后重发` : '获取验证码' }}
</button>
</view>
</view>
<!-- 注册按钮 -->
<button
class="btn register-btn"
:class="{ disabled: !canRegister }"
@click="handleRegister"
:disabled="!canRegister"
>
注册
</button>
<!-- 返回登录 -->
<view class="back-link">
<text class="back-text" @click="goToLogin">已有账号点击登录</text>
</view>
</view>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue'
//
const account = ref('')
const pwd = ref('')
const confirmPwd = ref('')
const phone = ref('')
const userCaptcha = ref('') //
const smsCode = ref('') //
const countdown = ref(0) //
let timer = null //
//
const isPhoneValidForCaptcha = computed(() => {
const phoneRegex = /^1[3-9]\d{9}$/
return phone.value.trim() && phoneRegex.test(phone.value)
})
//
const canRegister = computed(() => {
//
const isAccountValid = account.value.trim().length >= 6
const isPasswordValid = pwd.value.trim().length > 0
const isConfirmPasswordValid = confirmPwd.value === pwd.value
const isPhoneValid = isPhoneValidForCaptcha.value
const isCaptchaValid = userCaptcha.value.trim().length > 0
return isAccountValid &&
isPasswordValid &&
isConfirmPasswordValid &&
isPhoneValid &&
isCaptchaValid
})
//
const sendSmsCode = async () => {
//
const phoneRegex = /^1[3-9]\d{9}$/
if (!phone.value.trim()) {
uni.showToast({ title: '请输入手机号', icon: 'none' })
return
}
if (!phoneRegex.test(phone.value)) {
uni.showToast({ title: '手机号格式不正确', icon: 'none' })
return
}
//
try {
uni.showLoading({ title: '发送中...' })
// API
await new Promise(resolve => setTimeout(resolve, 1000))
// 6
const code = Math.floor(100000 + Math.random() * 900000).toString()
smsCode.value = code
console.log('发送的验证码:', code) //
uni.hideLoading()
uni.showToast({ title: '验证码已发送', icon: 'success' })
//
startCountdown()
} catch (error) {
uni.hideLoading()
uni.showToast({ title: '验证码发送失败', icon: 'none' })
}
}
//
const startCountdown = () => {
countdown.value = 60
if (timer) clearInterval(timer)
timer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(timer)
timer = null
}
}, 1000)
}
//
const handleRegister = async () => {
if (!account.value.trim()) {
uni.showToast({ title: '请输入账号', icon: 'none' })
return
}
if (account.value.length < 6) {
uni.showToast({ title: '账号长度不能少于6位', icon: 'none' })
return
}
if (!pwd.value.trim()) {
uni.showToast({ title: '请输入密码', icon: 'none' })
return
}
if (pwd.value !== confirmPwd.value) {
uni.showToast({ title: '两次输入的密码不一致', icon: 'none' })
return
}
//
const phoneRegex = /^1[3-9]\d{9}$/
if (!phone.value.trim()) {
uni.showToast({ title: '请输入手机号', icon: 'none' })
return
}
if (!phoneRegex.test(phone.value)) {
uni.showToast({ title: '手机号格式不正确', icon: 'none' })
return
}
if (!userCaptcha.value.trim()) {
uni.showToast({ title: '请输入验证码', icon: 'none' })
return
}
//
if (userCaptcha.value !== smsCode.value) {
uni.showToast({ title: '验证码错误', icon: 'none' })
return
}
//
uni.showLoading({ title: '注册中...' })
try {
// API
await new Promise(resolve => setTimeout(resolve, 1000))
uni.hideLoading()
uni.showToast({ title: '注册成功', icon: 'success' })
//
setTimeout(() => {
uni.navigateTo({
url: '/pages/login/index'
})
}, 500)
} catch (error) {
uni.hideLoading()
uni.showToast({ title: '注册失败', icon: 'none' })
}
}
//
const goToLogin = () => {
uni.navigateTo({ url: '/pages/login/index' })
}
//
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
</script>
<style scoped>
.register-container {
padding: 40rpx;
background-color: #fff;
min-height: 100vh;
}
.input-row {
display: flex;
align-items: center;
margin-bottom: 40rpx;
}
.label {
width: 160rpx;
font-size: 32rpx;
color: #333;
}
.input-field {
flex: 1;
height: 80rpx;
border: 2px solid #ccc;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
-webkit-appearance: none;
outline: none;
}
.captcha-container {
display: flex;
align-items: center;
flex: 1;
}
.captcha-input {
flex: 1;
margin-right: 10rpx;
}
.captcha-btn {
width: 180rpx;
height: 80rpx;
line-height: 80rpx;
text-align: center;
background-color: #007aff;
color: #fff;
border: none;
border-radius: 8rpx;
font-size: 24rpx;
}
.captcha-btn.disabled {
background-color: #ccc;
color: #999;
}
.btn {
width: 100%;
height: 80rpx;
border-radius: 8rpx;
font-size: 32rpx;
color: #fff;
background-color: #007aff;
margin-bottom: 30rpx;
}
.btn.disabled {
background-color: #ccc;
}
.back-link {
text-align: center;
}
.back-text {
font-size: 26rpx;
color: #999;
text-decoration: underline;
}
.back-text:active {
color: #007aff;
}
</style>

170
pages/shop/detail.vue Normal file
View File

@ -0,0 +1,170 @@
<template>
<scroll-view class="page" scroll-y>
<view class="banner" v-if="product.image">
<image class="banner-img" :src="product.image" mode="widthFix" />
<view class="banner-badge" v-if="requiredPoints > 0">{{ requiredPoints }}积分</view>
</view>
<view class="header">
<view class="title">{{ product.title || product.name || '-' }}</view>
<view class="meta" v-if="requiredPoints > 0">所需积分{{ requiredPoints }}</view>
<view class="meta" v-if="userPoints !== null">我的积分{{ userPoints }}</view>
</view>
<view class="desc" v-if="product.description">{{ product.description }}</view>
<view class="actions">
<view class="qty">
<text class="label">数量</text>
<view class="stepper">
<text class="btn" @tap="changeQty(-1)">-</text>
<text class="num">{{ quantity }}</text>
<text class="btn" @tap="changeQty(1)">+</text>
</view>
</view>
<button class="redeem-btn" type="primary" @tap="onRedeem" :disabled="redeeming || !canRedeem">积分兑换</button>
<view class="hint" v-if="requiredPoints > 0">共需积分{{ totalRequired }}</view>
</view>
</scroll-view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getProductDetail, getPointsBalance, redeemProductByPoints } from '@/api/appUser'
const productId = ref('')
const product = ref({})
const quantity = ref(1)
const userPoints = ref(null)
const redeeming = ref(false)
const requiredPoints = computed(() => {
const p = product.value || {}
const n = Number(p.price)
return isNaN(n) ? 0 : n
})
const totalRequired = computed(() => requiredPoints.value * quantity.value)
const canRedeem = computed(() => {
const up = Number(userPoints.value)
const need = Number(totalRequired.value)
return requiredPoints.value > 0 && !isNaN(up) && up >= need
})
function changeQty(delta) {
const next = quantity.value + delta
quantity.value = next < 1 ? 1 : next
}
async function fetchDetail(id) {
try {
const res = await getProductDetail(id)
const data = res && (res.data || res.item || res.product) ? (res.data || res.item || res.product) : res
const img = data && (data.main_image || data.image || data.img || data.pic || '')
const title = data && (data.title || data.name || data.product_name || '')
const price = data && (data.price ?? data.points_required ?? data.points ?? data.integral ?? null)
const description = data && (data.description ?? data.desc ?? data.rules ?? '')
const albumRaw = data && data.album
const album = normalizeAlbum(albumRaw)
const first = album[0] || img
product.value = { image: first, album, title, price, description, ...data }
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
}
}
async function fetchMyPoints() {
try {
const uid = uni.getStorageSync('user_id')
const b = await getPointsBalance(uid)
const balance = b && b.balance !== undefined ? b.balance : b
userPoints.value = Number(balance) || 0
} catch (e) { userPoints.value = 0 }
}
async function onRedeem() {
const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound')
if (!token || !phoneBound) {
uni.showModal({
title: '提示',
content: '请先登录并绑定手机号',
confirmText: '去登录',
success: (res) => { if (res.confirm) uni.navigateTo({ url: '/pages/login/index' }) }
})
return
}
const need = requiredPoints.value * quantity.value
if (!requiredPoints.value || need <= 0) {
uni.showToast({ title: '该商品未设置积分', icon: 'none' })
return
}
if (userPoints.value === null) {
await fetchMyPoints()
}
if (Number(userPoints.value) < Number(need)) {
uni.showToast({ title: '积分不足', icon: 'none' })
return
}
try {
redeeming.value = true
const uid = uni.getStorageSync('user_id')
await redeemProductByPoints(uid, productId.value, quantity.value)
uni.showToast({ title: '兑换成功', icon: 'success' })
try {
const b = await getPointsBalance(uid)
const balance = b && b.balance !== undefined ? b.balance : b
userPoints.value = Number(balance) || 0
} catch (_) {}
} catch (e) {
uni.showToast({ title: e && (e.message || e.errMsg) || '兑换失败', icon: 'none' })
} finally {
redeeming.value = false
}
}
onLoad((opts) => {
const id = (opts && (opts.id || opts.product_id)) || ''
productId.value = id
if (id) fetchDetail(id)
fetchMyPoints()
})
function normalizeAlbum(x) {
const result = []
try {
if (!x) return result
if (Array.isArray(x)) {
return x.map(s => String(s || '').trim()).filter(Boolean)
}
const s = String(x || '').trim()
if (!s) return result
if (s.startsWith('[') && s.endsWith(']')) {
const arr = JSON.parse(s)
if (Array.isArray(arr)) return arr.map(u => String(u || '').trim()).filter(Boolean)
}
if (s.includes(',')) {
return s.split(',').map(u => u.trim()).filter(Boolean)
}
return [s]
} catch (_) {
return result
}
}
</script>
<style scoped>
.page { min-height: 100vh; padding-bottom: 120rpx; background: #F8F8F8 }
.banner { position: relative; padding: 0 }
.banner-img { width: 100%; display: block }
.banner-badge { position: absolute; right: 16rpx; bottom: 16rpx; background: rgba(0,0,0,0.7); color: #fff; padding: 8rpx 16rpx; border-radius: 24rpx; font-size: 24rpx }
.header { padding: 24rpx; background: #fff }
.title { font-size: 36rpx; font-weight: 700; color: #333 }
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
.desc { background: #fff; padding: 0 24rpx 24rpx; font-size: 26rpx; color: #444 }
.actions { margin-top: 12rpx; padding: 24rpx; background: #fff }
.qty { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16rpx }
.label { font-size: 28rpx }
.stepper { display: flex; align-items: center; }
.btn { width: 56rpx; height: 56rpx; line-height: 56rpx; text-align: center; background: #f4f4f4; border-radius: 12rpx }
.num { width: 80rpx; text-align: center; font-size: 28rpx }
.redeem-btn { margin-top: 8rpx }
.hint { margin-top: 12rpx; font-size: 24rpx; color: #999 }
</style>

35
utils/assets.js Normal file
View File

@ -0,0 +1,35 @@
export async function ensureLocal(url) {
return new Promise((resolve) => {
try {
const cache = uni.getStorageSync('asset_cache_v1') || {}
const hit = cache[url]
if (hit && typeof hit === 'string' && hit) { resolve(hit); return }
uni.downloadFile({
url,
success: (res) => {
const tmp = res.tempFilePath || ''
if (!tmp) { resolve(url); return }
uni.saveFile({
tempFilePath: tmp,
success: (s) => {
const saved = s.savedFilePath || tmp
cache[url] = saved
uni.setStorageSync('asset_cache_v1', cache)
resolve(saved)
},
fail: () => { resolve(tmp) }
})
},
fail: () => { resolve(url) }
})
} catch (_) { resolve(url) }
})
}
export async function preloadAssets(urls) {
const tasks = urls.map(u => ensureLocal(u))
const results = await Promise.allSettled(tasks)
const map = {}
results.forEach((r, i) => { map[urls[i]] = r.status === 'fulfilled' ? r.value : urls[i] })
return map
}