This commit is contained in:
2025-09-24 20:35:15 +08:00
parent 39679f7330
commit 8a458ff0a4
12033 changed files with 1537546 additions and 13292 deletions

View File

@@ -0,0 +1,418 @@
<template>
<view class="login-container">
<!-- 背景装饰 -->
<view class="background-decoration">
<view class="circle circle-1"></view>
<view class="circle circle-2"></view>
<view class="circle circle-3"></view>
</view>
<!-- 主要内容卡片 -->
<view class="login-card">
<!-- 顶部Logo区域 -->
<view class="header-section">
<view class="logo-container">
<view class="logo-icon">
<svg viewBox="0 0 24 24" class="icon">
<path d="M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2ZM21 9V7L15 4V6C15 7.66 13.66 9 12 9S9 7.66 9 6V4L3 7V9C3 10.1 3.9 11 5 11V17C5 18.1 5.9 19 7 19H9C9 20.1 9.9 21 11 21H13C14.1 21 15 20.1 15 19H17C18.1 19 19 18.1 19 17V11C20.1 11 21 10.1 21 9Z"/>
</svg>
</view>
<text class="app-name">配件询价</text>
</view>
<text class="welcome-text">欢迎回来</text>
<text class="subtitle">请登录您的账户</text>
</view>
<!-- 表单区域 -->
<view class="form-section">
<view class="input-group">
<view class="input-container" :class="{ focused: phoneFocused, filled: form.phone }">
<view class="input-icon">
<svg viewBox="0 0 24 24" class="icon">
<path d="M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.25 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z"/>
</svg>
</view>
<input
class="input-field"
v-model.trim="form.phone"
type="number"
placeholder="请输入手机号"
maxlength="11"
@focus="phoneFocused = true"
@blur="phoneFocused = false"
/>
</view>
</view>
<view class="input-group">
<view class="input-container" :class="{ focused: passwordFocused, filled: form.password }">
<view class="input-icon">
<svg viewBox="0 0 24 24" class="icon">
<path d="M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z"/>
</svg>
</view>
<input
class="input-field"
v-model.trim="form.password"
password
placeholder="请输入密码"
@focus="passwordFocused = true"
@blur="passwordFocused = false"
/>
</view>
</view>
</view>
<!-- 按钮区域 -->
<view class="actions-section">
<button class="login-button" @click="onLogin">
<text class="button-text">登录</text>
</button>
<button class="register-button" @click="onGoRegister">
<text class="button-text">注册新账户</text>
</button>
</view>
<!-- 提示信息 -->
<view class="footer-section"></view>
</view>
</view>
</template>
<script>
import { post } from '../../common/http.js'
export default {
data() {
return {
form: { phone: '', password: '' },
phoneFocused: false,
passwordFocused: false
}
},
methods: {
validate() {
const p = String(this.form.phone||'').trim()
const okPhone = /^1[3-9]\d{9}$/.test(p)
if (!okPhone) { uni.showToast({ title: '请输入正确的手机号', icon: 'none' }); return false }
return true
},
async onLogin() {
if (!this.validate()) return
try {
const phone = String(this.form.phone||'').trim()
const password = String(this.form.password||'')
const res = await post('/api/auth/password/login', { phone, password })
// 统一存储 TOKEN 并跳转首页
if (res && res.token) {
uni.setStorageSync('TOKEN', res.token)
if (res.user && res.user.phone) uni.setStorageSync('USER_MOBILE', res.user.phone)
uni.showToast({ title: '登录成功', icon: 'none' })
setTimeout(() => { uni.reLaunch({ url: '/pages/index/index' }) }, 200)
}
} catch(e) {
uni.showToast({ title: (e && e.message) || '登录失败', icon: 'none' })
}
},
onGoRegister() {
uni.navigateTo({
url: '/pages/auth/register'
})
}
}
}
</script>
<style lang="scss">
// 主容器
.login-container {
position: relative;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40rpx 20rpx;
overflow: hidden;
}
// 背景装饰圆形
.background-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
.circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
&.circle-1 {
width: 200rpx;
height: 200rpx;
top: 10%;
left: 10%;
animation: float 6s ease-in-out infinite;
}
&.circle-2 {
width: 150rpx;
height: 150rpx;
top: 60%;
right: 15%;
animation: float 8s ease-in-out infinite reverse;
}
&.circle-3 {
width: 100rpx;
height: 100rpx;
bottom: 20%;
left: 20%;
animation: float 5s ease-in-out infinite;
}
}
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
// 主卡片容器
.login-card {
position: relative;
z-index: 1;
width: 90%;
max-width: 680rpx;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
border-radius: 32rpx;
padding: 60rpx 40rpx 50rpx;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.1);
border: 1rpx solid rgba(255, 255, 255, 0.2);
}
// 头部区域
.header-section {
text-align: center;
margin-bottom: 50rpx;
.logo-container {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
margin-bottom: 20rpx;
.logo-icon {
width: 60rpx;
height: 60rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
.icon {
width: 36rpx;
height: 36rpx;
fill: white;
}
}
.app-name {
font-size: 36rpx;
font-weight: 700;
color: #2d3748;
letter-spacing: 1rpx;
}
}
.welcome-text {
display: block;
font-size: 48rpx;
font-weight: 700;
color: #2d3748;
margin-bottom: 8rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
display: block;
font-size: 28rpx;
color: #718096;
font-weight: 400;
}
}
// 表单区域
.form-section {
margin-bottom: 40rpx;
.input-group {
margin-bottom: 28rpx;
.input-container {
position: relative;
background: #f7fafc;
border: 2rpx solid #e2e8f0;
border-radius: 16rpx;
display: flex;
align-items: center;
transition: all 0.3s ease;
&.focused {
border-color: #667eea;
background: #ffffff;
box-shadow: 0 0 0 6rpx rgba(102, 126, 234, 0.1);
transform: translateY(-2rpx);
}
&.filled {
background: #ffffff;
border-color: #cbd5e0;
}
.input-icon {
display: flex;
align-items: center;
justify-content: center;
width: 50rpx;
margin-left: 20rpx;
.icon {
width: 32rpx;
height: 32rpx;
fill: #a0aec0;
transition: fill 0.3s ease;
}
}
&.focused .input-icon .icon {
fill: #667eea;
}
.input-field {
flex: 1;
background: transparent;
border: none;
padding: 24rpx 20rpx 24rpx 12rpx;
font-size: 32rpx;
color: #2d3748;
&::placeholder {
color: #a0aec0;
font-size: 28rpx;
}
}
}
}
}
// 按钮区域
.actions-section {
margin-bottom: 30rpx;
.login-button {
width: 100%;
height: 96rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 16rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.3);
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.2) 0%, transparent 50%);
opacity: 0;
transition: opacity 0.3s ease;
}
&:active::before {
opacity: 1;
}
.button-text {
font-size: 32rpx;
font-weight: 600;
color: white;
letter-spacing: 1rpx;
}
}
.register-button {
width: 100%;
height: 86rpx;
background: transparent;
border: 2rpx solid #e2e8f0;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&:active {
background: #f7fafc;
border-color: #cbd5e0;
transform: translateY(1rpx);
}
.button-text {
font-size: 28rpx;
font-weight: 500;
color: #718096;
}
}
}
// 页脚区域
.footer-section {
text-align: center;
.hint-text {
font-size: 24rpx;
color: #a0aec0;
line-height: 1.5;
background: rgba(160, 174, 192, 0.1);
padding: 16rpx 20rpx;
border-radius: 12rpx;
border: 1rpx solid rgba(160, 174, 192, 0.2);
}
}
// 响应式设计
@media (max-width: 750rpx) {
.login-card {
margin: 20rpx;
padding: 50rpx 30rpx 40rpx;
}
.header-section .welcome-text {
font-size: 42rpx;
}
}
</style>

View File

