This commit is contained in:
左哥 2025-11-16 18:59:10 +08:00
parent ce44170338
commit b2ac8a0e1a
9 changed files with 1060 additions and 464 deletions

51
api/request.js Normal file
View File

@ -0,0 +1,51 @@
const baseUrl = 'http://scrm.1024tool.vip/api/';
function request(url, method = 'GET', data = {}) {
const header = {
'content-type': 'application/json',
// 有其他content-type需求加点逻辑判断处理即可
};
// 获取token有就丢进请求头
const tokenString = wx.getStorageSync('access_token');
if (tokenString) {
header.Authorization = `${tokenString}`;
}
return new Promise((resolve, reject) => {
wx.request({
url: baseUrl + url,
method,
data,
dataType: 'json', // 微信官方文档中介绍会对数据进行一次JSON.parse
header,
success(res) {
if (res.data.code) {
if (res.data.code == 10103) {
wx.removeStorageSync('access_token');
// wx.navigateTo({
// url: '/pages/login/login',
// });
reject(res.data);
return;
}
wx.showToast({
title: res.data.message,
icon: 'none'
});
reject(res.data);
} else {
resolve(res.data);
}
},
fail(err) {
console.log(err)
// 断网、服务器挂了都会fail回调直接reject即可
reject(err);
},
});
});
}
// 导出请求和服务地址
export default request;

31
api/upload.js Normal file
View File

@ -0,0 +1,31 @@
function uploadFile(filePath) {
return new Promise((resolve, reject) => {
if (!filePath) {
const err = new Error('uploadFile requires a filePath parameter');
console.error('上传失败:未提供文件路径', err);
return reject(err);
}
// 获取token有就丢进请求头
const tokenString = wx.getStorageSync('access_token');
const header = {
'Authorization': `${tokenString}`
};
wx.uploadFile({
filePath: filePath,
name: 'file',
header: header,
url: 'http://scrm.1024tool.vip/api/xcx/upload/image',
success: (res) => {
const data = JSON.parse(res.data);
resolve('http://scrm.1024tool.vip/' + data.preview_image_url);
},
fail: (err) => {
console.error('上传失败', err);
reject(err);
}
});
})
}
export default uploadFile;

9
api/user.js Normal file
View File

@ -0,0 +1,9 @@
import request from "./request";
export const login = (data) => {
return request(
'api/user/login', // 根据实际后端接口路径修改
'POST',
data
);
};

View File

@ -36,6 +36,12 @@
"style": {
"navigationBarTitleText": "编辑信息"
}
},
{
"path": "pages/login/index",
"style": {
"navigationBarTitleText": "用户登录"
}
}
],
"globalStyle": {

View File

@ -26,39 +26,39 @@
<view class="container">
<view class="search flex justify-between">
<view class="search-left flex">
<view class="recommend" :class="{active: searchType == 1}" @click="changeSearch(1)">
<view class="recommend" :class="{ active: searchType == 1 }" @click="changeSearch(1)">
<text class="iconfont icon-tubiaoshangshengqushi"></text>
推荐产品
</view>
<view class="group-buying" :class="{active: searchType == 2}" @click="changeSearch(2)">
<view class="group-buying" :class="{ active: searchType == 2 }" @click="changeSearch(2)">
<text class="iconfont icon-aixin"></text>
拼团活动
</view>
</view>
<view class="search-right flex">
<view :class="{active: searchType2 == 1}" @click="changeSearch2(1)">
<view :class="{ active: searchType2 == 1 }" @click="changeSearch2(1)">
<text class="iconfont icon-wanggeshezhi">
</text>
</view>
<view :class="{active: searchType2 == 2}" @click="changeSearch2(2)">
<view :class="{ active: searchType2 == 2 }" @click="changeSearch2(2)">
<text class="iconfont icon-caidan">
</text>
</view>
<view :class="{active: searchType2 == 3}" @click="changeSearch2(3)">
<view :class="{ active: searchType2 == 3 }" @click="changeSearch2(3)">
<text class="iconfont icon-shaixuan1">
</text>
</view>
</view>
</view>
<view class="shop-list" v-if="searchType == 1">
<view class="shop-item">
<view class="shop-item" v-for="(item, index) in shopList">
<view class="shop-image">
<image src=""></image>
<image :src="item.main_image_url"></image>
<view class="shop-tag flex">
<view class="tag danger">
<view class="tag danger" v-if="item.is_hot_selling == 1">
<text class="iconfont icon-tubiaoshangshengqushi"></text>热销
</view>
<view class="tag warning">
@ -67,33 +67,34 @@
</view>
</view>
<view class="shop-container">
<view class="shop-tags flex">
<!-- <view class="shop-tags flex">
<view class="tag">
热销
</view>
<view class="tag">
热销
</view>
</view>
</view> -->
<view class="sku-title">
薰衣草助眠香氛
{{item.name}}
</view>
<view class="sku-description">
天然薰衣草精油助眠好伴侣舒缓身心
{{item.description}}
</view>
<view class="sku-rating">
<text class="iconfont icon-xingxing"></text>
<text class="sku-rating-num">4.6</text>
<text class="sku-rating-num">{{item.rating}}</text>
<text class="sku-rating-text">156条评价 </text>
<text class="sku-rating-text"> 423 人喜欢</text>
<text class="sku-rating-text"> {{item.like_count}} 人喜欢</text>
</view>
<view class="sku-price">
<text>89</text>
<text class="original-price">129</text>
<view class="sku-price" v-if=" item.skus && item.skus.length > 0">
<text>{{ item.skus[0].price }}</text>
<text class="original-price">{{ item.skus[0].original_price }}</text>
</view>
<view class="sku-operation flex">
<view class="sku-num">
<text class="iconfont icon-aixin"></text> 423
<view class="sku-num" @click="handelLike(item)">
<text v-if="item.is_liked" class="iconfont icon-xin heart-filled"></text>
<text v-else class="iconfont icon-xin1 heart-outline"></text> {{item.like_count}}
</view>
<view class="">
<text class="iconfont icon-zhifeiji1"></text>
@ -124,11 +125,11 @@
<view class="tag danger">
<text class="iconfont icon-shijian"></text>即将结束
</view>
</view>
</view>
<view class="shop-container">
<view class="sku-title">
3人拼团 玫瑰香水
</view>
@ -154,7 +155,7 @@
</view>
</view>
</view>
<view class="sku-group-price flex justify-between">
<view class="group-price">
<view class="group-price-num">
@ -177,14 +178,15 @@
</view>
</view>
</view>
<view class="group-progress">
<view class="group-progress-num flex justify-between">
<text class="group-progress-text">拼团进度</text>
<text class="number">2/3 </text>
</view>
<view class="group-progress-width">
<progress :percent="50" stroke-width="20rpx" border-radius="20" activeColor="#9810fa" />
<progress :percent="50" stroke-width="20rpx" border-radius="20"
activeColor="#9810fa" />
</view>
<view class="group-progress-num font-text flex justify-between">
<text class="group-progress-text">已参团2人</text>
@ -209,375 +211,475 @@
</view>
</template>
<script>
export default {
data() {
return {
searchType: 1,
searchType2: 2,
}
},
onLoad() {
import request from '/api/request';
export default {
data() {
return {
searchType: 1,
searchType2: 2,
page: 1,
page_size: 10,
category_id: '',
name: '',
is_hot_selling: '',
is_limited: '',
shopList: '',
}
},
onLoad() {
this.getShopList()
},
methods: {
getShopList(){
request('xcx/products', 'get', {
page: this.page,
page_size: this.page_size
}).then((res) => {
this.shopList = res.list
})
},
methods: {
changeSearch(index) {
this.searchType = index
},
changeSearch2(index) {
this.searchType2 = index
}
changeSearch(index) {
this.searchType = index
},
changeSearch2(index) {
this.searchType2 = index
},
handelLike(row) {
request('xcx/product/like', 'post', {
product_id: row.id,
type: row.is_liked ? 2 : 1
}).then((res) => {
row.is_liked = !row.is_liked
})
}
}
}
</script>
<style lang="scss" scoped>
.header {
padding: 30rpx;
box-shadow: 0 2rpx 6rpx 0 rgb(0 0 0 / 0.1), 0 2rpx 4rpx -2rpx rgb(0 0 0 / 0.1);
.header {
padding: 30rpx;
box-shadow: 0 2rpx 6rpx 0 rgb(0 0 0 / 0.1), 0 2rpx 4rpx -2rpx rgb(0 0 0 / 0.1);
}
.header-left {
.icon {
width: 50rpx;
height: 50rpx;
vertical-align: middle;
margin-right: 20rpx;
}
.header-left {
.icon {
width: 50rpx;
height: 50rpx;
vertical-align: middle;
margin-right: 20rpx;
text {
font-size: 30rpx;
color: #9810fa;
}
}
.header-right {
view {
box-shadow: none;
padding: 10rpx 10rpx;
height: 40rpx;
border: 0 solid transparent;
}
view+view {
margin-left: 20rpx;
}
}
.container {
padding: 40rpx 32rpx;
.search {
.search-left {
padding: 6rpx;
border-radius: 24rpx;
box-shadow: 0 2rpx 6rpx 0 rgb(0 0 0 / 0.1), 0 2rpx 4rpx -2rpx rgb(0 0 0 / 0.1);
view {
border-radius: 20rpx;
padding: 12rpx 12rpx;
font-size: 28rpx;
text {
margin: 0 6rpx;
}
}
.recommend {
image {
width: 40rpx;
height: 40rpx;
vertical-align: middle;
}
}
.group-buying {}
.active {
background-image: var(--background-linear-gradient);
color: #fff;
}
}
text {
font-size: 30rpx;
color: #9810fa;
.search-right {
padding-top: 10rpx;
view {
width: 60rpx;
height: 60rpx;
border-radius: 20rpx;
line-height: 60rpx;
text-align: center;
font-size: 40rpx;
display: flex;
align-items: center;
justify-content: center;
}
view+view {
margin-left: 10rpx;
}
.active {
background-color: #f3e8ff;
color: #9810fa;
}
}
}
.header-right{
view{
box-shadow: none;
padding: 10rpx 10rpx;
height: 40rpx;
border: 0 solid transparent;
}
view+view{
margin-left: 20rpx;
}
}
.container{
padding: 40rpx 32rpx;
.search{
.search-left{
padding: 6rpx;
border-radius: 24rpx;
box-shadow: 0 2rpx 6rpx 0 rgb(0 0 0 / 0.1), 0 2rpx 4rpx -2rpx rgb(0 0 0 / 0.1);
view{
border-radius: 20rpx;
padding: 12rpx 12rpx;
.shop-list {
margin-top: 60rpx;
.shop-item {
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
border-radius: 0 0 36rpx 36rpx;
image {
display: block;
width: 100%;
height: 400rpx;
}
.shop-container {
padding: 80rpx 32rpx;
.shop-tags {
.tag {
font-size: 20rpx;
color: #9810fa;
border: 2rpx solid #e9d4ff;
padding: 4rpx 10rpx;
border-radius: 12rpx;
}
.tag+.tag {
margin-left: 14rpx;
}
}
.sku-title {
margin-top: 20rpx;
font-size: 36rpx;
font-weight: bold;
}
.sku-description {
margin-top: 12rpx;
font-size: 28rpx;
text{
margin: 0 6rpx;
color: #4a5565;
}
.sku-rating {
margin-top: 40rpx;
.iconfont {
font-size: 32rpx;
color: #f0b100;
margin-right: 20rpx;
}
}
.recommend{
image{
width: 40rpx;
height: 40rpx;
vertical-align: middle;
}
}
.group-buying{
}
.active{
background-image: var(--background-linear-gradient);
color: #fff;
}
}
.search-right{
padding-top: 10rpx;
view{
width: 60rpx;
height: 60rpx;
border-radius: 20rpx;
line-height: 60rpx;
text-align: center;
font-size: 40rpx;
display: flex;
align-items: center;
justify-content: center;
}
view+view{
margin-left: 10rpx;
}
.active{
background-color: #f3e8ff;
color: #9810fa;
}
}
}
.shop-list{
margin-top: 60rpx;
.shop-item{
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
border-radius: 0 0 36rpx 36rpx;
image{
display: block;
width: 100%;
height: 400rpx;
}
.shop-container{
padding: 80rpx 32rpx;
.shop-tags{
.tag{
font-size: 20rpx;
color: #9810fa;
border: 2rpx solid #e9d4ff;
padding: 4rpx 10rpx;
border-radius: 12rpx;
}
.tag+.tag{
margin-left: 14rpx;
}
}
.sku-title{
margin-top: 20rpx;
font-size: 36rpx;
font-weight: bold;
}
.sku-description{
margin-top: 12rpx;
.sku-rating-text {
font-size: 28rpx;
color: #4a5565;
}
.sku-rating{
margin-top: 40rpx;
.iconfont{
font-size: 32rpx;
color: #f0b100;
margin-right: 20rpx;
}
.sku-price {
margin-top: 30rpx;
font-size: 48rpx;
color: #e7000b;
font-weight: bold;
.original-price {
margin-left: 20rpx;
font-size: 28rpx;
color: #99a1af;
font-weight: 400;
text-decoration: line-through;
}
}
.sku-operation {
margin-top: 40rpx;
justify-content: space-between;
view {
flex: 1;
border-radius: 12rpx;
text-align: center;
border: 2rpx solid rgb(0 0 0 / 0.1);
box-shadow: 0 2rpx 4rpx 0 rgb(0 0 0 / 0.05);
height: 60rpx;
line-height: 60rpx;
font-size: 24rpx;
}
.sku-num {
flex: 3;
margin-right: 30rpx;
.iconfont {
margin-right: 8rpx;
}
.sku-rating-text{
font-size: 28rpx;
color: #4a5565;
.heart-filled {
color: #e7000b;
// font-size: 28rpx;
}
.heart-outline {
color: #ccc;
// font-size: 24rpx;
// opacity: 0.8;
}
}
.sku-price{
margin-top: 30rpx;
font-size: 48rpx;
color: #e7000b;
font-weight: bold;
.original-price{
margin-left: 20rpx;
font-size: 28rpx;
color: #99a1af;
font-weight: 400;
text-decoration: line-through;
}
}
.sku-operation{
margin-top: 40rpx;
justify-content: space-between;
view{
flex: 1;
border-radius: 12rpx;
text-align: center;
border: 2rpx solid rgb(0 0 0 / 0.1);
box-shadow: 0 2rpx 4rpx 0 rgb(0 0 0 / 0.05);
height: 60rpx;
line-height: 60rpx;
font-size: 24rpx;
}
.sku-num{
flex: 3;
margin-right: 30rpx;
.iconfont{
margin-right: 8rpx;
}
}
.shop-cart{
flex: 3;
margin-left: 30rpx;
// color: #fff;
background-image: var(--background-linear-gradient);
.iconfont{
margin-right: 8rpx;
}
}
}
.sku-operation2{
view{
flex: auto;
border: 0;
box-shadow: none;
}
.sku-num{
flex: auto;
}
.shop-cart{
flex: auto;
background-image: none;
background-color: var(--color-danger);
color: #fff;
border-radius: 60rpx;
}
}
}
.shop-image{
position: relative;
image{
z-index: 0;
}
.shop-tag{
position: absolute;
top: 40rpx;
left: 32rpx;
z-index: 1;
.tag{
color: #fff;
height: 48rpx;
line-height: 48rpx;
font-size: 20rpx;
border-radius: 48rpx;
padding: 0 16rpx;
box-shadow: var(--tw-shadow);
text{
vertical-align: middle;
margin-right: 10rpx;
}
}
.tag+.tag{
margin-left: 14rpx;
}
.danger{
background-color: var(--color-danger);
}
.warning{
background-color: var(--color-warning);
}
.success{
background-color: var(--color-success);
}
}
.buttom{
top: auto;
buttom: 40rpx;
text{
font-size: 28rpx;
.shop-cart {
flex: 3;
margin-left: 30rpx;
// color: #fff;
background-image: var(--background-linear-gradient);
.iconfont {
margin-right: 8rpx;
}
}
}
}
}
.sku-group-buying{
margin-top: 16rpx;
padding: 24rpx;
border-radius: 16rpx;
background-color: #fbf9fa;
.group-img{
width: 70rpx;
height: 70rpx;
border-radius: 70rpx;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
background-image: var(--background-linear-gradient);
}
.group-name{
font-weight: bold;
font-size: 28rpx;
}
.group-text{
margin-top: 4rpx;
font-size: 24rpx;
color: #6a7282;
text{
margin-right: 8rpx;
font-size: 28rpx;
.sku-operation2 {
view {
flex: auto;
border: 0;
box-shadow: none;
}
.sku-num {
flex: auto;
}
.shop-cart {
flex: auto;
background-image: none;
background-color: var(--color-danger);
color: #fff;
border-radius: 60rpx;
}
}
}
.group-rating{
font-size: 28rpx;
color: #f0b100;
margin-right: 20rpx;
font-weight: bold;
text{
margin-right: 8rpx;
.shop-image {
position: relative;
image {
z-index: 0;
}
}
}
.sku-group-price{
margin-top: 32rpx;
padding: 24rpx;
border-radius: 16rpx;
background-color: #fff7ed;
.group-price-num{
font-size: 48rpx;
color: #e7000b;
font-weight: bold;
.original-price{
margin-left: 20rpx;
font-size: 28rpx;
color: #99a1af;
font-weight: 400;
text-decoration: line-through;
.shop-tag {
position: absolute;
top: 40rpx;
left: 32rpx;
z-index: 1;
.tag {
color: #fff;
height: 48rpx;
line-height: 48rpx;
font-size: 20rpx;
border-radius: 48rpx;
padding: 0 16rpx;
box-shadow: var(--tw-shadow);
text {
vertical-align: middle;
margin-right: 10rpx;
}
}
.tag+.tag {
margin-left: 14rpx;
}
.danger {
background-color: var(--color-danger);
}
.warning {
background-color: var(--color-warning);
}
.success {
background-color: var(--color-success);
}
}
}
.group-price{
flex: 2;
}
.group-price-save{
flex: 1;
}
.group-price-text{
font-size: 24rpx;
color: #4a5565;
margin-left: 20rpx;
}
.save-tag{
// display: inline-block;
background-color: var(--color-danger);
color: #fff;
font-size: 24rpx;
.iconfont{
margin-right: 6rpx;
display: inline-block;
.buttom {
top: auto;
buttom: 40rpx;
text {
font-size: 28rpx;
}
}
height: 48rpx;
line-height: 48rpx;
padding-left: 40rpx;
padding-right: 10rpx;
border-radius: 48rpx;
text-align: center;
}
.save-num{
margin-top: 16rpx;
font-size: 24rpx;
color: #4a5565;
text-align: right;
}
}
.group-progress{
margin-top: 32rpx;
font-size: 28rpx;
.group-progress-text{
font-size: 24rpx;
}
.number{
color: #9810fa;
font-weight: bold;
}
.group-progress-width{
margin-top: 26rpx;
margin-bottom: 26rpx;
}
.font-text{
color: #6a7282;
}
}
}
.sku-group-buying {
margin-top: 16rpx;
padding: 24rpx;
border-radius: 16rpx;
background-color: #fbf9fa;
.group-img {
width: 70rpx;
height: 70rpx;
border-radius: 70rpx;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
background-image: var(--background-linear-gradient);
}
.group-name {
font-weight: bold;
font-size: 28rpx;
}
.group-text {
margin-top: 4rpx;
font-size: 24rpx;
color: #6a7282;
text {
margin-right: 8rpx;
font-size: 28rpx;
}
}
.group-rating {
font-size: 28rpx;
color: #f0b100;
margin-right: 20rpx;
font-weight: bold;
text {
margin-right: 8rpx;
}
}
}
.sku-group-price {
margin-top: 32rpx;
padding: 24rpx;
border-radius: 16rpx;
background-color: #fff7ed;
.group-price-num {
font-size: 48rpx;
color: #e7000b;
font-weight: bold;
.original-price {
margin-left: 20rpx;
font-size: 28rpx;
color: #99a1af;
font-weight: 400;
text-decoration: line-through;
}
}
.group-price {
flex: 2;
}
.group-price-save {
flex: 1;
}
.group-price-text {
font-size: 24rpx;
color: #4a5565;
margin-left: 20rpx;
}
.save-tag {
// display: inline-block;
background-color: var(--color-danger);
color: #fff;
font-size: 24rpx;
.iconfont {
margin-right: 6rpx;
display: inline-block;
}
height: 48rpx;
line-height: 48rpx;
padding-left: 40rpx;
padding-right: 10rpx;
border-radius: 48rpx;
text-align: center;
}
.save-num {
margin-top: 16rpx;
font-size: 24rpx;
color: #4a5565;
text-align: right;
}
}
.group-progress {
margin-top: 32rpx;
font-size: 28rpx;
.group-progress-text {
font-size: 24rpx;
}
.number {
color: #9810fa;
font-weight: bold;
}
.group-progress-width {
margin-top: 26rpx;
margin-bottom: 26rpx;
}
.font-text {
color: #6a7282;
}
}
}
</style>

261
pages/login/index.vue Normal file
View File

@ -0,0 +1,261 @@
<template>
<view class="login-page">
<view class="login-container">
<view class="logo-section">
<text class="app-name">香水有毒</text>
<text class="welcome-text">欢迎使用</text>
</view>
<view class="login-form">
<!-- #ifdef MP-WEIXIN -->
<button class="login-btn quick-login-btn" open-type="getPhoneNumber"
@getphonenumber="handleGetPhoneNumber" :loading="loading">
<text class="btn-text">快捷登录</text>
</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<button class="login-btn quick-login-btn" @click="handleQuickLogin" :loading="loading">
<text class="btn-text">快捷登录</text>
</button>
<!-- #endif -->
</view>
<view class="tips-section">
<text class="tips-text">点击快捷登录即表示您同意</text>
<text class="link-text">用户协议</text>
<text class="tips-text"></text>
<text class="link-text">隐私政策</text>
</view>
</view>
</view>
</template>
<script>
import request from '@/api/request.js';
export default {
data() {
return {
loading: false
};
},
methods: {
//
async handleGetPhoneNumber(e) {
console.log('获取手机号授权结果:', e);
if (e.detail.errMsg === 'getPhoneNumber:ok') {
//
this.loading = true;
try {
// code
const loginRes = await this.wxLogin();
if (!loginRes.code) {
throw new Error('获取微信登录code失败');
}
// code
const loginData = {
code: e.detail.code,
invitation_code: e.detail.encryptedData,
// iv: e.detail.iv
};
const result = await request('xcx/quick_login', 'POST', loginData);
await wx.setStorageSync('access_token', result.token);
await wx.setStorageSync('is_personal_information_complete', result.result.is_personal_information_complete);
if (result.is_personal_information_complete) {
wx.navigateBack();
} else {
wx.navigateTo({
url: `/pages/my/editInfo/index`,
});
}
// token
// if (result.token) {
// wx.setStorageSync('access_token', result.token);
// } else if (result.data && result.data.token) {
// wx.setStorageSync('access_token', result.data.token);
// }
// //
// if (result.userInfo) {
// wx.setStorageSync('userInfo', result.userInfo);
// } else if (result.data && result.data.userInfo) {
// wx.setStorageSync('userInfo', result.data.userInfo);
// }
uni.showToast({
title: '登录成功',
icon: 'success',
duration: 2000
});
//
// setTimeout(() => {
// //
// const pages = getCurrentPages();
// if (pages.length > 1) {
// uni.navigateBack();
// } else {
// uni.switchTab({
// url: '/pages/index/index'
// });
// }
// }, 1500);
} catch (error) {
console.error('登录失败:', error);
uni.showToast({
title: error.message || '登录失败,请重试',
icon: 'none',
duration: 2000
});
} finally {
this.loading = false;
}
} else {
//
uni.showToast({
title: '需要授权手机号才能登录',
icon: 'none',
duration: 2000
});
}
},
// code
wxLogin() {
return new Promise((resolve, reject) => {
// #ifdef MP-WEIXIN
wx.login({
success: (res) => {
if (res.code) {
resolve(res);
} else {
reject(new Error('获取code失败'));
}
},
fail: (err) => {
reject(err);
}
});
// #endif
// #ifndef MP-WEIXIN
// 使uni.login
uni.login({
provider: 'weixin',
success: (res) => {
resolve(res);
},
fail: (err) => {
reject(err);
}
});
// #endif
});
},
//
handleQuickLogin() {
uni.showToast({
title: '当前平台不支持快捷登录',
icon: 'none'
});
}
}
};
</script>
<style scoped>
.login-page {
min-height: 100vh;
background: linear-gradient(180deg, #9810fa 0%, #f5f5f5 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 32rpx;
}
.login-container {
width: 100%;
max-width: 600rpx;
background: #ffffff;
border-radius: 32rpx;
padding: 80rpx 60rpx;
box-shadow: 0 8rpx 32rpx rgba(152, 16, 250, 0.15);
}
.logo-section {
text-align: center;
margin-bottom: 80rpx;
}
.app-name {
display: block;
font-size: 56rpx;
font-weight: bold;
color: #1A1A1A;
margin-bottom: 16rpx;
}
.welcome-text {
display: block;
font-size: 28rpx;
color: #6a7282;
}
.login-form {
margin-bottom: 60rpx;
}
.login-btn {
width: 100%;
height: 96rpx;
background: linear-gradient(135deg, #9810fa 0%, #7a0bc7 100%);
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
border: none;
box-shadow: 0 8rpx 24rpx rgba(152, 16, 250, 0.3);
transition: all 0.3s;
}
.login-btn::after {
border: none;
}
.login-btn:active {
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(152, 16, 250, 0.3);
}
.btn-text {
color: #ffffff;
font-size: 32rpx;
font-weight: 500;
}
.quick-login-btn {
margin-top: 0;
}
.tips-section {
text-align: center;
font-size: 24rpx;
color: #9ca3af;
line-height: 1.6;
}
.tips-text {
color: #9ca3af;
}
.link-text {
color: #9810fa;
margin: 0 4rpx;
}
</style>

View File

@ -8,26 +8,27 @@
<view class="form-card">
<view class="form-item">
<text class="form-label">姓名</text>
<input
class="form-input"
v-model="form.name"
type="text"
placeholder="请输入您的姓名"
placeholder-class="form-placeholder"
/>
<input class="form-input" v-model="form.username" type="text" placeholder="请输入您的姓名"
placeholder-class="form-placeholder" />
</view>
<view class="form-item">
<text class="form-label">性别</text>
<picker mode="selector" :range="genderOptions" range-key="label" :value="genderIndex"
@change="handleGenderChange">
<view class="form-picker">
<text :class="form.sex ? '' : 'form-placeholder'">
{{ genderText || '请选择性别' }}
</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">手机号</text>
<input
class="form-input"
v-model="form.phone"
type="number"
maxlength="11"
placeholder="请输入常用手机号"
placeholder-class="form-placeholder"
/>
<input class="form-input" v-model="form.mobile" type="number" maxlength="11" placeholder="请输入常用手机号"
placeholder-class="form-placeholder" />
</view>
<view class="form-item">
<!-- <view class="form-item">
<text class="form-label">邮箱</text>
<input
class="form-input"
@ -36,8 +37,8 @@
placeholder="请输入联系邮箱"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-item">
</view> -->
<!-- <view class="form-item">
<text class="form-label">地址</text>
<textarea
class="form-textarea"
@ -46,7 +47,7 @@
placeholder-class="form-placeholder"
auto-height="true"
/>
</view>
</view> -->
</view>
<view class="tips-card">
@ -64,34 +65,58 @@
</template>
<script>
import request from '@/api/request.js';
export default {
data() {
return {
form: {
name: '',
phone: '',
email: '',
address: ''
}
username: '',
sex: null,
mobile: '',
// email: '',
// address: ''
},
genderOptions: [
{ label: '男', value: 1 },
{ label: '女', value: 2 }
]
};
},
computed: {
genderIndex() {
if (!this.form.sex) return 0;
const option = this.genderOptions.find(item => item.value === this.form.sex);
return option ? this.genderOptions.indexOf(option) : 0;
},
genderText() {
if (!this.form.sex) return '';
const option = this.genderOptions.find(item => item.value === this.form.sex);
return option ? option.label : '';
}
},
onLoad() {
this.loadProfile();
},
methods: {
loadProfile() {
try {
const stored = uni.getStorageSync('userProfile');
if (stored) {
this.form = {
...this.form,
...stored
};
const is_personal_information_complete = uni.getStorageSync('is_personal_information_complete');
if (is_personal_information_complete) {
request('xcx/get_user_info', 'GET').then(res => {
this.form = {
...this.form,
...res
};
});
}
} catch (error) {
console.warn('加载个人信息失败', error);
}
},
handleGenderChange(e) {
const index = e.detail.value;
this.form.sex = this.genderOptions[index].value;
},
validatePhone(phone) {
const phoneReg = /^(?:(?:\+|00)86)?1[3-9]\d{9}$/;
return phoneReg.test(phone);
@ -102,32 +127,48 @@ export default {
return emailReg.test(email);
},
handleCancel() {
uni.navigateBack({
delta: 1
});
const is_personal_information_complete = uni.getStorageSync('is_personal_information_complete');
if (is_personal_information_complete) {
wx.navigateBack();
} else {
uni.setStorageSync('is_personal_information_complete', true);
wx.navigateTo({
url: `/pages/my/editInfo/index`,
});
}
},
handleSubmit() {
if (!this.form.name.trim()) {
if (!this.form.username.trim()) {
return this.showToast('请填写姓名');
}
if (!this.form.phone.trim() || !this.validatePhone(this.form.phone.trim())) {
if (!this.form.sex || (this.form.sex !== 1 && this.form.sex !== 2)) {
return this.showToast('请选择性别');
}
if (!this.form.mobile.trim() || !this.validatePhone(this.form.mobile.trim())) {
return this.showToast('请填写有效的手机号');
}
if (this.form.email.trim() && !this.validateEmail(this.form.email.trim())) {
return this.showToast('请填写有效的邮箱');
}
if (!this.form.address.trim()) {
return this.showToast('请填写联系地址');
}
// if (this.form.email.trim() && !this.validateEmail(this.form.email.trim())) {
// return this.showToast('');
// }
// if (!this.form.address.trim()) {
// return this.showToast('');
// }
try {
uni.setStorageSync('userProfile', { ...this.form });
this.showToast('保存成功', 'success');
setTimeout(() => {
uni.navigateBack({
delta: 1
});
}, 800);
request('xcx/set_user_info', 'POST', this.form).then(res => {
this.showToast('保存成功', 'success');
const is_personal_information_complete = uni.getStorageSync('is_personal_information_complete');
if (is_personal_information_complete) {
wx.navigateBack();
} else {
uni.setStorageSync('is_personal_information_complete', true);
wx.navigateTo({
url: `/pages/my/editInfo/index`,
});
}
});
} catch (error) {
this.showToast('信息保存失败,请稍后重试');
console.error('保存个人信息失败', error);
@ -192,8 +233,10 @@ export default {
color: #1a1a1a;
font-weight: 500;
}
.form-input{
.form-input {
height: 80rpx;
line-height: 80rpx;
}
.form-input,
@ -201,10 +244,29 @@ export default {
width: 100%;
background: #f5f7fb;
border-radius: 16rpx;
padding: 0 28rpx;
font-size: 28rpx;
color: #1a1a1a;
box-sizing: border-box;
}
.form-picker {
width: 100%;
height: 80rpx;
background: #f5f7fb;
border-radius: 16rpx;
padding: 24rpx 28rpx;
font-size: 28rpx;
color: #1a1a1a;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
}
.picker-arrow {
font-size: 32rpx;
color: #b0b3ba;
}
.form-textarea {

View File

@ -2,13 +2,15 @@
<view class="page">
<view class="profile-card">
<view class="profile-header">
<view class="avatar-wrapper">
<view class="avatar">100x100</view>
<view class="avatar-wrapper" @click="uploadAvatar">
<view class="avatar">
<image :src="avatarUrl" mode="aspectFill" @error="handleImageError"></image>
</view>
<view class="avatar-upload">📷</view>
</view>
<view class="profile-info">
<view class="title-row">
<text class="name">香香公主</text>
<text class="name">{{ userInfo.username }}</text>
<view class="vip-badge">
<text>品鉴会员</text>
</view>
@ -32,13 +34,8 @@
</view>
<view class="tabs">
<view
class="tab-item"
v-for="(tab, index) in tabs"
:key="tab"
:class="{ active: index === currentTab }"
@click="handleTabClick(index)"
>
<view class="tab-item" v-for="(tab, index) in tabs" :key="tab" :class="{ active: index === currentTab }"
@click="handleTabClick(index)">
{{ tab }}
</view>
</view>
@ -48,10 +45,7 @@
<view class="monthly-list">
<view class="monthly-item" v-for="item in monthlyData" :key="item.label">
<text class="monthly-label">{{ item.label }}</text>
<text
class="monthly-value"
:style="{ color: item.color || '#1A1A1A' }"
>
<text class="monthly-value" :style="{ color: item.color || '#1A1A1A' }">
{{ item.value }}
</text>
</view>
@ -85,11 +79,7 @@
<text class="amount-value">{{ order.amount }}</text>
</view>
<view class="order-actions">
<view
class="order-action"
v-for="action in order.actions"
:key="action.label"
>
<view class="order-action" v-for="action in order.actions" :key="action.label">
{{ action.label }}
</view>
</view>
@ -119,12 +109,8 @@
</view>
</view>
<view class="achievements-card" v-if="currentTab === 3">
<view
class="achievement-item"
v-for="achievement in achievements"
:key="achievement.title"
:class="{ highlight: achievement.highlight }"
>
<view class="achievement-item" v-for="achievement in achievements" :key="achievement.title"
:class="{ highlight: achievement.highlight }">
<view class="achievement-left">
<view class="achievement-icon" :style="{ backgroundColor: achievement.iconBg }">
<text class="achievement-icon-text">{{ achievement.icon }}</text>
@ -132,15 +118,10 @@
<view class="achievement-info">
<text class="achievement-title">{{ achievement.title }}</text>
<text class="achievement-desc">{{ achievement.desc }}</text>
<view
class="achievement-progress"
v-if="achievement.progressText"
>
<view class="achievement-progress" v-if="achievement.progressText">
<view class="achievement-progress-bar">
<view
class="achievement-progress-fill"
:style="{ width: achievement.progress + '%' }"
></view>
<view class="achievement-progress-fill"
:style="{ width: achievement.progress + '%' }"></view>
</view>
<text class="achievement-progress-text">{{ achievement.progressText }}</text>
</view>
@ -186,25 +167,24 @@
<view class="quick-card">
<view class="quick-title">快捷功能</view>
<view class="quick-grid">
<view
class="quick-item"
v-for="action in quickActions"
:key="action.label"
>
<view class="quick-item" v-for="action in quickActions" :key="action.label">
<view class="quick-icon">{{ action.icon }}</view>
<text class="quick-label">{{ action.label }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
import request from '@/api/request.js';
import uploadFile from '@/api/upload.js';
export default {
data() {
return {
userInfo: {},
currentTab: 0,
stats: [
{ icon: '🎁', value: '1280', label: '当前积分', color: '#7B43FF' },
@ -315,7 +295,87 @@ export default {
]
};
},
computed: {
avatarUrl() {
// URL使URL使
if (this.userInfo && this.userInfo.avatar_url && this.userInfo.avatar_url.trim() !== '') {
return this.userInfo.avatar_url;
}
// 使base64SVG
//
return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYwIiBoZWlnaHQ9IjE2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48Y2lyY2xlIGN4PSI4MCIgY3k9IjgwIiByPSI4MCIgZmlsbD0iI2U1ZTdlYSIvPjxjaXJjbGUgY3g9IjgwIiBjeT0iNjUiIHI9IjI1IiBmaWxsPSIjOWZhMGFmIi8+PHBhdGggZD0iTSA4MCAxMDAgUSA1MCAxMDAgNTAgMTMwIEwgNTAgMTYwIEwgMTEwIDE2MCBMIDExMCAxMzAgUSAxMTAgMTAwIDgwIDEwMCBaIiBmaWxsPSIjOWZhMGFmIi8+PC9zdmc+';
}
},
onShow: function () {
const token = wx.getStorageSync('access_token');
if (!token) {
uni.navigateTo({
url: '/pages/login/index'
});
return;
} else {
this.loadProfile();
}
},
methods: {
handleImageError(e) {
// 使
console.log('头像加载失败,使用默认头像', e);
},
loadProfile() {
try {
request('xcx/get_user_info', 'GET').then(res => {
this.userInfo = res
});
} catch (error) {
console.warn('加载个人信息失败', error);
}
},
uploadAvatar() {
console.log('uploadAvatar 被点击');
uni.chooseImage({
count: 1,
mediaType: ['image'],
sizeType: ['compressed', 'original'],
sourceType: ['album', 'camera'],
success: (res) => {
console.log('选择图片成功', res);
const tempFilePaths = res.tempFilePaths;
if (tempFilePaths && tempFilePaths.length > 0) {
uni.showLoading({
title: '上传中...'
});
uploadFile(tempFilePaths[0]).then((res) => {
console.log('上传成功', res);
this.userInfo.avatar_url = res;
request('xcx/set_user_info', 'POST', {
avatar_url: res,
username: this.userInfo.username,
sex: this.userInfo.sex,
mobile: this.userInfo.mobile,
}).then(res => {
uni.hideLoading({
title: '上传成功...'
});
console.log('设置头像成功', res);
}).catch((err) => {
uni.hideLoading({
title: '上传失败...'
});
console.error('设置头像失败', err);
})
});
}
},
fail: (err) => {
console.error('选择图片失败', err);
uni.showToast({
title: '选择图片失败',
icon: 'none'
});
}
});
},
handleTabClick(index) {
this.currentTab = index;
},
@ -328,7 +388,7 @@ export default {
};
</script>
<style scoped>
<style scoped lang="scss">
.page {
min-height: 100vh;
/* padding: 24rpx; */
@ -342,7 +402,7 @@ export default {
.profile-card {
background-color: #ffffff;
/* border-radius: 24rpx; */
padding-bottom: 24rpx;
}
@ -372,6 +432,13 @@ export default {
font-size: 26rpx;
color: #ffffff;
border: 4rpx solid rgba(255, 255, 255, 0.4);
position: relative;
z-index: 0;
image {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
.avatar-upload {
@ -384,10 +451,13 @@ export default {
align-items: center;
justify-content: center;
position: absolute;
z-index: 10;
bottom: 0;
right: 0;
font-size: 32rpx;
box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.1);
pointer-events: auto;
cursor: pointer;
}
.profile-info {
@ -450,7 +520,7 @@ export default {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24rpx;
}
.stat-card {

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 5057334 */
src: url('//at.alicdn.com/t/c/font_5057334_yrkd6oeyr39.woff2?t=1762676246439') format('woff2'),
url('//at.alicdn.com/t/c/font_5057334_yrkd6oeyr39.woff?t=1762676246439') format('woff'),
url('//at.alicdn.com/t/c/font_5057334_yrkd6oeyr39.ttf?t=1762676246439') format('truetype');
src: url('//at.alicdn.com/t/c/font_5057334_7ak9qra77f.woff2?t=1763287992869') format('woff2'),
url('//at.alicdn.com/t/c/font_5057334_7ak9qra77f.woff?t=1763287992869') format('woff'),
url('//at.alicdn.com/t/c/font_5057334_7ak9qra77f.ttf?t=1763287992869') format('truetype');
}
.iconfont {
@ -13,6 +13,14 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-xin:before {
content: "\e641";
}
.icon-xin1:before {
content: "\e6cd";
}
.icon-lihe:before {
content: "\11c33";
}
@ -60,7 +68,3 @@
.icon-sousuo:before {
content: "\e61f";
}
.icon-aixin:before {
content: "\e8ab";
}