@@ -0,0 +1,506 @@
<template>
<view class="register-container">
<!-- 背景装饰 -->
<view class="background-decoration">
<view class="circle circle-1"></view>
<view class="circle circle-2"></view>
<view class="circle circle-3"></view>
</view>
<!-- 主要内容卡片 -->
<view class="register-card">
<!-- 顶部Logo区域 -->
<view class="header-section">
<view class="logo-container">
<view class="logo-icon">
<svg viewBox="0 0 24 24" class="icon">
<path d="M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2ZM21 9V7L15 4V6C15 7.66 13.66 9 12 9S9 7.66 9 6V4L3 7V9C3 10.1 3.9 11 5 11V17C5 18.1 5.9 19 7 19H9C9 20.1 9.9 21 11 21H13C14.1 21 15 20.1 15 19H17C18.1 19 19 18.1 19 17V11C20.1 11 21 10.1 21 9Z"/>
</svg>
</view>
<text class="app-name">配件询价</text>
</view>
<text class="welcome-text">创建账户</text>
<text class="subtitle">请填写以下信息完成注册</text>
</view>
<!-- 表单区域 -->
<view class="form-section">
<!-- 店铺名称 -->
<view class="input-group">
<view class="input-container" :class="{ focused: shopNameFocused, filled: form.shopName }">
<view class="input-icon">
<svg viewBox="0 0 24 24" class="icon">
<path d="M12,3L2,12H5V20H19V12H22L12,3M12,8.75A2.25,2.25 0 0,1 14.25,11A2.25,2.25 0 0,1 12,13.25A2.25,2.25 0 0,1 9.75,11A2.25,2.25 0 0,1 12,8.75Z"/>
</svg>
</view>
<input
class="input-field"
v-model.trim="form.shopName"
type="text"
placeholder="请输入店铺名称"
@focus="shopNameFocused = true"
@blur="shopNameFocused = false"
/>
</view>
</view>
<!-- 姓名 -->
<view class="input-group">
<view class="input-container" :class="{ focused: nameFocused, filled: form.name }">
<view class="input-icon">
<svg viewBox="0 0 24 24" class="icon">
<path d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z"/>
</svg>
</view>
<input
class="input-field"
v-model.trim="form.name"
type="text"
placeholder="请输入您的姓名"
@focus="nameFocused = true"
@blur="nameFocused = false"
/>
</view>
</view>
<!-- 手机号 -->
<view class="input-group">
<view class="input-container" :class="{ focused: phoneFocused, filled: form.phone }">
<view class="input-icon">
<svg viewBox="0 0 24 24" class="icon">
<path d="M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.25 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z"/>
</svg>
</view>
<input
class="input-field"
v-model.trim="form.phone"
type="number"
placeholder="请输入手机号"
maxlength="11"
@focus="phoneFocused = true"
@blur="phoneFocused = false"
/>
</view>
</view>
<!-- 密码 -->
<view class="input-group">
<view class="input-container" :class="{ focused: passwordFocused, filled: form.password }">
<view class="input-icon">
<svg viewBox="0 0 24 24" class="icon">
<path d="M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z"/>
</svg>
</view>
<input
class="input-field"
v-model.trim="form.password"
password
placeholder="请输入密码至少6位"
@focus="passwordFocused = true"
@blur="passwordFocused = false"
/>
</view>
</view>
<!-- 确认密码 -->
<view class="input-group">
<view class="input-container" :class="{ focused: confirmPasswordFocused, filled: form.confirmPassword }">
<view class="input-icon">
<svg viewBox="0 0 24 24" class="icon">
<path d="M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z"/>
</svg>
</view>
<input
class="input-field"
v-model.trim="form.confirmPassword"
password
placeholder="请再次输入密码"
@focus="confirmPasswordFocused = true"
@blur="confirmPasswordFocused = false"
/>
</view>
</view>
</view>
<!-- 按钮区域 -->
<view class="actions-section">
<button class="register-button" @click="onRegister">
<text class="button-text">立即注册</text>
</button>
<button class="login-button" @click="onGoLogin">
<text class="button-text">已有账户去登录</text>
</button>
</view>
<!-- 提示信息 -->
<view class="footer-section">
<text class="hint-text">注册即表示您同意我们的服务条款和隐私政策</text>
</view>
</view>
</view>
</template>
<script>
import { post } from '../../common/http.js'
export default {
data() {
return {
form: {
shopName: '',
name: '',
phone: '',
password: '',
confirmPassword: ''
},
shopNameFocused: false,
nameFocused: false,
phoneFocused: false,
passwordFocused: false,
confirmPasswordFocused: false
}
},
methods: {
validate() {
const phone = String(this.form.phone || '').trim();
const ok = /^1[3-9]\d{9}$/.test(phone);
if (!ok) { uni.showToast({ title: '请输入正确的手机号', icon: 'none' }); return false }
if (!this.form.password) { uni.showToast({ title: '请输入密码', icon: 'none' }); return false }
if (this.form.password.length < 6) { uni.showToast({ title: '密码至少6位', icon: 'none' }); return false }
if (!this.form.confirmPassword) { uni.showToast({ title: '请确认密码', icon: 'none' }); return false }
if (this.form.password !== this.form.confirmPassword) { uni.showToast({ title: '两次密码不一致', icon: 'none' }); return false }
return true;
},
async onRegister() {
if (!this.validate()) return;
const phone = String(this.form.phone||'').trim();
const name = String(this.form.name||'').trim();
const password = String(this.form.password||'');
try {
const data = await post('/api/auth/register', { phone, name: name || undefined, password });
if (data && data.token) {
uni.setStorageSync('TOKEN', data.token)
if (data.user && data.user.phone) uni.setStorageSync('USER_MOBILE', data.user.phone)
uni.showToast({ title: '注册成功', icon: 'none' })
setTimeout(() => { uni.reLaunch({ url: '/pages/index/index' }) }, 300)
}
} catch(e) {
const msg = (e && e.message) || '注册失败'
uni.showToast({ title: msg, icon: 'none' })
}
},
onGoLogin() {
uni.navigateTo({
url: '/pages/auth/login'
});
}
}
}
</script>
<style lang="scss">
// 主容器
.register-container {
position: relative;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40rpx 20rpx;
overflow: hidden;
}
// 背景装饰圆形
.background-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
.circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
&.circle-1 {
width: 220rpx;
height: 220rpx;
top: 8%;
left: 12%;
animation: float 7s ease-in-out infinite;
}
&.circle-2 {
width: 180rpx;
height: 180rpx;
top: 65%;
right: 10%;
animation: float 9s ease-in-out infinite reverse;
}
&.circle-3 {
width: 120rpx;
height: 120rpx;
bottom: 15%;
left: 25%;
animation: float 6s ease-in-out infinite;
}
}
}
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-25px) rotate(5deg); }
}
// 主卡片容器
.register-card {
position: relative;
z-index: 1;
width: 90%;
max-width: 680rpx;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
border-radius: 32rpx;
padding: 50rpx 40rpx 45rpx;
box-shadow: 0 25rpx 70rpx rgba(0, 0, 0, 0.12);
border: 1rpx solid rgba(255, 255, 255, 0.3);
}
// 头部区域
.header-section {
text-align: center;
margin-bottom: 45rpx;
.logo-container {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
margin-bottom: 20rpx;
.logo-icon {
width: 60rpx;
height: 60rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
.icon {
width: 36rpx;
height: 36rpx;
fill: white;
}
}
.app-name {
font-size: 36rpx;
font-weight: 700;
color: #2d3748;
letter-spacing: 1rpx;
}
}
.welcome-text {
display: block;
font-size: 48rpx;
font-weight: 700;
color: #2d3748;
margin-bottom: 8rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
display: block;
font-size: 28rpx;
color: #718096;
font-weight: 400;
}
}
// 表单区域
.form-section {
margin-bottom: 40rpx;
.input-group {
margin-bottom: 24rpx;
.input-container {
position: relative;
background: #f7fafc;
border: 2rpx solid #e2e8f0;
border-radius: 16rpx;
display: flex;
align-items: center;
transition: all 0.3s ease;
&.focused {
border-color: #667eea;
background: #ffffff;
box-shadow: 0 0 0 6rpx rgba(102, 126, 234, 0.1);
transform: translateY(-2rpx);
}
&.filled {
background: #ffffff;
border-color: #cbd5e0;
}
.input-icon {
display: flex;
align-items: center;
justify-content: center;
width: 50rpx;
margin-left: 20rpx;
.icon {
width: 32rpx;
height: 32rpx;
fill: #a0aec0;
transition: fill 0.3s ease;
}
}
&.focused .input-icon .icon {
fill: #667eea;
}
.input-field {
flex: 1;
background: transparent;
border: none;
padding: 24rpx 20rpx 24rpx 12rpx;
font-size: 32rpx;
color: #2d3748;
&::placeholder {
color: #a0aec0;
font-size: 28rpx;
}
}
}
}
}
// 按钮区域
.actions-section {
margin-bottom: 30rpx;
.register-button {
width: 100%;
height: 96rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 16rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.3);
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.2) 0%, transparent 50%);
opacity: 0;
transition: opacity 0.3s ease;
}
&:active::before {
opacity: 1;
}
.button-text {
font-size: 32rpx;
font-weight: 600;
color: white;
letter-spacing: 1rpx;
}
}
.login-button {
width: 100%;
height: 86rpx;
background: transparent;
border: 2rpx solid #e2e8f0;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&:active {
background: #f7fafc;
border-color: #cbd5e0;
transform: translateY(1rpx);
}
.button-text {
font-size: 28rpx;
font-weight: 500;
color: #718096;
}
}
}
// 页脚区域
.footer-section {
text-align: center;
.hint-text {
display: block;
font-size: 24rpx;
color: #a0aec0;
line-height: 1.6;
margin-bottom: 12rpx;
}
.static-hint {
display: block;
font-size: 22rpx;
color: #a0aec0;
line-height: 1.5;
background: rgba(160, 174, 192, 0.1);
padding: 12rpx 16rpx;
border-radius: 10rpx;
border: 1rpx solid rgba(160, 174, 192, 0.2);
}
}
// 响应式设计
@media (max-width: 750rpx) {
.register-card {
margin: 20rpx;
padding: 40rpx 30rpx 35rpx;
}
.header-section .welcome-text {
font-size: 42rpx;
}
.form-section .input-group {
margin-bottom: 20rpx;
}
}
</style>

View File

@@ -1,13 +1,5 @@
<template>
<view class="page">
<!-- 顶部时间维度筛选 -->
<view class="seg">
<view :class="['seg-item', range==='custom' && 'active']" @click="switchRange('custom')">自定义</view>
<view :class="['seg-item', range==='week' && 'active']" @click="switchRange('week')">本周</view>
<view :class="['seg-item', range==='today' && 'active']" @click="switchRange('today')">今日</view>
<view :class="['seg-item', range==='month' && 'active']" @click="switchRange('month')">本月</view>
<view :class="['seg-item', range==='year' && 'active']" @click="switchRange('year')">本年</view>
</view>
<view class="page">
<!-- 业务类型侧边切换销售/进货/收款/资金/盘点 -->
<view class="content">
@@ -16,13 +8,24 @@
</view>
<view class="panel">
<!-- 搜索框与期间显示总额 -->
<!-- 期间选择 + 搜索框 + 查询按钮 -->
<view class="toolbar">
<view class="search">
<input class="search-input" v-model.trim="query.kw" :placeholder="placeholder" @confirm="reload" />
<view class="period-group">
<text class="period-label">期间</text>
<picker mode="date" :value="startDate" @change="onStartChange">
<view class="date-chip">{{ startDate }}</view>
</picker>
<text class="sep">~</text>
<picker mode="date" :value="endDate" @change="onEndChange">
<view class="date-chip">{{ endDate }}</view>
</picker>
</view>
<view class="search-row">
<view class="search">
<input class="search-input" v-model.trim="query.kw" :placeholder="placeholder" @confirm="reload" />
</view>
<button class="btn" size="mini" @click="reload">查询</button>
</view>
<view class="period">{{ periodLabel }}</view>
<button size="mini" @click="reload">查询</button>
</view>
<view class="total">合计¥{{ totalAmount.toFixed(2) }}</view>
@@ -89,9 +92,16 @@ export default {
totalAmount() { return this.items.reduce((s, it) => s + Number(it.amount || 0), 0) }
},
onLoad() {
try { console.log('[detail] onLoad route = pages/detail/index') } catch(e){}
this.computeRange()
this.reload()
const hasToken = (() => { try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } })()
if (!hasToken) {
this.items = []
this.total = 0
uni.showToast({ title: '请登录使用该功能', icon: 'none' })
return
}
try { console.log('[detail] onLoad route = pages/detail/index') } catch(e){}
this.computeRange()
this.reload()
},
methods: {
switchBiz(k) { if (this.biz === k) return; this.biz = k; this.reload() },
@@ -108,7 +118,9 @@ export default {
else { start = new Date(now.getFullYear(), now.getMonth(), 1); end = new Date(now.getFullYear(), now.getMonth() + 1, 0) }
this.startDate = fmt(start); this.endDate = fmt(end)
},
reload() { this.items = []; this.page = 1; this.finished = false; this.loadMore() },
reload() { this.items = []; this.page = 1; this.finished = false; this.loadMore() },
onStartChange(e) { this.startDate = e?.detail?.value || this.startDate; if (this.endDate && this.startDate > this.endDate) this.endDate = this.startDate; this.reload() },
onEndChange(e) { this.endDate = e?.detail?.value || this.endDate; if (this.startDate && this.endDate < this.startDate) this.startDate = this.endDate; this.reload() },
async loadMore() {
if (this.loading || this.finished) return
this.loading = true
@@ -134,9 +146,8 @@ export default {
<style lang="scss">
.page { display:flex; flex-direction: column; height: 100vh; }
.seg { display:flex; background:#fff; border-bottom:2rpx solid $uni-border-color; }
.seg-item { flex:1; padding: 18rpx 0; text-align:center; color:$uni-text-color-grey; }
.seg-item.active { color:#fff; background:$uni-color-primary; border-radius: 12rpx; margin: 8rpx; }
/* 顶部分段(如需保留,可以隐藏或后续扩展) */
.seg { display:none; }
.content { display:flex; flex:1; min-height: 0; }
.biz-tabs { width: 140rpx; background:#fff; border-right:2rpx solid $uni-border-color; display:flex; flex-direction: column; }
@@ -144,10 +155,15 @@ export default {
.biz.active { background:rgba(76,141,255,0.10); color:$uni-color-primary; font-weight:700; }
.panel { flex:1; display:flex; flex-direction: column; background:#fff; margin: 16rpx; border-radius: 16rpx; padding: 12rpx; border:2rpx solid $uni-border-color; }
.toolbar { display:flex; align-items: center; gap: 12rpx; padding: 8rpx 6rpx; border-bottom:2rpx solid $uni-border-color; }
.toolbar { display:flex; flex-direction: column; gap: 10rpx; padding: 10rpx 6rpx 6rpx; border-bottom:2rpx solid $uni-border-color; }
.period-group { display:flex; align-items:center; gap: 8rpx; background:#f6f8fb; border:2rpx solid #e6ebf2; border-radius: 10rpx; padding: 8rpx 10rpx; }
.period-label { color:#6b778c; }
.date-chip { padding: 8rpx 12rpx; background:#fff; border:2rpx solid #e6ebf2; border-radius: 8rpx; }
.sep { color:#99a2b3; padding: 0 6rpx; }
.search-row { display:flex; align-items:center; gap: 10rpx; }
.search { flex:1; }
.search-input { width:100%; background:$uni-bg-color-hover; border-radius: 12rpx; padding: 12rpx; color:$uni-text-color; }
.period { color:$uni-text-color-grey; font-size: 24rpx; padding: 0 6rpx; }
.search-input { width:100%; background:#fff; border-radius: 12rpx; padding: 12rpx; color:$uni-text-color; border:2rpx solid #e6ebf2; }
.btn { background:$uni-color-primary; color:#fff; border: none; }
.total { color:$uni-color-primary; font-weight: 700; padding: 10rpx 6rpx 12rpx; background:#fff; }
.list { flex:1; }
.item { display:grid; grid-template-columns: 1fr auto auto; align-items:center; gap: 8rpx; padding: 18rpx 12rpx; border-bottom: 1rpx solid $uni-border-color; }

View File

@@ -23,22 +23,34 @@
<text class="cta-text">咨询</text>
</view>
</view>
<view class="kpi">
<view class="kpi-item">
<text class="kpi-label">今日销售额</text>
<text class="kpi-value">{{ kpi.todaySales }}</text>
<view class="kpi kpi-grid">
<view class="kpi-item kpi-card">
<image :src="KPI_ICONS.todaySales" class="kpi-icon" mode="aspectFit"></image>
<view class="kpi-content">
<text class="kpi-label">今日销售额</text>
<text class="kpi-value">{{ kpi.todaySales }}</text>
</view>
</view>
<view class="kpi-item">
<text class="kpi-label">本月销售额</text>
<text class="kpi-value">{{ kpi.monthSales }}</text>
<view class="kpi-item kpi-card">
<image :src="KPI_ICONS.monthSales" class="kpi-icon" mode="aspectFit"></image>
<view class="kpi-content">
<text class="kpi-label">本月销售额</text>
<text class="kpi-value">{{ kpi.monthSales }}</text>
</view>
</view>
<view class="kpi-item">
<text class="kpi-label">本月利润</text>
<text class="kpi-value">{{ kpi.monthProfit }}</text>
<view class="kpi-item kpi-card">
<image :src="KPI_ICONS.monthProfit" class="kpi-icon" mode="aspectFit"></image>
<view class="kpi-content">
<text class="kpi-label">本月利润</text>
<text class="kpi-value">{{ kpi.monthProfit }}</text>
</view>
</view>
<view class="kpi-item">
<text class="kpi-label">库存商品数量</text>
<text class="kpi-value">{{ kpi.stockCount }}</text>
<view class="kpi-item kpi-card">
<image :src="KPI_ICONS.stockCount" class="kpi-icon" mode="aspectFit"></image>
<view class="kpi-content">
<text class="kpi-label">库存商品数量</text>
<text class="kpi-value">{{ kpi.stockCount }}</text>
</view>
</view>
</view>
</view>
@@ -51,7 +63,7 @@
</view>
<!-- 功能九宫格玻璃容器 + 圆角方形图标 -->
<view class="grid-wrap">
<view class="grid-wrap">
<view class="feature-grid">
<view class="feature-card" v-for="item in features" :key="item.key" @click="onFeatureTap(item)">
<view class="fc-icon">
@@ -71,9 +83,11 @@
<script>
import { get } from '../../common/http.js'
import { ROUTES } from '../../common/constants.js'
import { KPI_ICONS as KPI_ICON_MAP } from '../../common/config.js'
export default {
data() {
return {
return {
KPI_ICONS: KPI_ICON_MAP,
kpi: { todaySales: '0.00', monthSales: '0.00', monthProfit: '0.00', stockCount: '0' },
activeTab: 'home',
notices: [],
@@ -93,10 +107,17 @@
]
}
},
onLoad() {
this.fetchMetrics()
this.fetchNotices()
},
onLoad() {
const hasToken = (() => { try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } })()
if (!hasToken) {
this.kpi = { todaySales: '0.00', monthSales: '0.00', monthProfit: '0.00', stockCount: '0' }
this.notices = []
uni.showToast({ title: '请登录使用该功能', icon: 'none' })
return
}
this.fetchMetrics()
this.fetchNotices()
},
methods: {
async fetchMetrics() {
try {
@@ -197,12 +218,14 @@
.home {
padding-bottom: 140rpx;
position: relative;
/* 纯白背景 */
background: #ffffff;
/* 渐变背景:顶部淡蓝过渡到白色 */
background: linear-gradient(180deg, #f8fbff 0%, #ffffff 60%);
min-height: 100vh;
}
/* 首页横幅(移除) */
/* 公告栏 */
.notice {
@@ -239,9 +262,9 @@
/* 顶部英雄区:浅色玻璃卡片,带金色描边与柔和阴影 */
.hero {
margin: 24rpx;
padding: 32rpx;
border-radius: 28rpx;
margin: 16rpx 20rpx;
padding: 18rpx;
border-radius: 20rpx;
background: #ffffff;
border: 2rpx solid $uni-border-color;
box-shadow: none;
@@ -254,16 +277,18 @@
align-items: center;
margin-bottom: 20rpx;
}
.hero-sub { display:flex; gap: 12rpx; margin-bottom: 22rpx; flex-wrap: wrap; }
.chip { padding: 8rpx 16rpx; border-radius: 999rpx; background: rgba(76,141,255,0.10); color: $uni-color-primary; font-size: 24rpx; font-weight: 700; border: 2rpx solid rgba(76,141,255,0.25); }
.brand {
font-size: 42rpx;
font-size: 36rpx;
font-weight: 700;
letter-spacing: 2rpx;
letter-spacing: 1rpx;
color: $uni-color-primary;
}
.cta {
padding: 10rpx 22rpx;
padding: 8rpx 18rpx;
border-radius: 999rpx;
background: $uni-color-primary;
border: 2rpx solid $uni-color-primary;
@@ -275,18 +300,14 @@
/* KPI 卡片化布局2×2 */
.kpi { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16rpx; }
.kpi-item { text-align: center; background:#ffffff; border: 2rpx solid $uni-border-color; border-radius: 16rpx; padding: 16rpx 8rpx; }
.kpi-label {
opacity: 0.9;
font-size: 26rpx;
color: $uni-text-color-grey;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
line-height: 32rpx; /* 行高>=字体,避免上沿被裁切 */
min-height: 64rpx; /* 两行高度,防止折行挤压 */
}
.kpi-value { display: block; margin-top: 8rpx; font-size: 56rpx; font-weight: 800; color: $uni-color-primary; }
/* KPI 卡片(更扁平,降低高度) */
.kpi-grid { grid-template-columns: repeat(2, 1fr); gap: 16rpx; }
.kpi-card { display:flex; align-items:center; gap: 12rpx; text-align:left; padding: 12rpx 14rpx; border-radius: 12rpx; background:#fff; border:2rpx solid #eef2f6; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); }
.kpi-icon { width: 44rpx; height: 44rpx; opacity: .9; }
.kpi-content { display:flex; flex-direction:column; }
.kpi-label { color:#6b778c; font-weight:700; font-size: 24rpx; line-height: 30rpx; }
.kpi-value { color:#4C8DFF; font-size: 36rpx; line-height: 40rpx; margin-top: 0; font-weight: 800; }
/* 常用功能:胶囊+阴影卡片样式的图标栅格(旧风格保留以防回退) */
@@ -295,24 +316,23 @@
.icon-squircle { width: 140rpx; height: 140rpx; border-radius: 28rpx; background: #fff; border: 2rpx solid $uni-border-color; box-shadow: 0 10rpx 20rpx rgba(0,0,0,0.06); }
.grid-chip { margin-top: 12rpx; padding: 6rpx 14rpx; border-radius: 999rpx; background: rgba(76,141,255,0.12); color: $uni-color-primary; font-size: 26rpx; font-weight: 700; }
/* 功能容器:整体玻璃面板,增强融入感 */
/* 功能容器:更轻的留白 */
.grid-wrap {
margin: 0 20rpx 32rpx;
padding: 28rpx 20rpx 12rpx;
border-radius: 24rpx;
background: #ffffff;
border: 2rpx solid $uni-border-color;
box-shadow: none;
margin: 8rpx 12rpx 24rpx;
padding: 8rpx 8rpx 0;
border-radius: 20rpx;
background: transparent;
border: 0;
}
/* 新版功能卡片宫格(更现代卡片风 */
.feature-grid { display:grid; grid-template-columns: repeat(2, 1fr); gap: 18rpx; padding: 18rpx 18rpx 24rpx; }
.feature-card { background:#fff; border:2rpx solid $uni-border-color; border-radius: 16rpx; box-shadow: 0 10rpx 20rpx rgba(0,0,0,0.06); padding: 18rpx; display:flex; align-items:center; gap: 12rpx; }
.fc-icon { width: 96rpx; height: 96rpx; border-radius: 16rpx; background: $uni-bg-color-hover; display:flex; align-items:center; justify-content:center; }
.fc-img { width: 72rpx; height: 72rpx; }
.fc-emoji { font-size: 56rpx; }
.fc-placeholder { width: 72rpx; height: 72rpx; border-radius: 12rpx; background: $uni-bg-color-hover; border: 2rpx solid $uni-border-color; }
.fc-title { margin-left: 2rpx; font-size: 30rpx; font-weight: 700; color: $uni-text-color; }
/* 功能卡片宫格:方形竖排,图标在上文字在下(与截图一致 */
.feature-grid { display:grid; grid-template-columns: repeat(3, 1fr); gap: 14rpx; padding: 8rpx 8rpx 18rpx; }
.feature-card { height: 164rpx; background:#fff; border:2rpx solid #eef2f6; border-radius: 16rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.04); padding: 12rpx; display:flex; flex-direction: column; align-items:center; justify-content:center; }
.fc-icon { width: 86rpx; height: 86rpx; border-radius: 18rpx; background: #f7faff; border:2rpx solid #e8eef8; display:flex; align-items:center; justify-content:center; }
.fc-img { width: 56rpx; height: 56rpx; opacity: .95; }
.fc-emoji { font-size: 48rpx; }
.fc-placeholder { width: 56rpx; height: 56rpx; border-radius: 12rpx; background: $uni-bg-color-hover; border: 2rpx solid #e8eef8; }
.fc-title { margin-top: 10rpx; font-size: 26rpx; font-weight: 700; color: $uni-text-color; }
/* 底部操作条:浅色半透明 + 金色主按钮 */
.bottom-bar {

View File

@@ -1,5 +1,10 @@
<template>
<view class="me">
<view v-if="!isLoggedIn" class="card login">
<view class="login-title">登录/注册以同步数据</view>
<button class="login-btn" type="primary" @click="goLogin">登录</button>
<button class="login-btn minor" @click="goRegister">注册</button>
</view>
<view class="card user">
<image class="avatar" :src="avatarUrl" mode="aspectFill" @error="onAvatarError" />
<view class="meta">
@@ -9,6 +14,24 @@
</view>
</view>
<!-- VIP 卡片置于会员与订单分组上方 -->
<view class="card vip" :class="{ active: vipIsVip }">
<view class="vip-row">
<text class="vip-badge">{{ vipIsVip ? 'VIP' : '非VIP' }}</text>
<text class="vip-title">会员状态</text>
</view>
<view class="vip-meta">
<view class="item">
<text class="label">开始</text>
<text class="value">{{ vipStartDisplay }}</text>
</view>
<view class="item">
<text class="label">结束</text>
<text class="value">{{ vipEndDisplay }}</text>
</view>
</view>
</view>
<view class="group">
<view class="group-title">会员与订单</view>
<view class="cell" @click="goVip">
@@ -61,9 +84,9 @@
<text>关于与协议</text>
<text class="arrow"></text>
</view>
<view class="cell danger" @click="logout">
<text>退出登录</text>
</view>
<view v-if="isLoggedIn" class="cell danger" @click="logout">
<text>退出登录</text>
</view>
</view>
</view>
</template>
@@ -75,24 +98,47 @@ export default {
data() {
return {
avatarUrl: '/static/logo.png',
shopName: '我的店铺',
mobile: ''
shopName: '未登录',
mobile: '',
pendingJsCode: '',
logging: false,
vipIsVip: false,
vipStart: '',
vipEnd: ''
}
},
onLoad() {
this.fetchProfile()
onShow() {
this.fetchProfile()
this.loadVipFromStorage()
try {
if (uni.getStorageSync('TOKEN')) {
// 已登录时刷新资料并隐藏登录卡片
this.$forceUpdate && this.$forceUpdate()
}
} catch(e) {}
},
computed: {
isLoggedIn() { try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } },
mobileDisplay() {
const m = String(this.mobile || '')
return m.length === 11 ? m.slice(0,3) + '****' + m.slice(7) : (m || '未绑定手机号')
}
},
vipStartDisplay() { return this.formatDisplay(this.vipStart) },
vipEndDisplay() { return this.formatDisplay(this.vipEnd) }
},
methods: {
// 登录相关方法已移除
async fetchProfile() {
// 后端暂无专门店铺/用户信息接口,先使用概览接口作为在线性检测与占位数据来源
// 未登录则不触发任何用户/店铺接口,也不加载本地用户字段
const hasToken = (() => { try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } })()
if (!hasToken) {
this.shopName = '未登录'
this.avatarUrl = '/static/logo.png'
this.mobile = ''
return
}
// 已登录:拉取概览以确认在线状态,并回填本地用户信息
try { await get('/api/dashboard/overview') } catch(e) {}
// 读取本地可能保存的店铺名与头像
try {
const storeName = uni.getStorageSync('SHOP_NAME') || ''
const avatar = uni.getStorageSync('USER_AVATAR') || ''
@@ -102,10 +148,69 @@ export default {
this.mobile = phone
} catch(e) {}
},
loadVipFromStorage() {
try {
const isVip = String(uni.getStorageSync('USER_VIP_IS_VIP') || 'false').toLowerCase() === 'true'
const start = uni.getStorageSync('USER_VIP_START') || ''
const end = uni.getStorageSync('USER_VIP_END') || ''
this.vipIsVip = isVip
this.vipStart = start
this.vipEnd = end
} catch(e) {}
},
formatDisplay(value) {
if (!value) return '-'
const s = String(value)
// 简单规范化:只保留到分钟
const m = s.match(/^(\d{4}-\d{2}-\d{2})([ T](\d{2}:\d{2}))/)
if (m) return `${m[1]} ${m[3]}`
return s
},
startLogin() {
if (this.logging) return
this.logging = true
const tryOnce = async () => ({})
uni.login({ provider: 'weixin', success: async (res) => {
this.pendingJsCode = res.code || ''
if (!this.pendingJsCode) { this.logging = false; return uni.showToast({ title: '获取登录code失败', icon: 'none' }) }
try {
await tryOnce()
} catch(e) {
const msg = (e && e.message) || ''
if (msg.includes('40163') || msg.toLowerCase().includes('been used')) {
// 40163换新 code 再试一次
uni.login({ provider: 'weixin', success: async (r2) => {
const fresh = r2.code || ''
if (!fresh) { this.logging = false; return }
try {
await tryOnce()
} finally { this.logging = false }
} })
return
}
} finally {
this.logging = false
}
}, fail: () => { this.logging = false; uni.showToast({ title: '微信登录失败', icon: 'none' }) } })
},
goLogin(){ uni.navigateTo({ url: '/pages/auth/login' }) },
goRegister(){ uni.navigateTo({ url: '/pages/auth/register' }) },
onGetPhoneNumber(e) {
if (this.logging) return
this.logging = true
const phoneCode = ''
// 为避免 40163code been used此处重新获取一次 jsCode
uni.login({ provider: 'weixin', success: (res) => {
const jsCode = res.code || ''
if (!jsCode) { this.logging = false; return uni.showToast({ title: '获取登录code失败', icon: 'none' }) }
Promise.resolve().finally(() => { this.logging = false })
}, fail: () => { this.logging = false; uni.showToast({ title: '微信登录失败', icon: 'none' }) } })
},
goSmsLogin(){ uni.navigateTo({ url: '/pages/my/sms-login' }) },
onAvatarError() {
this.avatarUrl = '/static/logo.png'
},
goVip() { uni.showToast({ title: 'VIP会员开发中', icon: 'none' }) },
goVip() { uni.navigateTo({ url: '/pages/my/vip' }) },
goMyOrders() { uni.switchTab({ url: '/pages/detail/index' }) },
goSupplier() { uni.navigateTo({ url: '/pages/supplier/select' }) },
goCustomer() { uni.navigateTo({ url: '/pages/customer/select' }) },
@@ -116,17 +221,19 @@ export default {
goSystemParams() { uni.showToast({ title: '系统参数(开发中)', icon: 'none' }) },
goAbout() { uni.navigateTo({ url: '/pages/my/about' }) },
logout() {
try {
try {
uni.removeStorageSync('TOKEN')
uni.removeStorageSync('USER_AVATAR')
uni.removeStorageSync('USER_NAME')
uni.removeStorageSync('USER_MOBILE')
uni.removeStorageSync('SHOP_NAME')
uni.showToast({ title: '已退出', icon: 'none' })
setTimeout(() => { uni.reLaunch({ url: '/pages/index/index' }) }, 300)
} catch(e) {
uni.reLaunch({ url: '/pages/index/index' })
}
uni.removeStorageSync('LOGINED')
uni.removeStorageSync('LOGIN_PHONE')
uni.removeStorageSync('DEFAULT_USER_ID')
uni.setStorageSync('ENABLE_DEFAULT_USER', 'false')
uni.removeStorageSync('USER_AVATAR')
uni.removeStorageSync('USER_NAME')
uni.removeStorageSync('USER_MOBILE')
uni.removeStorageSync('SHOP_NAME')
uni.showToast({ title: '已清理本地信息', icon: 'none' })
setTimeout(() => { uni.reLaunch({ url: '/pages/index/index' }) }, 300)
} catch(e) { uni.reLaunch({ url: '/pages/index/index' }) }
}
}
}
@@ -134,6 +241,11 @@ export default {
<style lang="scss">
.me { padding: 24rpx; }
.card.login { display: flex; flex-direction: column; gap: 16rpx; padding: 22rpx; background: $uni-bg-color-grey; border-radius: 16rpx; margin-bottom: 24rpx; }
.login-title { font-size: 28rpx; font-weight: 700; }
.login-btn { }
.login-btn.minor { background: $uni-bg-color-hover; color: $uni-text-color; }
.hint { font-size: 22rpx; color: $uni-text-color-grey; }
.card.user { display: flex; gap: 18rpx; padding: 22rpx; background: $uni-bg-color-grey; border-radius: 16rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.16); align-items: center; }
.avatar { width: 120rpx; height: 120rpx; border-radius: 60rpx; background: $uni-bg-color-hover; }
.meta { display: flex; flex-direction: column; gap: 6rpx; }
@@ -141,12 +253,33 @@ export default {
.phone { font-size: 26rpx; color: $uni-text-color-grey; }
.role { font-size: 22rpx; color: $uni-text-color-grey; }
/* VIP 卡片样式 */
.card.vip { margin-top: 24rpx; padding: 22rpx; background: $uni-bg-color-grey; border-radius: 16rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.12); }
.card.vip.active { border: 1rpx solid rgba(255, 208, 0, 0.6); background-image: radial-gradient(60% 60% at 80% 0%, rgba(255, 214, 0, 0.08), transparent 60%); }
.vip-row { display: flex; align-items: center; gap: 12rpx; margin-bottom: 10rpx; }
.vip-badge { background: #f1c40f; color: #111; font-weight: 800; padding: 2rpx 10rpx; border-radius: 8rpx; font-size: 22rpx; }
.vip-title { font-size: 28rpx; font-weight: 700; color: $uni-text-color; }
.vip-meta { display: grid; grid-template-columns: 1fr 1fr; gap: 8rpx 16rpx; }
.vip-meta .item { display: flex; align-items: center; gap: 10rpx; }
.vip-meta .label { width: 80rpx; color: $uni-text-color-grey; font-size: 24rpx; }
.vip-meta .value { color: $uni-text-color; font-size: 26rpx; word-break: break-all; }
.group { margin-top: 24rpx; background: $uni-bg-color-grey; border-radius: 16rpx; overflow: hidden; }
.group-title { padding: 18rpx 22rpx; font-size: 26rpx; color: $uni-text-color-grey; background: $uni-bg-color-hover; }
.cell { display: flex; align-items: center; padding: 26rpx 22rpx; border-top: 1rpx solid $uni-border-color; color: $uni-text-color; }
.cell .desc { margin-left: auto; margin-right: 8rpx; font-size: 22rpx; color: $uni-text-color-grey; }
.cell .arrow { margin-left: auto; color: #99a2b3; }
.cell.danger { color: #dd524d; justify-content: center; font-weight: 700; }
/* 简易对话框样式 */
.dialog-mask { position: fixed; left: 0; right: 0; top: 0; bottom: 0; background: rgba(0,0,0,0.45); display: flex; align-items: center; justify-content: center; z-index: 999; }
.dialog { width: 600rpx; background: #fff; border-radius: 16rpx; padding: 24rpx; box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.2); }
.dialog-title { font-size: 30rpx; font-weight: 700; margin-bottom: 16rpx; }
.dialog-input { width: 100%; height: 72rpx; padding: 0 16rpx; border: 1rpx solid $uni-border-color; border-radius: 10rpx; background: #fff; color: $uni-text-color; }
.dialog-actions { display: flex; gap: 16rpx; margin-top: 18rpx; justify-content: flex-end; }
.dialog-btn { padding: 16rpx 22rpx; border-radius: 10rpx; }
.dialog-btn.cancel { background: $uni-bg-color-hover; color: $uni-text-color; }
.dialog-btn.confirm { background: #2979ff; color: #fff; }
</style>

View File

@@ -0,0 +1,144 @@
<template>
<view class="page sms-login">
<view class="card">
<view class="title">短信验证码登录</view>
<view class="form">
<input class="input" type="number" maxlength="11" placeholder="请输入手机号" v-model="phone"/>
<view class="row">
<input class="input code" type="number" maxlength="6" placeholder="请输入验证码" v-model="code"/>
<button class="send" :disabled="countdown>0 || sending" @click="sendCode">{{ btnText }}</button>
</view>
<button class="login" type="primary" :disabled="logging" @click="doLogin">登录/注册</button>
<button class="login" :disabled="logging" @click="quickRegister">注册为店主</button>
</view>
<view class="hint">首次登录将自动创建店铺与店主用户</view>
<view class="debug">
<view class="debug-title" @click="toggleDebug">请求体示例点击{{ showDebug? '收起':'展开' }}</view>
<view v-if="showDebug" class="debug-body">
<view class="code-title">POST /api/auth/sms/send</view>
<view class="code-wrap">
<text class="code">{{ sendBodyJson }}</text>
<button size="mini" class="copy" @click="copy(sendBodyJson)">复制</button>
</view>
<view class="code-title">POST /api/auth/sms/login</view>
<view class="code-wrap">
<text class="code">{{ loginBodyJson }}</text>
<button size="mini" class="copy" @click="copy(loginBodyJson)">复制</button>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { post } from '../../common/http.js'
export default {
data(){
return { phone: '', code: '', countdown: 0, timer: null, sending: false, logging: false, showDebug: true }
},
computed:{
btnText(){ return this.countdown>0 ? `${this.countdown}s` : (this.sending ? '发送中...' : '获取验证码') }
,
trimmedPhone(){ return String(this.phone||'').trim() },
sendBodyJson(){ return JSON.stringify({ phone: this.trimmedPhone, scene: 'login' }, null, 2) },
loginBodyJson(){ return JSON.stringify({ phone: this.trimmedPhone, code: String(this.code||'').trim() }, null, 2) }
},
onUnload(){ if (this.timer) clearInterval(this.timer) },
methods:{
validatePhone(p){ return /^1\d{10}$/.test(String(p||'').trim()) },
startCountdown(sec){
this.countdown = sec
if (this.timer) clearInterval(this.timer)
this.timer = setInterval(() => {
if (this.countdown<=1) { clearInterval(this.timer); this.timer=null; this.countdown=0; return }
this.countdown--
}, 1000)
},
async sendCode(){
if (this.sending || this.countdown>0) return
const p = String(this.phone||'').trim()
if (!this.validatePhone(p)) return uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
this.sending = true
try {
const res = await post('/api/auth/sms/send', { phone: p, scene: 'login' })
const cd = Number(res && res.cooldownSec || 60)
this.startCountdown(cd)
uni.showToast({ title: '验证码已发送', icon: 'none' })
} catch(e) {
const msg = (e && e.message) || '发送失败'
uni.showToast({ title: msg, icon: 'none' })
} finally { this.sending=false }
},
async doLogin(){
if (this.logging) return
const p = String(this.phone||'').trim()
const c = String(this.code||'').trim()
if (!this.validatePhone(p)) return uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
if (!/^\d{6}$/.test(c)) return uni.showToast({ title: '验证码格式不正确', icon: 'none' })
this.logging = true
try {
const data = await post('/api/auth/sms/login', { phone: p, code: c })
if (data && data.token) {
uni.setStorageSync('TOKEN', data.token)
if (data.user && data.user.phone) uni.setStorageSync('USER_MOBILE', data.user.phone)
uni.showToast({ title: '登录成功', icon: 'none' })
setTimeout(() => { uni.reLaunch({ url: '/pages/index/index' }) }, 300)
}
} catch(e) {
const msg = (e && e.message) || '登录失败'
uni.showToast({ title: msg, icon: 'none' })
} finally { this.logging=false }
}
,
async quickRegister(){
if (this.logging) return
const p = String(this.phone||'').trim()
if (!this.validatePhone(p)) return uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
this.logging = true
try {
const data = await post('/api/auth/register', { phone: p })
if (data && data.token) {
uni.setStorageSync('TOKEN', data.token)
if (data.user && data.user.phone) uni.setStorageSync('USER_MOBILE', data.user.phone)
uni.showToast({ title: '注册成功', icon: 'none' })
setTimeout(() => { uni.reLaunch({ url: '/pages/index/index' }) }, 300)
}
} catch(e) {
const msg = (e && e.message) || '注册失败'
uni.showToast({ title: msg, icon: 'none' })
} finally { this.logging=false }
}
,
copy(text){
try { uni.setClipboardData({ data: String(text||'') }); uni.showToast({ title: '已复制', icon: 'none' }) } catch(e) {}
}
,
toggleDebug(){ this.showDebug = !this.showDebug }
}
}
</script>
<style lang="scss">
.sms-login { padding: 24rpx; }
.card { background: $uni-bg-color-grey; border-radius: 16rpx; padding: 28rpx; }
.title { font-size: 32rpx; font-weight: 700; margin-bottom: 20rpx; }
.form { display: flex; flex-direction: column; gap: 16rpx; }
.row { display: flex; gap: 12rpx; }
.input { background: #fff; border: 1rpx solid $uni-border-color; border-radius: 12rpx; padding: 20rpx; font-size: 28rpx; flex: 1; }
.input.code { flex: 1; }
.send { min-width: 220rpx; }
.login { margin-top: 8rpx; }
.hint { margin-top: 12rpx; font-size: 22rpx; color: $uni-text-color-grey; }
.debug { margin-top: 20rpx; }
.debug-title { font-size: 26rpx; color: $uni-text-color-grey; }
.debug-body { margin-top: 12rpx; display: flex; flex-direction: column; gap: 12rpx; }
.code-title { font-size: 24rpx; color: $uni-text-color-grey; }
.code-wrap { position: relative; background: #fff; border: 1rpx solid $uni-border-color; border-radius: 12rpx; padding: 16rpx; }
.code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; font-size: 24rpx; white-space: pre-wrap; }
.copy { position: absolute; top: 8rpx; right: 8rpx; }
</style>

369
frontend/pages/my/vip.vue Normal file
View File

@@ -0,0 +1,369 @@
<template>
<view class="vip-page" style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); min-height: 100vh;">
<!-- 主要内容区域 -->
<view class="main-content">
<!-- VIP状态头部 -->
<view class="vip-header">
<view class="vip-crown">
<text class="crown-icon">👑</text>
</view>
<text class="vip-title">{{ isVip ? 'VIP会员' : '成为VIP会员' }}</text>
<text class="vip-subtitle">{{ isVip ? '尊享专属特权' : '解锁更多权益' }}</text>
<view class="vip-status" :class="{ active: isVip }">
<text class="status-text">{{ isVip ? 'VIP会员' : '普通用户' }}</text>
</view>
</view>
<!-- 会员功能 -->
<view class="features-section">
<text class="section-title">会员功能</text>
<view class="feature-card">
<view class="feature-icon">💾</view>
<text class="feature-text">永久存储数据</text>
</view>
</view>
<!-- VIP状态信息 -->
<view v-if="isVip" class="vip-info">
<view class="info-card">
<text class="info-label">会员状态</text>
<text class="info-value active">已激活</text>
</view>
<view class="info-card">
<text class="info-label">有效期至</text>
<text class="info-value">{{ expireDisplay }}</text>
</view>
</view>
<!-- 价格和购买 -->
<view v-if="!isVip" class="purchase-section">
<view class="price-card">
<text class="price-label">会员价格</text>
<view class="price-display">
<text class="price-symbol">¥</text>
<text class="price-amount">{{ price }}</text>
<text class="price-period">/</text>
</view>
</view>
<button class="purchase-btn" @click="onPay">
<text class="btn-text">立即开通VIP</text>
</button>
</view>
</view>
<!-- 背景装饰 -->
<view class="bg-decoration">
<view class="decoration-circle circle-1"></view>
<view class="decoration-circle circle-2"></view>
<view class="decoration-circle circle-3"></view>
</view>
</view>
</template>
<script>
import { VIP_PRICE_PER_MONTH } from '../../common/config.js'
export default {
data(){
return {
isVip: false,
expire: '',
price: VIP_PRICE_PER_MONTH
}
},
onShow(){
this.loadVip()
},
computed: {
expireDisplay(){
const s = String(this.expire || '')
return s || '11年11月11日'
}
},
methods: {
loadVip(){
try {
this.isVip = String(uni.getStorageSync('USER_VIP_IS_VIP') || 'false').toLowerCase() === 'true'
this.expire = uni.getStorageSync('USER_VIP_END') || ''
} catch(e) {}
},
onPay(){
uni.showToast({ title: '静态页面演示:支付功能未接入', icon: 'none' })
}
}
}
</script>
<style lang="scss">
page {
background: #1a1a2e !important;
}
.vip-page {
min-height: 100vh;
width: 100%;
position: relative;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%) !important;
overflow: hidden;
display: flex;
flex-direction: column;
}
.main-content {
flex: 1;
padding: 60rpx 40rpx 40rpx;
position: relative;
z-index: 10;
}
/* VIP头部区域 */
.vip-header {
text-align: center;
margin-bottom: 80rpx;
.vip-crown {
margin-bottom: 30rpx;
.crown-icon {
font-size: 80rpx;
filter: drop-shadow(0 4rpx 12rpx rgba(255, 215, 0, 0.3));
}
}
.vip-title {
display: block;
font-size: 48rpx;
font-weight: 700;
color: #fff;
margin-bottom: 16rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.vip-subtitle {
display: block;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 40rpx;
}
.vip-status {
display: inline-block;
padding: 16rpx 32rpx;
border-radius: 50rpx;
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10rpx);
border: 1rpx solid rgba(255, 215, 0, 0.4);
&.active {
background: linear-gradient(45deg, #ffd700, #ffed4e);
border: 1rpx solid rgba(255, 215, 0, 0.3);
.status-text {
color: #333;
}
}
.status-text {
font-size: 26rpx;
font-weight: 600;
color: #fff;
}
}
}
/* 会员功能区域 */
.features-section {
margin-bottom: 60rpx;
.section-title {
display: block;
font-size: 36rpx;
font-weight: 600;
color: #fff;
text-align: center;
margin-bottom: 40rpx;
}
.feature-card {
background: rgba(0, 0, 0, 0.15);
backdrop-filter: blur(15rpx);
border-radius: 24rpx;
padding: 40rpx;
text-align: center;
border: 1rpx solid rgba(255, 215, 0, 0.3);
transition: all 0.3s ease;
&:hover {
transform: translateY(-4rpx);
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.15);
}
.feature-icon {
font-size: 60rpx;
margin-bottom: 24rpx;
filter: drop-shadow(0 4rpx 12rpx rgba(255, 215, 0, 0.3));
}
.feature-text {
display: block;
font-size: 32rpx;
font-weight: 600;
color: #fff;
letter-spacing: 1rpx;
}
}
}
/* VIP信息卡片 */
.vip-info {
margin-bottom: 60rpx;
.info-card {
background: rgba(0, 0, 0, 0.15);
backdrop-filter: blur(15rpx);
border-radius: 20rpx;
padding: 32rpx;
margin-bottom: 20rpx;
border: 1rpx solid rgba(255, 215, 0, 0.3);
display: flex;
justify-content: space-between;
align-items: center;
.info-label {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
.info-value {
font-size: 30rpx;
font-weight: 600;
color: #fff;
&.active {
color: #ffd700;
}
}
}
}
/* 购买区域 */
.purchase-section {
.price-card {
background: rgba(0, 0, 0, 0.15);
backdrop-filter: blur(15rpx);
border-radius: 24rpx;
padding: 40rpx;
text-align: center;
margin-bottom: 40rpx;
border: 1rpx solid rgba(255, 215, 0, 0.3);
.price-label {
display: block;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 16rpx;
}
.price-display {
display: flex;
align-items: baseline;
justify-content: center;
gap: 8rpx;
.price-symbol {
font-size: 32rpx;
color: #ffd700;
font-weight: 600;
}
.price-amount {
font-size: 60rpx;
font-weight: 700;
color: #ffd700;
}
.price-period {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
}
}
.purchase-btn {
width: 100%;
height: 96rpx;
background: linear-gradient(45deg, #ffd700, #ffed4e);
border-radius: 50rpx;
border: none;
box-shadow: 0 8rpx 24rpx rgba(255, 215, 0, 0.3);
transition: all 0.3s ease;
&:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 16rpx rgba(255, 215, 0, 0.4);
}
.btn-text {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
}
}
/* 背景装饰 */
.bg-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 1;
.decoration-circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.05);
&.circle-1 {
width: 300rpx;
height: 300rpx;
top: -150rpx;
right: -100rpx;
}
&.circle-2 {
width: 200rpx;
height: 200rpx;
bottom: 200rpx;
left: -100rpx;
}
&.circle-3 {
width: 150rpx;
height: 150rpx;
top: 50%;
right: 50rpx;
transform: translateY(-50%);
}
}
}
/* 响应式调整 */
@media (max-width: 375px) {
.benefits-grid {
grid-template-columns: 1fr;
}
.vip-header .vip-title {
font-size: 42rpx;
}
.main-content {
padding: 40rpx 30rpx;
}
}
</style>

View File

@@ -8,17 +8,35 @@
<text :class="{ active: biz==='expense' }" @click="switchBiz('expense')">其他支出</text>
</view>
<!-- 子类目按钮 -->
<view class="subtabs" v-if="biz==='sale'">
<button class="subbtn" :class="{ active: saleType==='out' }" @click="saleType='out'">出货</button>
<button class="subbtn" :class="{ active: saleType==='return' }" @click="saleType='return'">退货</button>
<button class="subbtn" :class="{ active: saleType==='collect' }" @click="saleType='collect'">收款</button>
</view>
<view class="subtabs" v-else-if="biz==='purchase'">
<button class="subbtn" :class="{ active: purchaseType==='in' }" @click="purchaseType='in'">进货</button>
<button class="subbtn" :class="{ active: purchaseType==='return' }" @click="purchaseType='return'">退货</button>
<button class="subbtn" :class="{ active: purchaseType==='pay' }" @click="purchaseType='pay'">付款</button>
</view>
<!-- 子类目改为分段式圆角胶囊与需求截图一致 -->
<view class="seg3" v-if="biz==='sale'">
<view :class="['seg3-item', saleType==='out' && 'active']" @click="saleType='out'">
<image :src="SEG_ICONS.sale.out" class="seg3-icon" mode="aspectFit" />
<text>出货</text>
</view>
<view :class="['seg3-item', saleType==='return' && 'active']" @click="saleType='return'">
<image :src="SEG_ICONS.sale.return" class="seg3-icon" mode="aspectFit" />
<text>退货</text>
</view>
<view :class="['seg3-item', saleType==='collect' && 'active']" @click="saleType='collect'">
<image :src="SEG_ICONS.sale.collect" class="seg3-icon" mode="aspectFit" />
<text>收款</text>
</view>
</view>
<view class="seg3" v-else-if="biz==='purchase'">
<view :class="['seg3-item', purchaseType==='in' && 'active']" @click="purchaseType='in'">
<image :src="SEG_ICONS.purchase.in" class="seg3-icon" mode="aspectFit" />
<text>进货</text>
</view>
<view :class="['seg3-item', purchaseType==='return' && 'active']" @click="purchaseType='return'">
<image :src="SEG_ICONS.purchase.return" class="seg3-icon" mode="aspectFit" />
<text>退货</text>
</view>
<view :class="['seg3-item', purchaseType==='pay' && 'active']" @click="purchaseType='pay'">
<image :src="SEG_ICONS.purchase.pay" class="seg3-icon" mode="aspectFit" />
<text>付款</text>
</view>
</view>
<!-- 日期与客户 -->
<picker mode="date" :value="order.orderTime" @change="onDateChange">
@@ -79,25 +97,25 @@
<!-- 已选商品与合计销售/进货 出入库 -->
<view v-else-if="biz==='sale' || biz==='purchase'">
<!-- 快捷操作加商品/选择客户(或供应商)/选择日期 不改功能只换排版 -->
<view class="info-card">
<view class="info-field" @click="biz==='sale' ? chooseCustomer() : chooseSupplier()">
<text class="info-label">{{ biz==='sale' ? '客户' : '供应商' }}</text>
<text class="info-value">{{ biz==='sale' ? customerLabel : supplierLabel }}</text>
</view>
<picker class="info-field" mode="date" :value="order.orderTime" @change="onDateChange">
<view>
<text class="info-label">时间</text>
<text class="info-value">{{ order.orderTime }}</text>
</view>
</picker>
<button class="info-action" @click="chooseProduct">
<image src="/static/icons/icons8-shopping-cart-100.png" class="info-icon" mode="aspectFit" />
<text>加商品</text>
</button>
</view>
<view class="info-card">
<view class="info-field party-field" @click="biz==='sale' ? chooseCustomer() : chooseSupplier()">
<text class="info-label">{{ biz==='sale' ? '客户' : '供应商' }}</text>
<text class="info-value">{{ biz==='sale' ? customerLabel : supplierLabel }}</text>
</view>
<picker class="info-field time-field" mode="date" :value="order.orderTime" @change="onDateChange">
<view>
<text class="info-label">时间</text>
<text class="info-value">{{ order.orderTime }}</text>
</view>
</picker>
<button class="info-action" @click="chooseProduct">
<image src="/static/icons/icons8-shopping-cart-100.png" class="info-icon" mode="aspectFit" />
<text>加商品</text>
</button>
</view>
<view class="summary">
<text>选中货品{{ totalQuantity }}</text>
<text>合计金额¥ {{ totalAmount.toFixed(2) }}</text>
<text class="sel">选中货品{{ totalQuantity }}</text>
<view class="total-pill">合计 ¥ {{ totalAmount.toFixed(2) }}</view>
</view>
<!-- 加号添加商品 -->
@@ -196,7 +214,19 @@
selectedAccountName: '',
// 收款/付款输入
payments: { cash: 0, bank: 0, wechat: 0 },
showMore: false
showMore: false,
SEG_ICONS: {
sale: {
out: '/static/icons/sale.png',
return: '/static/icons/other-pay.png',
collect: '/static/icons/report.png'
},
purchase: {
in: '/static/icons/purchase.png',
return: '/static/icons/other-pay.png',
pay: '/static/icons/account.png'
}
}
}
},
computed: {
@@ -366,9 +396,12 @@
.tabs { display: flex; justify-content: space-around; padding: 16rpx 24rpx; }
.tabs text { color: $uni-text-color-grey; }
.tabs text.active { color: $uni-text-color; font-weight: 700; }
.subtabs { display: flex; gap: 16rpx; padding: 0 24rpx 16rpx; }
.subbtn { padding: 10rpx 20rpx; border-radius: 999rpx; background: $uni-bg-color-hover; color: $uni-text-color-grey; }
.subbtn.active { background: $uni-color-primary; color: #fff; }
/* 三段式胶囊切换 */
.seg3 { display:flex; gap: 0; margin: 12rpx 16rpx; padding: 6rpx; background:#fff; border:2rpx solid #e6ebf2; border-radius: 999rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.04); }
.seg3-item { flex:1; display:flex; align-items:center; justify-content:center; gap: 8rpx; padding: 12rpx 0; color:$uni-text-color; border-radius: 999rpx; }
/* 间隔通过内边距处理,避免空选择器 */
.seg3-item.active { background:#fff; color:#4C8DFF; box-shadow: 0 4rpx 12rpx rgba(76,141,255,0.20), 0 0 0 2rpx #4C8DFF inset; }
.seg3-icon { width: 28rpx; height: 28rpx; opacity: .9; }
.field { display:flex; justify-content: space-between; padding: 22rpx 24rpx; background: $uni-bg-color-grey; border-bottom: 1rpx solid $uni-border-color; }
.label { color:$uni-text-color-grey; }
.value { color:$uni-text-color; }
@@ -395,16 +428,15 @@
.chip { padding: 10rpx 20rpx; border-radius: 999rpx; background: $uni-bg-color-hover; color:$uni-text-color-grey; }
.chip.active { background: $uni-color-primary; color:#fff; }
/* 顶部旧 tabs 简化隐藏(改为更简洁布局) */
.tabs { display: none; }
/* 顶部业务 Tabs 显示 */
/* 快捷操作宫格 */
/* 信息卡片式表达(更稳重) */
.info-card { display:grid; grid-template-columns: 1fr 1fr auto; gap: 10rpx; margin: 12rpx 16rpx 0; background:#fff; border:2rpx solid $uni-border-color; border-radius: 16rpx; padding: 10rpx; align-items:center; }
.info-field { background:$uni-bg-color-grey; border:2rpx solid $uni-border-color; border-radius: 12rpx; padding: 10rpx 12rpx; }
.info-card { display:grid; grid-template-columns: 1fr 1fr auto; gap: 10rpx; margin: 10rpx 12rpx 0; background:transparent; padding: 0; align-items:center; }
.info-field { background:#fff; border:2rpx solid #e6ebf2; border-radius: 12rpx; padding: 10rpx 12rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.04); }
.info-label { color:$uni-text-color-grey; font-size: 24rpx; margin-right: 8rpx; }
.info-value { color:$uni-text-color; font-weight: 700; }
.info-action { display:flex; align-items:center; gap: 6rpx; background:$uni-color-primary; color:#fff; border-radius: 12rpx; padding: 12rpx 14rpx; }
.info-action { display:flex; align-items:center; gap: 6rpx; background:$uni-color-primary; color:#fff; border-radius: 12rpx; padding: 14rpx 16rpx; box-shadow: 0 8rpx 18rpx rgba(76,141,255,0.26); }
.info-icon { width: 32rpx; height: 32rpx; }
</style>

View File

@@ -50,10 +50,19 @@ export default {
}
},
onLoad() {
const hasToken = (() => { try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } })()
if (!hasToken) {
this.items = []
this.categories = []
uni.showToast({ title: '请登录使用该功能', icon: 'none' })
return
}
this.fetchCategories()
this.reload()
},
onShow() {
const hasToken = (() => { try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } })()
if (!hasToken) return
// 从创建/编辑页返回时,确保刷新最新列表
this.reload()
},