🎨 优化核心组件,优化登录和首页

This commit is contained in:
2025-08-18 23:49:43 +08:00
parent 829c348370
commit 3da97eb797
30 changed files with 4213 additions and 257 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
VITE_BASE_URL = http://127.0.0.1:8888

View File

@@ -1,12 +1,37 @@
<script setup>
import { useSystemStore } from '@/store/system.js'
import { onLaunch } from '@dcloudio/uni-app'
import { useUserStore } from '@/store/user.js'
import { onLaunch, onShow } from '@dcloudio/uni-app'
const systemStore = useSystemStore()
const userStore = useUserStore()
onLaunch(() => {
systemStore.init()
checkLoginStatus()
})
onShow(() => {
// 每次显示时检查登录状态
checkLoginStatus()
})
// 检查登录状态
const checkLoginStatus = () => {
const isLoggedIn = userStore.checkLoginStatus()
// 如果未登录且不在登录页面,则跳转到登录页
if (!isLoggedIn) {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage && currentPage.route !== 'pages/user/login') {
uni.reLaunch({
url: '/pages/user/login'
})
}
}
}
</script>
<style lang="scss">
@@ -20,7 +45,7 @@ onLaunch(() => {
}
//主题scss
.red-theme {
@import 'styles/themes/red-theme.scss';
@import 'styles/themes/default-theme.scss';
}
</style>

View File

@@ -1,111 +0,0 @@
import Request from 'luch-request'
const tokeyKey = 'Authorization'
const env = import.meta.env
// 创建实例
const axiosInstance = new Request({
baseURL: env.VITE_BASE_URL,
timeout: 30 * 1000, // 超时配置
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
// 请求拦截
axiosInstance.interceptors.request.use(
config => {
const token = ''
if (token) {
config.headers[tokeyKey] = ''
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截
axiosInstance.interceptors.response.use(
response => {
// console.log('response=>', response)
const { status, data, config } = response
if (status === 200) {
const { code, message, data: resData } = data
if (code === '0000') {
return resData
}
// 文件
if (config?.responseType === 'blob') {
return response
}
// token过期
if (code === '5010') {
uni.showToast({
title: 'token过期请重新登陆',
icon: 'none'
})
// userStore.logout(false)
return
}
uni.showToast({
title: message || '接口请求异常',
icon: 'none'
})
return Promise.reject(data)
}
uni.showToast({
title: `${status}服务器响应异常`,
icon: 'none'
})
return Promise.reject(data)
},
error => {
// 如果是取消请求返回空的Promise避免触发异常处理
if (axiosInstance.isCancel(error)) {
return new Promise(() => {})
}
const { response, code, message, config } = error
if (!response) {
uni.showToast({
title: '连接超时,请稍后重试',
icon: 'none'
})
} else {
const { _retry } = config
const { status } = response
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1 && !_retry) {
uni.showToast({
title: '连接超时,请稍后重试',
icon: 'none'
})
} else {
uni.showToast({
title: `${status}`,
icon: 'none'
})
}
}
return Promise.reject(error)
}
)
/**
* 上传文件
* @param {*} url
* @param {*} data
* @returns
*/
export const upload = (url, data) => {
let formData = new FormData()
Object.keys(data || []).forEach(key => {
formData.append(key, data[key])
})
return axiosInstance.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
export default axiosInstance

19
src/api/index.js Normal file
View File

@@ -0,0 +1,19 @@
import net from './request.js';
// import store from '../store';
const API = {
/*
用户相关
*/
login: data => net.post('/user/login', data, false), // 登录
getUserInfo: () => net.get('/user/info', {}, true), // 获取用户信息
getUserInfoById: id => net.get(`/user/info/id`, {}, true), // 获取用户信息
updateUserInfo: data => net.put('/user/info', data, true), // 更新用户信息
upload: data => net.post('/user/upload', data, true), // 上传头像
getLoverInfo: () => net.get('/user/lover', {}, true), // 获取另一半信息
};
export default API

View File

@@ -1,12 +0,0 @@
import http from '@/api/http.js'
const VITE_BASE_URL_AUTH = import.meta.env.VITE_BASE_URL_AUTH // 认证中心
export default {
login: data => {
return http.request({
baseURL: VITE_BASE_URL_AUTH,
url: '/login',
method: 'post',
data
})
}
}

150
src/api/request.js Normal file
View File

@@ -0,0 +1,150 @@
import config from '../config'
const LOGIN_PAGE = '/pages/user/login'
/**
* 获取本地 token
*/
function getToken() {
return uni.getStorageSync('token') || ''
}
/**
* 统一处理未登录或被踢下线
*/
function handleAuthFail(checkLogin = true) {
console.log('开始清理登录信息并跳转登录页')
uni.removeStorageSync('token')
uni.removeStorageSync('tokenExpiresAt')
uni.removeStorageSync('userInfo')
if (checkLogin) {
console.log('准备跳转到登录页:', LOGIN_PAGE)
// 使用 reLaunch 强制跳转到登录页,避免用户返回到已失效的页面
uni.reLaunch({
url: LOGIN_PAGE,
success: () => {
console.log('跳转登录页成功')
},
fail: (err) => {
console.log('跳转登录页失败:', err)
}
})
}
}
/**
* 构建请求头
*/
function buildHeaders(customHeaders = {}) {
return {
'Content-Type': 'application/json',
'Authorization': getToken(),
...customHeaders
}
}
/**
* 通用请求方法
*/
function baseRequest({ url, method = 'GET', data = {}, checkLogin = true, headers = {} }) {
return new Promise((resolve, reject) => {
// 确保URL拼接正确避免重复的斜杠
const fullUrl = config.baseUrl.replace(/\/$/, '') + '/' + url.replace(/^\//, '')
console.log('请求URL:', fullUrl)
uni.request({
url: fullUrl,
method,
data,
header: buildHeaders(headers),
success: (res) => {
const { statusCode, data: resData } = res
if (statusCode === 200 && resData) {
// 账号在其他设备登录
if (resData.code === 409) {
uni.showModal({
title: '提示',
content: '您的账号已在其他设备登录,已强制下线!',
showCancel: false,
success: () => handleAuthFail(checkLogin)
})
resolve(resData)
return
}
// 用户认证失败
if (resData.code === 5) {
handleAuthFail(checkLogin)
resolve(resData)
return
}
// 授权已过期
if (resData.code === 5) {
console.log('检测到授权过期code=7准备跳转登录页')
uni.showToast({
title: '授权已过期,请重新登录',
icon: 'none'
})
handleAuthFail(checkLogin)
resolve(resData)
return
}
resolve(resData)
} else {
reject({
code: statusCode,
msg: res.errMsg || '网络错误',
data: null
})
}
},
fail: (err) => {
console.log('请求失败:', err)
// 处理401未授权错误
if (err.statusCode === 401 || err.code === 401) {
console.log('检测到401未授权准备跳转登录页')
uni.showToast({
title: '授权已过期,请重新登录',
icon: 'none'
})
handleAuthFail(true)
reject({
code: 7,
msg: '授权已过期',
data: null
})
return
}
reject({
code: 0,
msg: err.errMsg || '请求失败',
data: null
})
}
})
})
}
/**
* 导出常用请求方法
*/
const request = {
get(url, data = {}, checkLogin = true, headers = {}) {
return baseRequest({ url, method: 'GET', data, checkLogin, headers })
},
post(url, data = {}, checkLogin = true, headers = {}) {
return baseRequest({ url, method: 'POST', data, checkLogin, headers })
},
put(url, data = {}, checkLogin = true, headers = {}) {
return baseRequest({ url, method: 'PUT', data, checkLogin, headers })
},
delete(url, data = {}, checkLogin = true, headers = {}) {
return baseRequest({ url, method: 'DELETE', data, checkLogin, headers })
},
// 兼容原有的http.request方法
request: ({ url, method = 'GET', data = {}, headers = {} }) => {
return baseRequest({ url, method, data, checkLogin: true, headers })
}
}
export default request

18
src/config/index.js Normal file
View File

@@ -0,0 +1,18 @@
// isdev 为 true 表示开发环境 false 表示发布环境
const isdev = true;
// 开发环境使用代理生产环境使用完整URL
const baseUrl = isdev ? 'http://127.0.0.1:8888/' : 'http://lckt.hnlc5588.cn/';// 开发环境代理 & 生产环境
const shareUrl = isdev ? 'https://h5.gwkjxb.com/' : 'http://test_h5.gwkjxb.com/';
const config = {
appName: '小呆呆的私人菜谱',
baseUrl,
appVersion: '1.0.0',
developer: '小呆呆的私人菜谱',
shareUrl,
appID:'wx8ed262fbd9eaaf74',
isdev
}
export default config

View File

@@ -1,6 +1,6 @@
{
"name" : "zy",
"appid" : "",
"name" : "xdd-menu",
"appid" : "__UNI__3FD9A6A",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
@@ -44,7 +44,7 @@
},
"quickapp" : {},
"mp-weixin" : {
"appid" : "",
"appid" : "wx8ed262fbd9eaaf74",
"mergeVirtualHostAttributes" : true,
"setting" : {
"urlCheck" : false,

View File

@@ -2,7 +2,6 @@
"easycom": {
"autoscan": true,
"custom": {
// uni-ui 规则如下配置
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
"^u--(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^u-([^-].*)": "uview-plus/components/u-$1/u-$1.vue",
@@ -12,59 +11,135 @@
}
},
"pages": [
//pages数组中第一项表示应用启动页参考https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/user/login",
"style": {
"navigationBarTitleText": "登录",
"enablePullDownRefresh": false,
"navigationStyle": "custom"
}
},
{
"path": "pages/user/setup",
"style": {
"navigationBarTitleText": "完善信息",
"enablePullDownRefresh": false,
"navigationStyle": "custom"
}
},
{
"path": "pages/home/home",
"style": {
"navigationBarTitleText": "",
"navigationBarTitleText": "我们的纪念日",
"enablePullDownRefresh": false,
"navigationStyle": "custom"
}
},
{
"path": "pages/recipe/recipe",
"style": {
"navigationBarTitleText": "私人菜谱",
"enablePullDownRefresh": false
}
},
{
"path": "pages/components/components",
"path": "pages/recipe/create",
"style": {
"navigationBarTitleText": "",
"navigationBarTitleText": "创建菜品",
"enablePullDownRefresh": false
}
},
{
"path": "pages/functions/functions",
"path": "pages/recipe/detail",
"style": {
"navigationBarTitleText": "",
"navigationBarTitleText": "菜品详情",
"enablePullDownRefresh": false
}
},
{
"path": "pages/uploadDemo/uploadDemo",
"path": "pages/order/order",
"style": {
"navigationBarTitleText": "",
"navigationBarTitleText": "点餐",
"enablePullDownRefresh": false
}
},
{
"path": "pages/diary/diary",
"style": {
"navigationBarTitleText": "我们的日志",
"enablePullDownRefresh": false
}
},
{
"path": "pages/diary/create",
"style": {
"navigationBarTitleText": "写日志",
"enablePullDownRefresh": false
}
},
{
"path": "pages/checkin/checkin",
"style": {
"navigationBarTitleText": "打卡目标",
"enablePullDownRefresh": false
}
},
{
"path": "pages/checkin/create",
"style": {
"navigationBarTitleText": "创建目标",
"enablePullDownRefresh": false
}
},
{
"path": "pages/profile/profile",
"style": {
"navigationBarTitleText": "个人中心",
"enablePullDownRefresh": false
}
}
],
"globalStyle": {
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "订单中心",
"navigationStyle": "custom",
"navigationBarTitleText": "情侣菜谱",
"navigationStyle": "default",
"navigationBarTextStyle": "black"
},
"tabBar": {
"color": "#333333",
"selectedColor": "#2E69FF",
"color": "#999999",
"selectedColor": "#FF6B9D",
"borderStyle": "white",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/home/home",
"text": "首页"
"text": "首页",
"iconPath": "./static/images/home.png",
"selectedIconPath": "./static/images/home-active.png"
},
{
"pagePath": "pages/components/components",
"text": "组件"
"pagePath": "pages/recipe/recipe",
"text": "菜谱",
"iconPath": "./static/images/recipe.png",
"selectedIconPath": "./static/images/recipe-active.png"
},
{
"pagePath": "pages/functions/functions",
"text": "功能"
"pagePath": "pages/diary/diary",
"text": "日志",
"iconPath": "./static/images/diary.png",
"selectedIconPath": "./static/images/diary-active.png"
},
{
"pagePath": "pages/checkin/checkin",
"text": "打卡",
"iconPath": "./static/images/checkin.png",
"selectedIconPath": "./static/images/checkin-active.png"
},
{
"pagePath": "pages/profile/profile",
"text": "我的",
"iconPath": "./static/images/profile.png",
"selectedIconPath": "./static/images/profile-active.png"
}
]
}

View File

@@ -0,0 +1,577 @@
<template>
<view class="checkin-container">
<!-- 进度概览 -->
<view class="progress-overview">
<view class="progress-header">
<text class="progress-title">打卡进度</text>
<text class="progress-subtitle">已完成 {{ completedCount }}/{{ totalCount }}</text>
</view>
<view class="progress-bar">
<view class="progress-fill" :style="{ width: progressPercentage + '%' }"></view>
</view>
<view class="progress-stats">
<view class="stat-item">
<text class="stat-number">{{ completedCount }}</text>
<text class="stat-label">已完成</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ inProgressCount }}</text>
<text class="stat-label">进行中</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ streakDays }}</text>
<text class="stat-label">连续天数</text>
</view>
</view>
</view>
<!-- 目标分类 -->
<view class="category-tabs">
<view
v-for="(tab, index) in categoryTabs"
:key="index"
class="tab-item"
:class="{ active: activeTab === tab.id }"
@click="switchTab(tab.id)"
>
<text class="tab-text">{{ tab.name }}</text>
<text class="tab-count">({{ tab.count }})</text>
</view>
</view>
<!-- 目标列表 -->
<view class="goal-list">
<view
v-for="(goal, index) in filteredGoals"
:key="goal.id"
class="goal-item"
@click="viewGoal(goal)"
>
<view class="goal-header">
<view class="goal-icon">
<u-icon :name="goal.icon" size="24" :color="goal.color"></u-icon>
</view>
<view class="goal-info">
<text class="goal-name">{{ goal.name }}</text>
<text class="goal-desc">{{ goal.description }}</text>
</view>
<view class="goal-status" :class="goal.status">
<text class="status-text">{{ getStatusText(goal.status) }}</text>
</view>
</view>
<view class="goal-progress">
<view class="progress-info">
<text class="progress-text">进度: {{ goal.completedCount }}/{{ goal.totalCount }}</text>
<text class="progress-percentage">{{ Math.round((goal.completedCount / goal.totalCount) * 100) }}%</text>
</view>
<view class="progress-bar-mini">
<view
class="progress-fill-mini"
:style="{ width: (goal.completedCount / goal.totalCount) * 100 + '%' }"
></view>
</view>
</view>
<view class="goal-footer">
<view class="goal-tags">
<view
v-for="(tag, tagIndex) in goal.tags"
:key="tagIndex"
class="goal-tag"
>
<text class="tag-text">{{ tag }}</text>
</view>
</view>
<view class="goal-actions">
<view
v-if="goal.status === 'in-progress'"
class="checkin-btn"
@click.stop="checkinGoal(goal)"
>
<u-icon name="checkmark" size="16" color="#fff"></u-icon>
<text class="checkin-text">打卡</text>
</view>
<view
v-else-if="goal.status === 'completed'"
class="completed-badge"
>
<u-icon name="checkmark-circle" size="20" color="#52c41a"></u-icon>
<text class="completed-text">已完成</text>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="filteredGoals.length === 0" class="empty-state">
<u-empty
icon="target"
text="还没有打卡目标"
mode="data"
></u-empty>
<view class="empty-action">
<u-button
type="primary"
text="创建第一个目标"
@click="createGoal"
></u-button>
</view>
</view>
<!-- 悬浮添加按钮 -->
<view class="fab-button" @click="createGoal">
<u-icon name="plus" size="24" color="#fff"></u-icon>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
// 响应式数据
const activeTab = ref('all')
const categoryTabs = ref([
{ id: 'all', name: '全部', count: 0 },
{ id: 'daily', name: '日常', count: 0 },
{ id: 'weekly', name: '每周', count: 0 },
{ id: 'monthly', name: '每月', count: 0 },
{ id: 'special', name: '特殊', count: 0 }
])
const goals = ref([
{
id: 1,
name: '一起做饭',
description: '每天一起做一顿饭,增进感情',
icon: 'home',
color: '#FF6B9D',
status: 'in-progress',
category: 'daily',
totalCount: 30,
completedCount: 15,
tags: ['日常', '做饭', '感情'],
createTime: '2025-01-01',
deadline: '2025-02-01'
},
{
id: 2,
name: '一起运动',
description: '每周一起运动3次保持健康',
icon: 'heart',
color: '#52c41a',
status: 'in-progress',
category: 'weekly',
totalCount: 12,
completedCount: 8,
tags: ['运动', '健康', '每周'],
createTime: '2025-01-01',
deadline: '2025-04-01'
},
{
id: 3,
name: '一起旅行',
description: '每月一起去一个新地方',
icon: 'map',
color: '#1890ff',
status: 'completed',
category: 'monthly',
totalCount: 3,
completedCount: 3,
tags: ['旅行', '探索', '每月'],
createTime: '2025-01-01',
deadline: '2025-04-01'
},
{
id: 4,
name: '一起学习',
description: '一起学习新技能,共同成长',
icon: 'book',
color: '#722ed1',
status: 'in-progress',
category: 'special',
totalCount: 20,
completedCount: 12,
tags: ['学习', '成长', '技能'],
createTime: '2025-01-01',
deadline: '2025-06-01'
}
])
// 计算属性
const totalCount = computed(() => goals.value.length)
const completedCount = computed(() =>
goals.value.filter(goal => goal.status === 'completed').length
)
const inProgressCount = computed(() =>
goals.value.filter(goal => goal.status === 'in-progress').length
)
const streakDays = computed(() => {
// 这里可以计算连续打卡天数
return 7
})
const progressPercentage = computed(() => {
if (totalCount.value === 0) return 0
return Math.round((completedCount.value / totalCount.value) * 100)
})
const filteredGoals = computed(() => {
if (activeTab.value === 'all') {
return goals.value
}
return goals.value.filter(goal => goal.category === activeTab.value)
})
// 方法
const switchTab = (tabId) => {
activeTab.value = tabId
}
const getStatusText = (status) => {
const statusMap = {
'in-progress': '进行中',
'completed': '已完成',
'paused': '已暂停'
}
return statusMap[status] || '未知'
}
const viewGoal = (goal) => {
uni.navigateTo({
url: `/pages/checkin/detail?id=${goal.id}`
})
}
const createGoal = () => {
uni.navigateTo({
url: '/pages/checkin/create'
})
}
const checkinGoal = (goal) => {
if (goal.completedCount < goal.totalCount) {
goal.completedCount++
// 检查是否完成
if (goal.completedCount >= goal.totalCount) {
goal.status = 'completed'
uni.showToast({
title: '恭喜完成目标!',
icon: 'success'
})
} else {
uni.showToast({
title: '打卡成功!',
icon: 'success'
})
}
}
}
// 初始化分类数量
const initCategoryCounts = () => {
categoryTabs.value.forEach(tab => {
if (tab.id === 'all') {
tab.count = goals.value.length
} else {
tab.count = goals.value.filter(goal => goal.category === tab.id).length
}
})
}
// 生命周期
onMounted(() => {
initCategoryCounts()
})
</script>
<style lang="scss" scoped>
.checkin-container {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 120rpx;
}
.progress-overview {
background: linear-gradient(135deg, #FF6B9D 0%, #FF8E9E 100%);
padding: 40rpx 30rpx;
color: #fff;
.progress-header {
text-align: center;
margin-bottom: 30rpx;
.progress-title {
display: block;
font-size: 36rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.progress-subtitle {
font-size: 28rpx;
opacity: 0.9;
}
}
.progress-bar {
width: 100%;
height: 12rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 6rpx;
overflow: hidden;
margin-bottom: 30rpx;
.progress-fill {
height: 100%;
background: #fff;
border-radius: 6rpx;
transition: width 0.3s ease;
}
}
.progress-stats {
display: flex;
justify-content: space-around;
.stat-item {
text-align: center;
.stat-number {
display: block;
font-size: 40rpx;
font-weight: bold;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 24rpx;
opacity: 0.9;
}
}
}
}
.category-tabs {
background: #fff;
padding: 20rpx 0;
margin-bottom: 20rpx;
.tab-item {
display: inline-block;
padding: 16rpx 32rpx;
margin: 0 15rpx;
border-radius: 25rpx;
transition: all 0.3s;
.tab-text {
font-size: 28rpx;
color: #666;
}
.tab-count {
font-size: 24rpx;
color: #999;
margin-left: 5rpx;
}
&.active {
background: #FF6B9D;
.tab-text,
.tab-count {
color: #fff;
}
}
}
}
.goal-list {
padding: 0 30rpx;
.goal-item {
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.goal-header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.goal-icon {
width: 60rpx;
height: 60rpx;
background: #f8f8f8;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.goal-info {
flex: 1;
.goal-name {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.goal-desc {
font-size: 26rpx;
color: #999;
line-height: 1.4;
}
}
.goal-status {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
&.in-progress {
background: #e6f7ff;
color: #1890ff;
}
&.completed {
background: #f6ffed;
color: #52c41a;
}
&.paused {
background: #fff7e6;
color: #fa8c16;
}
}
}
.goal-progress {
margin-bottom: 20rpx;
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
.progress-text {
font-size: 26rpx;
color: #666;
}
.progress-percentage {
font-size: 26rpx;
color: #FF6B9D;
font-weight: bold;
}
}
.progress-bar-mini {
width: 100%;
height: 8rpx;
background: #f0f0f0;
border-radius: 4rpx;
overflow: hidden;
.progress-fill-mini {
height: 100%;
background: linear-gradient(135deg, #FF6B9D 0%, #FF8E9E 100%);
border-radius: 4rpx;
transition: width 0.3s ease;
}
}
}
.goal-footer {
display: flex;
justify-content: space-between;
align-items: center;
.goal-tags {
display: flex;
gap: 15rpx;
.goal-tag {
background: #f8f8f8;
padding: 8rpx 16rpx;
border-radius: 20rpx;
.tag-text {
font-size: 22rpx;
color: #666;
}
}
}
.goal-actions {
.checkin-btn {
display: flex;
align-items: center;
gap: 8rpx;
background: #FF6B9D;
color: #fff;
padding: 12rpx 24rpx;
border-radius: 25rpx;
font-size: 24rpx;
.checkin-text {
color: #fff;
}
}
.completed-badge {
display: flex;
align-items: center;
gap: 8rpx;
.completed-text {
font-size: 24rpx;
color: #52c41a;
}
}
}
}
}
}
.empty-state {
padding: 100rpx 0;
text-align: center;
.empty-action {
margin-top: 40rpx;
}
}
.fab-button {
position: fixed;
right: 40rpx;
bottom: 120rpx;
width: 100rpx;
height: 100rpx;
background: linear-gradient(135deg, #FF6B9D 0%, #FF8E9E 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 30rpx rgba(255, 107, 157, 0.3);
z-index: 999;
&:active {
transform: scale(0.9);
}
}
</style>

View File

@@ -1,46 +0,0 @@
<template>
<custom-page>
<custom-head title-text="组件"></custom-head>
<!-- 使用scroll-page 组件的话,需要加上content-container才能让里面的元素继承外部的高度-->
<scroll-page class="content-container">
<view class="list-item" v-for="item in list" :key="item.name" @tap="item.callback">
<view>{{ item.name }}</view>
<up-icon name="arrow-right"></up-icon>
</view>
</scroll-page>
<cu-tabbar></cu-tabbar>
</custom-page>
<city-picker :visible="cityPickerVisible" @cancel="cityPickerVisible = false"></city-picker>
</template>
<script setup>
const cityPickerVisible = ref(false)
const list = ref([
{
name: '省市区选择',
callback() {
cityPickerVisible.value = true
}
},
{
name: '图片视频上传',
callback() {
uni.navigateTo({
url: '/pages/uploadDemo/uploadDemo'
})
}
}
])
</script>
<style lang="scss">
.list-item {
height: 80rpx;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 30rpx;
border-bottom: 1rpx solid #ddd;
}
</style>

457
src/pages/diary/diary.vue Normal file
View File

@@ -0,0 +1,457 @@
<template>
<view class="diary-container">
<!-- 头部统计 -->
<view class="header-stats">
<view class="stats-item">
<text class="stats-number">{{ totalDiaries }}</text>
<text class="stats-label">总日志</text>
</view>
<view class="stats-divider"></view>
<view class="stats-item">
<text class="stats-number">{{ thisMonthDiaries }}</text>
<text class="stats-label">本月</text>
</view>
<view class="stats-divider"></view>
<view class="stats-item">
<text class="stats-number">{{ todayDiaries }}</text>
<text class="stats-label">今天</text>
</view>
</view>
<!-- 日志列表 -->
<view class="diary-list">
<view
v-for="(diary, index) in diaryList"
:key="diary.id"
class="diary-item"
@click="viewDiary(diary)"
>
<view class="diary-header">
<view class="diary-avatar">
<u-avatar :src="diary.author.avatar" size="40" shape="circle"></u-avatar>
</view>
<view class="diary-info">
<text class="diary-author">{{ diary.author.name }}</text>
<text class="diary-time">{{ formatTime(diary.createTime) }}</text>
</view>
<view class="diary-mood">
<u-icon :name="getMoodIcon(diary.mood)" size="20" :color="getMoodColor(diary.mood)"></u-icon>
</view>
</view>
<view class="diary-content">
<text class="diary-title">{{ diary.title }}</text>
<text class="diary-text">{{ diary.content }}</text>
</view>
<view v-if="diary.images && diary.images.length > 0" class="diary-images">
<view
v-for="(image, imgIndex) in diary.images.slice(0, 3)"
:key="imgIndex"
class="diary-image"
>
<u-image
:src="image"
width="120rpx"
height="120rpx"
radius="8rpx"
mode="aspectFill"
></u-image>
</view>
<view v-if="diary.images.length > 3" class="more-images">
<text class="more-text">+{{ diary.images.length - 3 }}</text>
</view>
</view>
<view class="diary-footer">
<view class="diary-tags">
<view
v-for="(tag, tagIndex) in diary.tags"
:key="tagIndex"
class="diary-tag"
>
<text class="tag-text">{{ tag }}</text>
</view>
</view>
<view class="diary-actions">
<view class="action-item" @click.stop="likeDiary(diary)">
<u-icon
:name="diary.isLiked ? 'heart-fill' : 'heart'"
size="16"
:color="diary.isLiked ? '#FF6B9D' : '#999'"
></u-icon>
<text class="action-text">{{ diary.likes }}</text>
</view>
<view class="action-item" @click.stop="commentDiary(diary)">
<u-icon name="chat" size="16" color="#999"></u-icon>
<text class="action-text">{{ diary.comments }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="diaryList.length === 0" class="empty-state">
<u-empty
icon="edit-pen"
text="还没有日志记录"
mode="data"
></u-empty>
<view class="empty-action">
<u-button
type="primary"
text="写第一篇日志"
@click="createDiary"
></u-button>
</view>
</view>
<!-- 悬浮添加按钮 -->
<view class="fab-button" @click="createDiary">
<u-icon name="edit-pen" size="24" color="#fff"></u-icon>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
// 响应式数据
const diaryList = ref([
{
id: 1,
title: '今天一起做饭很开心',
content: '今天和另一半一起做了红烧肉,虽然卖相一般,但是味道很棒!一起在厨房忙碌的感觉真的很温馨,希望以后能经常这样。',
author: {
name: '小美',
avatar: '/static/images/avatar1.jpg'
},
createTime: new Date('2025-01-15 18:30:00'),
mood: 'happy',
images: [
'/static/images/diary1.jpg',
'/static/images/diary2.jpg'
],
tags: ['做饭', '温馨', '美食'],
likes: 12,
comments: 3,
isLiked: false
},
{
id: 2,
title: '周末约会记录',
content: '周末去了新开的咖啡店,环境很好,咖啡也很香。最重要的是和喜欢的人在一起,做什么都开心。',
author: {
name: '小明',
avatar: '/static/images/avatar2.jpg'
},
createTime: new Date('2025-01-14 15:20:00'),
mood: 'excited',
images: [
'/static/images/diary3.jpg'
],
tags: ['约会', '咖啡', '周末'],
likes: 8,
comments: 2,
isLiked: true
}
])
// 计算属性
const totalDiaries = computed(() => diaryList.value.length)
const thisMonthDiaries = computed(() => {
const now = new Date()
const thisMonth = now.getMonth()
const thisYear = now.getFullYear()
return diaryList.value.filter(diary => {
const diaryDate = new Date(diary.createTime)
return diaryDate.getMonth() === thisMonth && diaryDate.getFullYear() === thisYear
}).length
})
const todayDiaries = computed(() => {
const now = new Date()
const today = now.toDateString()
return diaryList.value.filter(diary => {
const diaryDate = new Date(diary.createTime)
return diaryDate.toDateString() === today
}).length
})
// 方法
const formatTime = (time) => {
const now = new Date()
const diaryTime = new Date(time)
const diffTime = now - diaryTime
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
if (diffDays === 0) {
return '今天'
} else if (diffDays === 1) {
return '昨天'
} else if (diffDays < 7) {
return `${diffDays}天前`
} else {
return diaryTime.toLocaleDateString()
}
}
const getMoodIcon = (mood) => {
const moodIcons = {
happy: 'smile',
excited: 'star',
sad: 'sad',
angry: 'close',
normal: 'heart'
}
return moodIcons[mood] || 'heart'
}
const getMoodColor = (mood) => {
const moodColors = {
happy: '#FFD700',
excited: '#FF6B9D',
sad: '#87CEEB',
angry: '#FF6347',
normal: '#FF6B9D'
}
return moodColors[mood] || '#FF6B9D'
}
const viewDiary = (diary) => {
uni.navigateTo({
url: `/pages/diary/detail?id=${diary.id}`
})
}
const createDiary = () => {
uni.navigateTo({
url: '/pages/diary/create'
})
}
const likeDiary = (diary) => {
diary.isLiked = !diary.isLiked
if (diary.isLiked) {
diary.likes++
} else {
diary.likes--
}
}
const commentDiary = (diary) => {
uni.showToast({
title: '评论功能开发中',
icon: 'none'
})
}
// 生命周期
onMounted(() => {
// 可以在这里加载数据
})
</script>
<style lang="scss" scoped>
.diary-container {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 120rpx;
}
.header-stats {
display: flex;
align-items: center;
justify-content: space-around;
background: #fff;
padding: 40rpx 30rpx;
margin-bottom: 20rpx;
.stats-item {
text-align: center;
.stats-number {
display: block;
font-size: 48rpx;
font-weight: bold;
color: #FF6B9D;
margin-bottom: 10rpx;
}
.stats-label {
font-size: 24rpx;
color: #999;
}
}
.stats-divider {
width: 2rpx;
height: 60rpx;
background: #f0f0f0;
}
}
.diary-list {
padding: 0 30rpx;
.diary-item {
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.diary-header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.diary-avatar {
margin-right: 20rpx;
}
.diary-info {
flex: 1;
.diary-author {
display: block;
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 5rpx;
}
.diary-time {
font-size: 24rpx;
color: #999;
}
}
.diary-mood {
width: 40rpx;
height: 40rpx;
background: #f8f8f8;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
}
.diary-content {
margin-bottom: 20rpx;
.diary-title {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 15rpx;
line-height: 1.4;
}
.diary-text {
display: block;
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
}
.diary-images {
display: flex;
gap: 15rpx;
margin-bottom: 20rpx;
.diary-image {
position: relative;
}
.more-images {
width: 120rpx;
height: 120rpx;
background: rgba(0, 0, 0, 0.5);
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
.more-text {
color: #fff;
font-size: 24rpx;
}
}
}
.diary-footer {
display: flex;
justify-content: space-between;
align-items: center;
.diary-tags {
display: flex;
gap: 15rpx;
.diary-tag {
background: #f8f8f8;
padding: 8rpx 16rpx;
border-radius: 20rpx;
.tag-text {
font-size: 22rpx;
color: #666;
}
}
}
.diary-actions {
display: flex;
gap: 30rpx;
.action-item {
display: flex;
align-items: center;
gap: 8rpx;
.action-text {
font-size: 24rpx;
color: #999;
}
}
}
}
}
}
.empty-state {
padding: 100rpx 0;
text-align: center;
.empty-action {
margin-top: 40rpx;
}
}
.fab-button {
position: fixed;
right: 40rpx;
bottom: 120rpx;
width: 100rpx;
height: 100rpx;
background: linear-gradient(135deg, #FF6B9D 0%, #FF8E9E 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 30rpx rgba(255, 107, 157, 0.3);
z-index: 999;
&:active {
transform: scale(0.9);
}
}
</style>

View File

@@ -1,50 +0,0 @@
<template>
<custom-page>
<custom-head title-text="功能"></custom-head>
<!-- 使用scroll-page 组件的话,需要加上content-container才能让里面的元素继承外部的高度-->
<scroll-page class="content-container">
<div>
<up-radio-group v-model="themeValue" placement="column" @change="groupChange">
<up-radio
:customStyle="{ marginBottom: '8px' }"
v-for="(item, index) in themeList"
:key="index"
:label="item.label"
:name="item.name"
></up-radio>
</up-radio-group>
</div>
<div class="demo-btn">皮肤测试</div>
</scroll-page>
<cu-tabbar></cu-tabbar>
</custom-page>
</template>
<script setup>
import { useSystemStore } from '@/store/system.js'
const systemStore = useSystemStore()
const themeList = ref([
{
name: 'default-theme',
label: '默认主题'
},
{
name: 'red-theme',
label: '红色主题'
}
])
const themeValue = ref('default-theme')
const groupChange = name => {
systemStore.setThemeName(name)
}
</script>
<style lang="scss">
.demo-btn {
background: var(--color-primary);
}
</style>

View File

@@ -1,15 +1,450 @@
<template>
<custom-page>
<custom-head title-text="演示界面"></custom-head>
<!-- 使用scroll-page 组件的话,需要加上content-container才能让里面的元素继承外部的高度-->
<scroll-page class="content-container">
<div class="h-[3000rpx]">自适应的内容滚动区域,兼容IOS下不显示滚动条</div>
</scroll-page>
<view class="bg-red-500 h-[200rpx]">占位盒子</view>
<view class="home-container">
<!-- 自定义头部 -->
<view class="custom-header">
<view class="status-bar"></view>
<view class="header-content">
<view class="header-title">情侣菜谱</view>
<view class="header-actions">
<u-icon name="more-dot-fill" size="20" color="#fff"></u-icon>
</view>
</view>
</view>
<cu-tabbar></cu-tabbar>
</custom-page>
<!-- 主要内容区域 -->
<view class="main-content">
<!-- 情侣头像和恋爱天数 -->
<view class="couple-info">
<view class="avatar-section">
<view class="avatar left-avatar">
<u-avatar
:src="userInfo.avatar"
size="60"
shape="circle"
:key="userInfo.avatar"
></u-avatar>
</view>
<view class="heart-icon">
<u-icon name="heart-fill" size="24" color="#FF6B9D"></u-icon>
</view>
<view class="avatar right-avatar">
<u-avatar :src="partnerInfo.avatar" size="60" shape="circle"></u-avatar>
<view v-if="!partnerInfo.avatar" class="add-partner">
<u-icon name="plus" size="24" color="#FF6B9D"></u-icon>
</view>
</view>
</view>
<view class="relationship-info">
<text class="relationship-text">我们在一起已经</text>
<view class="days-count">
<text class="days-number">{{ loveDays }}</text>
<text class="days-unit"></text>
</view>
<text class="start-date">{{ startDate }}</text>
</view>
</view>
<!-- 快捷功能卡片 -->
<view class="feature-cards">
<view class="feature-card diary-card" @click="goToDiary">
<view class="card-content">
<text class="card-title">写日志</text>
<u-icon name="edit-pen" size="32" color="#fff"></u-icon>
</view>
</view>
<view class="feature-card order-card" @click="goToOrder">
<view class="card-content">
<text class="card-title">点餐下单</text>
<u-icon name="shopping-cart" size="32" color="#fff"></u-icon>
</view>
</view>
</view>
<!-- 纪念日列表 -->
<view class="anniversary-section">
<view class="section-header">
<text class="section-title">重要纪念日</text>
<view class="add-anniversary" @click="addAnniversary">
<u-icon name="plus" size="20" color="#FF6B9D"></u-icon>
</view>
</view>
<view class="anniversary-list">
<view
v-for="(item, index) in anniversaryList"
:key="index"
class="anniversary-item"
@click="viewAnniversary(item)"
>
<view class="anniversary-icon">
<u-icon :name="item.icon" size="24" color="#FF6B9D"></u-icon>
</view>
<view class="anniversary-info">
<text class="anniversary-name">{{ item.name }}</text>
<text class="anniversary-date">{{ item.date }}</text>
</view>
<view class="anniversary-countdown">
<text class="countdown-text">{{ item.countdown }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup></script>
<style lang="scss"></style>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useUserStore } from '@/store/user.js'
const userStore = useUserStore()
// 响应式数据
const userInfo = ref({
avatar: ''
})
const partnerInfo = ref({
avatar: ''
})
const startDate = ref('2025/01/01')
const loveDays = ref(0)
const anniversaryList = ref([
{
id: 1,
name: '恋爱纪念日',
date: '2025/01/01',
countdown: '还有 30 天',
icon: 'heart-fill'
},
{
id: 2,
name: '生日',
date: '2025/03/15',
countdown: '还有 45 天',
icon: 'gift-fill'
}
])
// 计算属性
const computedLoveDays = computed(() => {
// 这里可以根据实际的开始日期计算
return loveDays.value
})
// 方法
const invitePartner = () => {
uni.showToast({
title: '邀请功能开发中',
icon: 'none'
})
}
const goToDiary = () => {
uni.switchTab({
url: '/pages/diary/diary'
})
}
const goToOrder = () => {
uni.switchTab({
url: '/pages/order/order'
})
}
const addAnniversary = () => {
uni.showToast({
title: '添加纪念日功能开发中',
icon: 'none'
})
// TODO: 跳转到添加纪念日页面
// uni.navigateTo({
// url: '/pages/anniversary/create'
// })
}
const viewAnniversary = (item) => {
uni.showToast({
title: `查看${item.name}`,
icon: 'none'
})
// TODO: 跳转到纪念日详情页面
// uni.navigateTo({
// url: `/pages/anniversary/detail?id=${item.id}`
// })
}
// 获取用户信息
const getUserInfo = () => {
try {
// 从缓存获取用户信息
const cachedUser = uni.getStorageSync('userInfo')
if (cachedUser) {
userInfo.value = {
avatar: cachedUser.avatar,
nickname: cachedUser.nick_name || cachedUser.nickname || '用户'
}
console.log(userInfo.value)
} else {
// 如果没有userInfo缓存尝试从user缓存获取
const fallbackUser = uni.getStorageSync('user')
if (fallbackUser && fallbackUser.userInfo) {
userInfo.value = {
avatar: fallbackUser.userInfo.avatar,
nickname: fallbackUser.userInfo.nickname || '用户'
}
}
}
} catch (error) {
console.error('获取用户信息失败:', error)
}
}
// 生命周期
onMounted(() => {
// 获取用户信息
getUserInfo()
// 计算恋爱天数
const start = new Date('2025-01-01')
const today = new Date()
const diffTime = Math.abs(today - start)
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
loveDays.value = diffDays
})
</script>
<style lang="scss" scoped>
.home-container {
min-height: 100vh;
background: linear-gradient(135deg, #FFE4E1 0%, #FFB6C1 50%, #E6E6FA 100%);
}
.custom-header {
background: linear-gradient(135deg, #FF6B9D 0%, #FF8E9E 100%);
padding: 0 20rpx;
.status-bar {
height: var(--status-bar-height);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
.header-title {
color: #fff;
font-size: 36rpx;
font-weight: bold;
}
.header-actions {
display: flex;
gap: 20rpx;
}
}
}
.main-content {
padding: 40rpx 30rpx;
}
.couple-info {
text-align: center;
margin-bottom: 60rpx;
.avatar-section {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 30rpx;
.avatar {
position: relative;
.add-partner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.8);
border-radius: 50%;
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
}
}
.heart-icon {
margin: 0 30rpx;
animation: heartbeat 1.5s ease-in-out infinite;
}
}
.relationship-info {
margin-bottom: 30rpx;
.relationship-text {
display: block;
color: #666;
font-size: 28rpx;
margin-bottom: 20rpx;
}
.days-count {
margin-bottom: 20rpx;
.days-number {
font-size: 80rpx;
font-weight: bold;
color: #FF6B9D;
}
.days-unit {
font-size: 32rpx;
color: #FF6B9D;
margin-left: 10rpx;
}
}
.start-date {
color: #999;
font-size: 24rpx;
}
}
.invite-button {
display: inline-flex;
align-items: center;
gap: 10rpx;
background: linear-gradient(135deg, #FF6B9D 0%, #FF8E9E 100%);
color: #fff;
padding: 20rpx 40rpx;
border-radius: 50rpx;
font-size: 28rpx;
.invite-text {
color: #fff;
}
}
}
.feature-cards {
display: flex;
gap: 30rpx;
margin-bottom: 60rpx;
.feature-card {
flex: 1;
height: 160rpx;
border-radius: 20rpx;
overflow: hidden;
.card-content {
height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 30rpx;
.card-title {
color: #fff;
font-size: 32rpx;
font-weight: bold;
}
}
&.diary-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
&.order-card {
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
}
}
}
.anniversary-section {
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.add-anniversary {
width: 60rpx;
height: 60rpx;
background: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
}
}
.anniversary-list {
.anniversary-item {
display: flex;
align-items: center;
background: #fff;
padding: 30rpx;
border-radius: 20rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.anniversary-icon {
margin-right: 30rpx;
}
.anniversary-info {
flex: 1;
.anniversary-name {
display: block;
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.anniversary-date {
font-size: 24rpx;
color: #999;
}
}
.anniversary-countdown {
.countdown-text {
color: #FF6B9D;
font-size: 26rpx;
}
}
}
}
}
@keyframes heartbeat {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
</style>

562
src/pages/order/order.vue Normal file
View File

@@ -0,0 +1,562 @@
<template>
<view class="order-container">
<!-- 搜索栏 -->
<view class="search-section">
<view class="search-box">
<u-search
v-model="searchKeyword"
placeholder="搜索菜品名称"
:show-action="false"
@search="onSearch"
@custom="onSearch"
></u-search>
</view>
</view>
<!-- 分类标签 -->
<view class="category-section">
<scroll-view scroll-x class="category-scroll">
<view class="category-list">
<view
v-for="(category, index) in categories"
:key="index"
class="category-item"
:class="{ active: selectedCategory === category.id }"
@click="selectCategory(category.id)"
>
<text class="category-name">{{ category.name }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 菜品列表 -->
<view class="dish-list">
<view
v-for="(dish, index) in filteredDishes"
:key="dish.id"
class="dish-item"
>
<view class="dish-image">
<u-image
:src="dish.image"
width="120rpx"
height="120rpx"
radius="12rpx"
mode="aspectFill"
></u-image>
</view>
<view class="dish-info">
<text class="dish-name">{{ dish.name }}</text>
<text class="dish-desc">{{ dish.description }}</text>
<view class="dish-meta">
<view class="meta-item">
<u-icon name="clock" size="14" color="#999"></u-icon>
<text class="meta-text">{{ dish.cookTime }}分钟</text>
</view>
<view class="meta-item">
<u-icon name="star" size="14" color="#FFD700"></u-icon>
<text class="meta-text">{{ dish.difficulty }}</text>
</view>
</view>
</view>
<view class="dish-actions">
<view class="price-section">
<text class="price">¥{{ dish.price }}</text>
<text class="unit">/</text>
</view>
<view class="quantity-control">
<view
v-if="getCartQuantity(dish.id) > 0"
class="quantity-btn minus"
@click="decreaseQuantity(dish)"
>
<u-icon name="minus" size="16" color="#FF6B9D"></u-icon>
</view>
<text v-if="getCartQuantity(dish.id) > 0" class="quantity-text">
{{ getCartQuantity(dish.id) }}
</text>
<view class="quantity-btn plus" @click="addToCart(dish)">
<u-icon name="plus" size="16" color="#fff"></u-icon>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="filteredDishes.length === 0" class="empty-state">
<u-empty
icon="search"
text="暂无相关菜品"
mode="data"
></u-empty>
</view>
<!-- 购物车 -->
<view v-if="cartItems.length > 0" class="cart-section">
<view class="cart-header">
<view class="cart-info">
<u-icon name="shopping-cart" size="24" color="#FF6B9D"></u-icon>
<text class="cart-title">购物车</text>
<text class="cart-count">({{ totalQuantity }})</text>
</view>
<view class="cart-total">
<text class="total-label">总计:</text>
<text class="total-price">¥{{ totalPrice }}</text>
</view>
</view>
<view class="cart-actions">
<view class="clear-cart" @click="clearCart">
<u-icon name="trash" size="20" color="#999"></u-icon>
<text class="clear-text">清空</text>
</view>
<view class="checkout-btn" @click="checkout">
<text class="checkout-text">去下单</text>
<u-icon name="arrow-right" size="16" color="#fff"></u-icon>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
// 响应式数据
const searchKeyword = ref('')
const selectedCategory = ref('all')
const cartItems = ref([])
const categories = ref([
{ id: 'all', name: '全部' },
{ id: 'chinese', name: '中餐' },
{ id: 'western', name: '西餐' },
{ id: 'dessert', name: '甜点' },
{ id: 'drink', name: '饮品' },
{ id: 'snack', name: '小吃' }
])
const dishes = ref([
{
id: 1,
name: '红烧肉',
description: '经典美味的红烧肉,肥而不腻',
image: '/static/images/dish1.jpg',
category: 'chinese',
cookTime: 60,
difficulty: '中等',
price: 28,
ingredients: ['五花肉', '生抽', '老抽', '冰糖', '葱姜蒜']
},
{
id: 2,
name: '提拉米苏',
description: '意大利经典咖啡甜点',
image: '/static/images/dish2.jpg',
category: 'dessert',
cookTime: 30,
difficulty: '简单',
price: 18,
ingredients: ['手指饼干', '马斯卡彭奶酪', '咖啡', '鸡蛋', '糖']
},
{
id: 3,
name: '意式咖啡',
description: '浓郁香醇的意式浓缩咖啡',
image: '/static/images/dish3.jpg',
category: 'drink',
cookTime: 5,
difficulty: '简单',
price: 12,
ingredients: ['咖啡豆', '水']
},
{
id: 4,
name: '宫保鸡丁',
description: '川菜经典,麻辣鲜香',
image: '/static/images/dish4.jpg',
category: 'chinese',
cookTime: 25,
difficulty: '中等',
price: 22,
ingredients: ['鸡胸肉', '花生', '干辣椒', '葱姜蒜', '生抽']
}
])
// 计算属性
const filteredDishes = computed(() => {
let filtered = dishes.value
// 按分类筛选
if (selectedCategory.value !== 'all') {
filtered = filtered.filter(dish => dish.category === selectedCategory.value)
}
// 按关键词搜索
if (searchKeyword.value) {
filtered = filtered.filter(dish =>
dish.name.includes(searchKeyword.value) ||
dish.description.includes(searchKeyword.value)
)
}
return filtered
})
const totalQuantity = computed(() => {
return cartItems.value.reduce((total, item) => total + item.quantity, 0)
})
const totalPrice = computed(() => {
return cartItems.value.reduce((total, item) => total + (item.price * item.quantity), 0)
})
// 方法
const onSearch = () => {
console.log('搜索关键词:', searchKeyword.value)
}
const selectCategory = (categoryId) => {
selectedCategory.value = categoryId
}
const getCartQuantity = (dishId) => {
const cartItem = cartItems.value.find(item => item.id === dishId)
return cartItem ? cartItem.quantity : 0
}
const addToCart = (dish) => {
const existingItem = cartItems.value.find(item => item.id === dish.id)
if (existingItem) {
existingItem.quantity++
} else {
cartItems.value.push({
id: dish.id,
name: dish.name,
price: dish.price,
image: dish.image,
quantity: 1
})
}
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
}
const decreaseQuantity = (dish) => {
const existingItem = cartItems.value.find(item => item.id === dish.id)
if (existingItem) {
if (existingItem.quantity > 1) {
existingItem.quantity--
} else {
// 移除数量为0的商品
const index = cartItems.value.findIndex(item => item.id === dish.id)
cartItems.value.splice(index, 1)
}
}
}
const clearCart = () => {
uni.showModal({
title: '提示',
content: '确定要清空购物车吗?',
success: (res) => {
if (res.confirm) {
cartItems.value = []
uni.showToast({
title: '购物车已清空',
icon: 'success'
})
}
}
})
}
const checkout = () => {
if (cartItems.value.length === 0) {
uni.showToast({
title: '购物车为空',
icon: 'none'
})
return
}
// 这里可以跳转到订单确认页面
uni.showToast({
title: '下单功能开发中',
icon: 'none'
})
}
// 生命周期
onMounted(() => {
// 可以在这里加载数据
})
</script>
<style lang="scss" scoped>
.order-container {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 120rpx;
}
.search-section {
background: #fff;
padding: 20rpx 30rpx;
.search-box {
background: #f8f8f8;
border-radius: 20rpx;
overflow: hidden;
}
}
.category-section {
background: #fff;
padding: 20rpx 0;
margin-bottom: 20rpx;
.category-scroll {
white-space: nowrap;
.category-list {
display: flex;
padding: 0 30rpx;
.category-item {
display: inline-block;
padding: 16rpx 32rpx;
margin-right: 20rpx;
background: #f8f8f8;
border-radius: 25rpx;
transition: all 0.3s;
.category-name {
font-size: 28rpx;
color: #666;
}
&.active {
background: #FF6B9D;
.category-name {
color: #fff;
}
}
}
}
}
}
.dish-list {
padding: 0 30rpx;
.dish-item {
display: flex;
align-items: center;
background: #fff;
padding: 30rpx;
border-radius: 20rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.dish-image {
margin-right: 30rpx;
}
.dish-info {
flex: 1;
.dish-name {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.dish-desc {
display: block;
font-size: 26rpx;
color: #999;
margin-bottom: 20rpx;
line-height: 1.4;
}
.dish-meta {
display: flex;
gap: 30rpx;
.meta-item {
display: flex;
align-items: center;
gap: 8rpx;
.meta-text {
font-size: 24rpx;
color: #999;
}
}
}
}
.dish-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 20rpx;
.price-section {
text-align: right;
.price {
font-size: 32rpx;
font-weight: bold;
color: #FF6B9D;
}
.unit {
font-size: 24rpx;
color: #999;
}
}
.quantity-control {
display: flex;
align-items: center;
gap: 15rpx;
.quantity-btn {
width: 50rpx;
height: 50rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
&.minus {
background: #f8f8f8;
border: 2rpx solid #FF6B9D;
}
&.plus {
background: #FF6B9D;
}
&:active {
transform: scale(0.9);
}
}
.quantity-text {
font-size: 28rpx;
color: #333;
min-width: 40rpx;
text-align: center;
}
}
}
}
}
.empty-state {
padding: 100rpx 0;
}
.cart-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
border-radius: 20rpx 20rpx 0 0;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
z-index: 999;
.cart-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 2rpx solid #f5f5f5;
.cart-info {
display: flex;
align-items: center;
gap: 15rpx;
.cart-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.cart-count {
font-size: 24rpx;
color: #999;
}
}
.cart-total {
display: flex;
align-items: center;
gap: 10rpx;
.total-label {
font-size: 26rpx;
color: #666;
}
.total-price {
font-size: 32rpx;
font-weight: bold;
color: #FF6B9D;
}
}
}
.cart-actions {
display: flex;
padding: 30rpx;
gap: 20rpx;
.clear-cart {
display: flex;
align-items: center;
gap: 8rpx;
padding: 20rpx 30rpx;
background: #f8f8f8;
border-radius: 25rpx;
.clear-text {
font-size: 26rpx;
color: #999;
}
}
.checkout-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
background: linear-gradient(135deg, #FF6B9D 0%, #FF8E9E 100%);
color: #fff;
padding: 20rpx;
border-radius: 25rpx;
.checkout-text {
font-size: 28rpx;
font-weight: bold;
}
}
}
}
</style>

View File

@@ -0,0 +1,567 @@
<template>
<view class="profile-container">
<!-- 用户信息卡片 -->
<view class="user-card">
<view class="user-header">
<view class="user-avatar">
<u-avatar :src="userInfo.avatar" size="120" shape="circle"></u-avatar>
<view class="edit-avatar" @click="editAvatar">
<u-icon name="camera" size="20" color="#fff"></u-icon>
</view>
</view>
<view class="user-info">
<text class="user-name">{{ userInfo.nickname }}</text>
<text class="user-id">ID: {{ userInfo.userId }}</text>
<view class="user-level">
<u-icon name="star" size="16" color="#FFD700"></u-icon>
<text class="level-text">{{ userInfo.level }}</text>
</view>
</view>
<view class="user-actions">
<view class="action-btn" @click="editProfile">
<u-icon name="edit-pen" size="20" color="#FF6B9D"></u-icon>
</view>
</view>
</view>
</view>
<!-- 快捷功能 -->
<view class="quick-functions">
<view class="function-group">
<view class="group-title">我的功能</view>
<view class="function-list">
<view class="function-item" @click="goToMyRecipes">
<view class="function-icon">
<u-icon name="book" size="14" color="#FF6B9D"></u-icon>
</view>
<text class="function-name">我的菜谱</text>
<u-icon name="arrow-right" size="16" color="#ccc"></u-icon>
</view>
<view class="function-item" @click="goToMyDiaries">
<view class="function-icon">
<u-icon name="edit-pen" size="24" color="#52c41a"></u-icon>
</view>
<text class="function-name">我的日志</text>
<u-icon name="arrow-right" size="16" color="#ccc"></u-icon>
</view>
<view class="function-item" @click="goToMyGoals">
<view class="function-icon">
<u-icon name="target" size="14" color="#1890ff"></u-icon>
</view>
<text class="function-name">我的目标</text>
<u-icon name="arrow-right" size="16" color="#ccc"></u-icon>
</view>
</view>
</view>
<view class="function-group">
<view class="group-title">设置</view>
<view class="function-list">
<view class="function-item" @click="goToSettings">
<view class="function-icon">
<u-icon name="setting" size="24" color="#722ed1"></u-icon>
</view>
<text class="function-name">应用设置</text>
<u-icon name="arrow-right" size="16" color="#ccc"></u-icon>
</view>
<view class="function-item" @click="goToPrivacy">
<view class="function-icon">
<u-icon name="lock" size="24" color="#fa8c16"></u-icon>
</view>
<text class="function-name">隐私设置</text>
<u-icon name="arrow-right" size="16" color="#ccc"></u-icon>
</view>
<view class="function-item" @click="goToNotification">
<view class="function-icon">
<u-icon name="bell" size="24" color="#13c2c2"></u-icon>
</view>
<text class="function-name">消息通知</text>
<u-icon name="arrow-right" size="16" color="#ccc"></u-icon>
</view>
</view>
</view>
<view class="function-group">
<view class="group-title">其他</view>
<view class="function-list">
<view class="function-item" @click="goToHelp">
<view class="function-icon">
<u-icon name="question-circle" size="24" color="#eb2f96"></u-icon>
</view>
<text class="function-name">帮助中心</text>
<u-icon name="arrow-right" size="16" color="#ccc"></u-icon>
</view>
<view class="function-item" @click="goToFeedback">
<view class="function-icon">
<u-icon name="chat" size="24" color="#fa541c"></u-icon>
</view>
<text class="function-name">意见反馈</text>
<u-icon name="arrow-right" size="16" color="#ccc"></u-icon>
</view>
<view class="function-item" @click="goToAbout">
<view class="function-icon">
<u-icon name="info-circle" size="24" color="#2f54eb"></u-icon>
</view>
<text class="function-name">关于我们</text>
<u-icon name="arrow-right" size="16" color="#ccc"></u-icon>
</view>
</view>
</view>
</view>
<!-- 退出登录按钮 -->
<view class="logout-section">
<u-button
type="error"
text="退出登录"
@click="logout"
:custom-style="{ margin: '40rpx 30rpx' }"
></u-button>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useUserStore } from '@/store/user.js'
import api from '@/api'
const userStore = useUserStore()
// 响应式数据
const userInfo = ref({
nickname: '',
userId: '',
avatar: '/static/images/default-avatar.png',
level: 'LV.1 新手',
recipeCount: 0,
diaryCount: 0,
checkinCount: 0
})
// 获取用户信息
const getUserInfo = async () => {
try {
uni.showLoading({
title: '加载中...'
})
const result = await api.getUserInfo()
if (result.code === 0 && result.data) {
console.log('获取用户信息成功:', result.data)
// 更新用户信息
userInfo.value = {
nickname: result.data.nick_name || result.data.nickname || '用户',
userId: result.data.ID || result.data.userId || '',
avatar: result.data.avatar || '/static/images/default-avatar.png',
level: getUserLevel(result.data.level || 1),
recipeCount: result.data.recipe_count || result.data.recipeCount || 0,
diaryCount: result.data.diary_count || result.data.diaryCount || 0,
checkinCount: result.data.checkin_count || result.data.checkinCount || 0
}
uni.setStorageSync('user', result.data)
} else {
throw new Error(result.msg || '获取用户信息失败')
}
} catch (error) {
console.error('获取用户信息失败:', error)
uni.showToast({
title: error.message || '获取用户信息失败',
icon: 'none'
})
// 如果获取失败,尝试从缓存读取
const cachedUser = uni.getStorageSync('user')
if (cachedUser && cachedUser.userInfo) {
userInfo.value = {
nickname: cachedUser.userInfo.nickname || '用户',
userId: cachedUser.userInfo.userId || '',
avatar: cachedUser.userInfo.avatar || '/static/images/default-avatar.png',
level: getUserLevel(cachedUser.userInfo.level || 1),
recipeCount: cachedUser.userInfo.recipeCount || 0,
diaryCount: cachedUser.userInfo.diaryCount || 0,
checkinCount: cachedUser.userInfo.checkinCount || 0
}
}
} finally {
uni.hideLoading()
}
}
// 获取用户等级
const getUserLevel = (level) => {
const levelMap = {
1: 'LV.1 新手',
2: 'LV.2 初学者',
3: 'LV.3 进阶者',
4: 'LV.4 熟练者',
5: 'LV.5 美食达人',
6: 'LV.6 厨艺大师',
7: 'LV.7 传奇厨师',
8: 'LV.8 食神'
}
return levelMap[level] || 'LV.1 新手'
}
// 编辑头像
const editAvatar = () => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePath = res.tempFilePaths[0]
console.log('选择的头像:', tempFilePath)
// 上传头像
uploadAvatar(tempFilePath)
}
})
}
// 上传头像
const uploadAvatar = (tempFilePath) => {
uni.showLoading({
title: '上传头像中...'
})
// 获取token
const token = uni.getStorageSync('token')
if (!token) {
uni.hideLoading()
uni.showToast({
title: '未找到登录token',
icon: 'none'
})
return
}
uni.uploadFile({
url: 'http://127.0.0.1:8888/user/avatar',
filePath: tempFilePath,
name: 'avatar',
header: {
'Authorization': `Bearer ${token}`
},
success: (res) => {
uni.hideLoading()
console.log('头像上传响应:', res)
try {
const response = JSON.parse(res.data)
if (response.code === 0) {
// 更新头像
userInfo.value.avatar = response.data.url
// 更新缓存
const cachedUser = uni.getStorageSync('user') || {}
if (cachedUser.userInfo) {
cachedUser.userInfo.avatar = response.data.url
uni.setStorageSync('user', cachedUser)
}
// 更新状态管理
userStore.setUserInfo({
...userStore.userInfo,
avatar: response.data.url
})
uni.showToast({
title: '头像更新成功',
icon: 'success'
})
} else {
throw new Error(response.msg || '头像上传失败')
}
} catch (error) {
console.error('头像上传失败:', error)
uni.showToast({
title: error.message || '头像上传失败',
icon: 'none'
})
}
},
fail: (error) => {
uni.hideLoading()
console.error('头像上传失败:', error)
uni.showToast({
title: '头像上传失败,请重试',
icon: 'none'
})
}
})
}
const editProfile = () => {
uni.navigateTo({
url: '/pages/user/setup'
})
}
const goToMyRecipes = () => {
uni.switchTab({
url: '/pages/recipe/recipe'
})
}
const goToMyDiaries = () => {
uni.switchTab({
url: '/pages/diary/diary'
})
}
const goToMyGoals = () => {
uni.switchTab({
url: '/pages/checkin/checkin'
})
}
const goToFavorites = () => {
uni.showToast({
title: '收藏功能开发中',
icon: 'none'
})
}
const goToSettings = () => {
uni.showToast({
title: '设置功能开发中',
icon: 'none'
})
}
const goToPrivacy = () => {
uni.showToast({
title: '隐私设置功能开发中',
icon: 'none'
})
}
const goToNotification = () => {
uni.showToast({
title: '消息通知功能开发中',
icon: 'none'
})
}
const goToHelp = () => {
uni.showToast({
title: '帮助中心功能开发中',
icon: 'none'
})
}
const goToFeedback = () => {
uni.showToast({
title: '意见反馈功能开发中',
icon: 'none'
})
}
const goToAbout = () => {
uni.showToast({
title: '关于我们功能开发中',
icon: 'none'
})
}
const logout = () => {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
// 清除用户数据
userStore.logout()
uni.removeStorageSync('user')
uni.removeStorageSync('token')
uni.showToast({
title: '退出成功',
icon: 'success'
})
// 跳转到登录页
setTimeout(() => {
uni.reLaunch({
url: '/pages/user/login'
})
}, 1500)
}
}
})
}
// 生命周期
onMounted(() => {
// 页面加载时获取用户信息
getUserInfo()
})
</script>
<style lang="scss" scoped>
.profile-container {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 40rpx;
}
.user-card {
background: linear-gradient(135deg, #FF6B9D 0%, #FF8E9E 100%);
margin: 20rpx;
border-radius: 20rpx;
padding: 40rpx 30rpx;
color: #fff;
.user-header {
display: flex;
align-items: center;
margin-bottom: 40rpx;
.user-avatar {
position: relative;
margin-right: 30rpx;
.edit-avatar {
position: absolute;
bottom: 0;
right: 0;
width: 40rpx;
height: 40rpx;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
}
.user-info {
flex: 1;
.user-name {
display: block;
font-size: 36rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.user-id {
display: block;
font-size: 24rpx;
opacity: 0.8;
margin-bottom: 15rpx;
}
.user-level {
display: flex;
align-items: center;
gap: 8rpx;
.level-text {
font-size: 24rpx;
opacity: 0.9;
}
}
}
.user-actions {
.action-btn {
width: 60rpx;
height: 60rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.user-stats {
display: flex;
align-items: center;
justify-content: space-around;
.stat-item {
text-align: center;
.stat-number {
display: block;
font-size: 40rpx;
font-weight: bold;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 24rpx;
opacity: 0.9;
}
}
.stat-divider {
width: 2rpx;
height: 60rpx;
background: rgba(255, 255, 255, 0.3);
}
}
}
.quick-functions {
margin: 0 20rpx;
.function-group {
background: #fff;
border-radius: 20rpx;
margin-bottom: 20rpx;
overflow: hidden;
.group-title {
padding: 30rpx 30rpx 20rpx;
font-size: 28rpx;
font-weight: bold;
color: #333;
border-bottom: 2rpx solid #f5f5f5;
}
.function-list {
.function-item {
display: flex;
align-items: center;
padding: 30rpx;
border-bottom: 2rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.function-icon {
width: 60rpx;
height: 60rpx;
background: #f8f8f8;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.function-name {
flex: 1;
font-size: 30rpx;
color: #333;
}
}
}
}
}
.logout-section {
margin-top: 40rpx;
}
</style>

338
src/pages/recipe/recipe.vue Normal file
View File

@@ -0,0 +1,338 @@
<template>
<view class="recipe-container">
<!-- 搜索栏 -->
<view class="search-section">
<view class="search-box">
<u-search
v-model="searchKeyword"
placeholder="搜索菜品名称"
:show-action="false"
@search="onSearch"
@custom="onSearch"
></u-search>
</view>
</view>
<!-- 分类标签 -->
<view class="category-section">
<scroll-view scroll-x class="category-scroll">
<view class="category-list">
<view
v-for="(category, index) in categories"
:key="index"
class="category-item"
:class="{ active: selectedCategory === category.id }"
@click="selectCategory(category.id)"
>
<text class="category-name">{{ category.name }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 菜谱列表 -->
<view class="recipe-list">
<view
v-for="(recipe, index) in filteredRecipes"
:key="index"
class="recipe-item"
@click="viewRecipe(recipe)"
>
<view class="recipe-image">
<u-image
:src="recipe.image"
width="120rpx"
height="120rpx"
radius="12rpx"
mode="aspectFill"
></u-image>
</view>
<view class="recipe-info">
<text class="recipe-name">{{ recipe.name }}</text>
<text class="recipe-desc">{{ recipe.description }}</text>
<view class="recipe-meta">
<view class="meta-item">
<u-icon name="clock" size="14" color="#999"></u-icon>
<text class="meta-text">{{ recipe.cookTime }}分钟</text>
</view>
<view class="meta-item">
<u-icon name="star" size="14" color="#FFD700"></u-icon>
<text class="meta-text">{{ recipe.difficulty }}</text>
</view>
</view>
</view>
<view class="recipe-actions">
<view class="action-btn" @click.stop="orderRecipe(recipe)">
<u-icon name="shopping-cart" size="20" color="#FF6B9D"></u-icon>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="filteredRecipes.length === 0" class="empty-state">
<u-empty
icon="search"
text="暂无相关菜品"
mode="data"
></u-empty>
</view>
<!-- 悬浮添加按钮 -->
<view class="fab-button" @click="createRecipe">
<u-icon name="plus" size="24" color="#fff"></u-icon>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
// 响应式数据
const searchKeyword = ref('')
const selectedCategory = ref('all')
const categories = ref([
{ id: 'all', name: '全部' },
{ id: 'chinese', name: '中餐' },
{ id: 'western', name: '西餐' },
{ id: 'dessert', name: '甜点' },
{ id: 'drink', name: '饮品' },
{ id: 'snack', name: '小吃' }
])
const recipes = ref([
{
id: 1,
name: '红烧肉',
description: '经典美味的红烧肉,肥而不腻',
image: '/static/images/recipe1.jpg',
category: 'chinese',
cookTime: 60,
difficulty: '中等',
ingredients: ['五花肉', '生抽', '老抽', '冰糖', '葱姜蒜'],
steps: ['步骤1', '步骤2', '步骤3']
},
{
id: 2,
name: '提拉米苏',
description: '意大利经典咖啡甜点',
image: '/static/images/recipe2.jpg',
category: 'dessert',
cookTime: 30,
difficulty: '简单',
ingredients: ['手指饼干', '马斯卡彭奶酪', '咖啡', '鸡蛋', '糖'],
steps: ['步骤1', '步骤2', '步骤3']
},
{
id: 3,
name: '意式咖啡',
description: '浓郁香醇的意式浓缩咖啡',
image: '/static/images/recipe3.jpg',
category: 'drink',
cookTime: 5,
difficulty: '简单',
ingredients: ['咖啡豆', '水'],
steps: ['步骤1', '步骤2']
}
])
// 计算属性
const filteredRecipes = computed(() => {
let filtered = recipes.value
// 按分类筛选
if (selectedCategory.value !== 'all') {
filtered = filtered.filter(recipe => recipe.category === selectedCategory.value)
}
// 按关键词搜索
if (searchKeyword.value) {
filtered = filtered.filter(recipe =>
recipe.name.includes(searchKeyword.value) ||
recipe.description.includes(searchKeyword.value)
)
}
return filtered
})
// 方法
const onSearch = () => {
// 搜索逻辑
console.log('搜索关键词:', searchKeyword.value)
}
const selectCategory = (categoryId) => {
selectedCategory.value = categoryId
}
const viewRecipe = (recipe) => {
uni.navigateTo({
url: `/pages/recipe/detail?id=${recipe.id}`
})
}
const orderRecipe = (recipe) => {
uni.navigateTo({
url: `/pages/order/order?recipeId=${recipe.id}`
})
}
const createRecipe = () => {
uni.navigateTo({
url: '/pages/recipe/create'
})
}
</script>
<style lang="scss" scoped>
.recipe-container {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 120rpx;
}
.search-section {
background: #fff;
padding: 20rpx 30rpx;
.search-box {
background: #f8f8f8;
border-radius: 20rpx;
overflow: hidden;
}
}
.category-section {
background: #fff;
padding: 20rpx 0;
margin-bottom: 20rpx;
.category-scroll {
white-space: nowrap;
.category-list {
display: flex;
padding: 0 30rpx;
.category-item {
display: inline-block;
padding: 16rpx 32rpx;
margin-right: 20rpx;
background: #f8f8f8;
border-radius: 25rpx;
transition: all 0.3s;
.category-name {
font-size: 28rpx;
color: #666;
}
&.active {
background: #FF6B9D;
.category-name {
color: #fff;
}
}
}
}
}
}
.recipe-list {
padding: 0 30rpx;
.recipe-item {
display: flex;
align-items: center;
background: #fff;
padding: 30rpx;
border-radius: 20rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.recipe-image {
margin-right: 30rpx;
}
.recipe-info {
flex: 1;
.recipe-name {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.recipe-desc {
display: block;
font-size: 26rpx;
color: #999;
margin-bottom: 20rpx;
line-height: 1.4;
}
.recipe-meta {
display: flex;
gap: 30rpx;
.meta-item {
display: flex;
align-items: center;
gap: 8rpx;
.meta-text {
font-size: 24rpx;
color: #999;
}
}
}
}
.recipe-actions {
.action-btn {
width: 60rpx;
height: 60rpx;
background: #f8f8f8;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
&:active {
background: #FF6B9D;
transform: scale(0.9);
}
}
}
}
}
.empty-state {
padding: 100rpx 0;
}
.fab-button {
position: fixed;
right: 40rpx;
bottom: 120rpx;
width: 100rpx;
height: 100rpx;
background: linear-gradient(135deg, #FF6B9D 0%, #FF8E9E 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 30rpx rgba(255, 107, 157, 0.3);
z-index: 999;
&:active {
transform: scale(0.9);
}
}
</style>

526
src/pages/user/login.vue Normal file
View File

@@ -0,0 +1,526 @@
<template>
<view class="login-container">
<!-- 背景装饰 -->
<view class="bg-decoration">
<view class="circle circle-1"></view>
<view class="circle circle-2"></view>
<view class="circle circle-3"></view>
</view>
<!-- 主要内容 -->
<view class="main-content">
<!-- Logo和标题 -->
<view class="header-section">
<view class="app-logo">
<u-icon name="heart-fill" size="80" color="#FF6B9D"></u-icon>
</view>
<text class="app-title">情侣菜谱</text>
<text class="app-subtitle">记录我们的美食时光</text>
</view>
<!-- 功能介绍 -->
<view class="features-section">
<view class="feature-item">
<u-icon name="book" size="24" color="#FF6B9D"></u-icon>
<text class="feature-text">私人菜谱管理</text>
</view>
<view class="feature-item">
<u-icon name="edit-pen" size="24" color="#52c41a"></u-icon>
<text class="feature-text">记录美好时光</text>
</view>
<view class="feature-item">
<u-icon name="target" size="24" color="#1890ff"></u-icon>
<text class="feature-text">打卡目标完成</text>
</view>
<view class="feature-item">
<u-icon name="shopping-cart" size="24" color="#722ed1"></u-icon>
<text class="feature-text">便捷点餐服务</text>
</view>
</view>
<!-- 登录按钮 -->
<view class="login-section">
<view class="login-tips">
<text class="tips-text">登录后即可使用全部功能</text>
</view>
<button
class="wechat-login-btn"
@click="onGetUserInfo"
:loading="isLogging"
:disabled="isLogging"
>
<view class="btn-content">
<text class="btn-text">{{ isLogging ? '登录中...' : '微信一键登录' }}</text>
</view>
</button>
<view class="privacy-tips">
<text class="privacy-text">登录即表示同意</text>
<text class="privacy-link" @click="viewPrivacy">用户协议</text>
<text class="privacy-text"></text>
<text class="privacy-link" @click="viewPrivacy">隐私政策</text>
</view>
</view>
</view>
<!-- 底部装饰 -->
<view class="bottom-decoration">
<view class="wave wave-1"></view>
<view class="wave wave-2"></view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { useUserStore } from '@/store/user.js'
import api from '@/api'
const userStore = useUserStore()
// 响应式数据
const isLogging = ref(false)
// 方法
const onGetUserInfo = async () => {
// 直接进行登录流程
console.log('用户点击登录按钮,开始登录流程')
try {
isLogging.value = true
// 获取微信登录code
const loginRes = await uni.login({
provider: 'weixin'
})
console.log('微信登录结果:', loginRes)
if (loginRes.errMsg !== 'login:ok') {
throw new Error('微信登录失败')
}
// 调用登录接口
const loginResult = await api.login({
code: loginRes.code
})
// 检查登录结果 - 处理可能被错误包装的成功响应
if (!loginResult) {
throw new Error('登录接口返回数据为空')
}
// 如果loginResult本身就是一个包含成功数据的对象直接使用
if (loginResult.code === 0) {
console.log('登录成功,开始处理用户信息')
// 检查必要的数据字段
if (!loginResult.data || !loginResult.data.token || !loginResult.data.Info) {
throw new Error('登录返回数据格式错误')
}
// 登录成功保存token到本地缓存
const token = loginResult.data.token
console.log('获取到的token:', token)
uni.setStorageSync('token', token)
userStore.setToken(token)
uni.showToast({
title: '登录成功',
icon: 'success'
})
// 判断是否首次登录
if (loginResult.data.isFirstLogin) {
// 首次登录,跳转到设置页面
setTimeout(() => {
uni.navigateTo({
url: '/pages/user/setup'
})
}, 1500)
} else {
// 非首次登录,获取最新用户信息并缓存
try {
console.log('非首次登录,开始获取最新用户信息...')
const userInfoResult = await api.getUserInfo()
if (userInfoResult.code === 0 && userInfoResult.data) {
console.log('获取用户信息成功:', userInfoResult.data)
// 缓存用户信息到userInfo
uni.setStorageSync('userInfo', userInfoResult.data)
// 更新用户store
userStore.setUserInfo({
userId: userInfoResult.data.ID || userInfoResult.data.userId,
nickname: userInfoResult.data.nick_name || userInfoResult.data.nickname,
avatar: userInfoResult.data.avatar,
phone: userInfoResult.data.phone
})
console.log('用户信息已缓存到userInfo:', userInfoResult.data)
} else {
console.warn('获取用户信息失败,使用登录返回的基础信息')
// 如果获取失败,使用登录返回的基础信息
const basicUserInfo = {
ID: loginResult.data.Info.ID,
nick_name: loginResult.data.Info.nick_name,
avatar: loginResult.data.Info.avatar,
phone: loginResult.data.Info.phone
}
uni.setStorageSync('userInfo', basicUserInfo)
}
} catch (error) {
console.error('获取用户信息失败:', error)
// 如果获取失败,使用登录返回的基础信息
const basicUserInfo = {
ID: loginResult.data.Info.ID,
nick_name: loginResult.data.Info.nick_name,
avatar: loginResult.data.Info.avatar,
phone: loginResult.data.Info.phone
}
uni.setStorageSync('userInfo', basicUserInfo)
}
// 跳转到首页
setTimeout(() => {
uni.switchTab({
url: '/pages/home/home'
})
}, 1500)
}
} else {
console.log('登录失败,错误信息:', loginResult?.msg)
throw new Error(loginResult?.msg || '登录失败')
}
} catch (error) {
// 检查是否是被错误包装的成功响应
if (error && typeof error === 'object' && error.code === 0) {
console.log('🔍 检测到被错误包装的成功响应,正在重新处理...')
try {
// 重新处理成功响应
const loginResult = error
if (loginResult.data && loginResult.data.token && loginResult.data.Info) {
console.log('✅ 成功解析登录数据,开始处理用户信息')
// 登录成功保存token到本地缓存
const token = loginResult.data.token
console.log('🔑 获取到的token:', token)
uni.setStorageSync('token', token)
userStore.setToken(token)
uni.showToast({
title: '登录成功',
icon: 'success'
})
// 判断是否首次登录
if (loginResult.data.isFirstLogin) {
console.log('🎯 首次登录,准备跳转设置页面')
setTimeout(() => {
uni.navigateTo({
url: '/pages/user/setup'
})
}, 1500)
} else {
console.log('🏠 非首次登录,准备获取最新用户信息并跳转首页')
// 非首次登录,获取最新用户信息并缓存
try {
const userInfoResult = await api.getUserInfo()
if (userInfoResult.code === 0 && userInfoResult.data) {
console.log('✅ 获取用户信息成功:', userInfoResult.data)
// 缓存用户信息到userInfo
uni.setStorageSync('userInfo', userInfoResult.data)
// 更新用户store
userStore.setUserInfo({
userId: userInfoResult.data.ID || userInfoResult.data.userId,
nickname: userInfoResult.data.nick_name || userInfoResult.data.nickname,
avatar: userInfoResult.data.avatar,
phone: userInfoResult.data.phone
})
console.log('🎉 用户信息已缓存到userInfo:', userInfoResult.data)
} else {
console.warn('⚠️ 获取用户信息失败,使用登录返回的基础信息')
// 如果获取失败,使用登录返回的基础信息
const basicUserInfo = {
ID: loginResult.data.Info.ID,
nick_name: loginResult.data.Info.nick_name,
avatar: loginResult.data.Info.avatar,
phone: loginResult.data.Info.phone
}
uni.setStorageSync('userInfo', basicUserInfo)
}
} catch (error) {
console.error('❌ 获取用户信息失败:', error)
// 如果获取失败,使用登录返回的基础信息
const basicUserInfo = {
ID: loginResult.data.Info.ID,
nick_name: loginResult.data.Info.nick_name,
avatar: loginResult.data.Info.avatar,
phone: loginResult.data.Info.phone
}
uni.setStorageSync('userInfo', basicUserInfo)
}
// 跳转到首页
setTimeout(() => {
uni.switchTab({
url: '/pages/home/home'
})
}, 1500)
}
// 标记为已处理,避免后续错误日志
error._handled = true
console.log('🎉 登录流程处理完成')
return // 成功处理,退出函数
} else {
console.warn('⚠️ 登录数据格式不完整,无法处理')
}
} catch (innerError) {
console.error('❌ 重新处理成功响应时出错:', innerError)
}
}
// 真正的错误处理 - 只处理未被标记的错误
if (!error._handled) {
console.error('❌ 登录遇到真实错误:', error)
console.error('🚨 错误类型:', error.constructor.name)
console.error('💬 错误消息:', error.message)
// 如果是我们自定义的错误,直接显示
if (error.message && error.message !== '登录失败,请重试') {
uni.showToast({
title: error.message,
icon: 'none'
})
} else {
uni.showToast({
title: '登录失败,请重试',
icon: 'none'
})
}
}
} finally {
isLogging.value = false
}
}
const viewPrivacy = () => {
uni.showToast({
title: '隐私政策功能开发中',
icon: 'none'
})
}
</script>
<style lang="scss" scoped>
.login-container {
min-height: 100vh;
background: linear-gradient(135deg, #FFE4E1 0%, #FFB6C1 50%, #E6E6FA 100%);
position: relative;
overflow: hidden;
}
.bg-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
.circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
&.circle-1 {
width: 200rpx;
height: 200rpx;
top: 10%;
right: -50rpx;
animation: float 6s ease-in-out infinite;
}
&.circle-2 {
width: 150rpx;
height: 150rpx;
top: 30%;
left: -30rpx;
animation: float 8s ease-in-out infinite reverse;
}
&.circle-3 {
width: 100rpx;
height: 100rpx;
top: 60%;
right: 20%;
animation: float 10s ease-in-out infinite;
}
}
}
.main-content {
position: relative;
z-index: 1;
padding: 100rpx 60rpx 60rpx;
text-align: center;
}
.header-section {
margin-bottom: 80rpx;
.app-logo {
margin-bottom: 30rpx;
animation: bounce 2s ease-in-out infinite;
}
.app-title {
display: block;
font-size: 48rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.app-subtitle {
font-size: 28rpx;
color: #666;
line-height: 1.4;
}
}
.features-section {
margin-bottom: 80rpx;
.feature-item {
display: flex;
align-items: center;
justify-content: center;
gap: 20rpx;
margin-bottom: 30rpx;
.feature-text {
font-size: 28rpx;
color: #333;
}
}
}
.login-section {
.login-tips {
margin-bottom: 40rpx;
.tips-text {
font-size: 26rpx;
color: #999;
}
}
.wechat-login-btn {
width: 100%;
height: 100rpx;
background: linear-gradient(135deg, #07c160 0%, #10ad6a 100%);
border: none;
border-radius: 50rpx;
margin-bottom: 40rpx;
box-shadow: 0 8rpx 30rpx rgba(7, 193, 96, 0.3);
transition: all 0.3s;
&:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 15rpx rgba(7, 193, 96, 0.3);
}
.btn-content {
display: flex;
align-items: center;
justify-content: center;
gap: 15rpx;
height: 100%;
.btn-text {
color: #fff;
font-size: 32rpx;
font-weight: bold;
}
}
}
.privacy-tips {
text-align: center;
line-height: 1.6;
.privacy-text {
font-size: 24rpx;
color: #999;
}
.privacy-link {
font-size: 24rpx;
color: #FF6B9D;
text-decoration: underline;
}
}
}
.bottom-decoration {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 200rpx;
pointer-events: none;
.wave {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 50% 50% 0 0;
&.wave-1 {
bottom: 0;
animation: wave 4s ease-in-out infinite;
}
&.wave-2 {
bottom: 20rpx;
animation: wave 6s ease-in-out infinite reverse;
}
}
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-10px); }
60% { transform: translateY(-5px); }
}
@keyframes wave {
0%, 100% { transform: scaleX(1); }
50% { transform: scaleX(1.1); }
}
</style>

328
src/pages/user/setup.vue Normal file
View File

@@ -0,0 +1,328 @@
<template>
<view class="container">
<cu-custom bgColor="none" :isBack="false">
<block slot="content">
<span style="color: #333;font-weight: 400;">获取用户头像昵称</span>
</block>
</cu-custom>
<view class="avatarUrl">
<button type="balanced" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<image :src="avatarUrl" class="refreshIcon"></image>
</button>
</view>
<view class="userName">
<text>昵称</text>
<input
:clearable="false"
type="nickname"
class="weui-input"
:value="userName"
@blur="bindBlur"
placeholder="请输入昵称"
@input="bindInput"
/>
</view>
<view style="width: 100%;height: 1px; background: #EEE;"></view>
<view style="width: 700rpx; height: 20px; font-size: 13px; margin: auto; margin-top: 40rpx;">
· 申请获取以下权限
</view>
<view style="width: 700rpx; height: 20px; font-size: 13px; margin: auto; color: #cbcbcb; margin-top: 25rpx;">
· 获得你的信息(昵称头像等)
</view>
<view class="btn">
<u-button @click="onSubmit" type="primary">保存</u-button>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { pathToBase64, base64ToPath } from 'image-tools'
import api from '@/api'
import { useUserStore } from '@/store/user.js'
const userStore = useUserStore()
// 响应式数据
const avatarUrl = ref('https://menu-echo.oss-cn-chengdu.aliyuncs.com/avatar/def.jpg')
const userName = ref('')
const appUserId = ref('')
// 输入事件处理
const bindBlur = (e) => {
userName.value = e.detail.value // 获取微信昵称
}
const bindInput = (e) => {
userName.value = e.detail.value // 获取微信昵称
}
// 头像选择事件
const onChooseAvatar = (e) => {
const { avatarUrl: newAvatarUrl } = e.detail
avatarUrl.value = newAvatarUrl
}
// 提交事件
const onSubmit = () => {
uni.login({
"provider": "weixin",
"onlyAuthorize": true, // 微信登录仅请求授权认证
success: async function(event) {
const { code } = event
const name = userName.value
// 判断头像以及昵称不为空 再上传
if (userName.value && avatarUrl.value) {
console.log('头像URL:', avatarUrl.value) // 返回的是临时图片地址
// 检查是否是临时文件,如果是则需要先上传
if (avatarUrl.value.startsWith('http://tmp/') || avatarUrl.value.startsWith('wxfile://')) {
console.log('检测到临时文件,开始上传头像...')
uploadAvatar(avatarUrl.value).then(uploadResult => {
if (uploadResult.success) {
console.log('头像上传成功:', uploadResult.data)
// 使用上传后的URL保存用户信息
save(uploadResult.data.url)
} else {
uni.showToast({
title: uploadResult.message || '头像上传失败',
icon: 'none'
})
}
}).catch(error => {
console.error('头像上传失败:', error)
uni.showToast({
title: '头像上传失败,请重试',
icon: 'none'
})
})
} else {
// 已经是网络图片,直接保存
save(avatarUrl.value)
}
} else {
uni.showToast({
icon: 'none',
title: '请上传头像并填写昵称'
})
return false
}
},
fail: function(err) {
console.log('err', err)
}
})
}
/**
* 上传头像到服务器
*/
const uploadAvatar = (tempFilePath) => {
return new Promise((resolve, reject) => {
uni.showLoading({
title: '上传头像中...'
})
// 获取token
const token = uni.getStorageSync('token')
if (!token) {
uni.hideLoading()
reject(new Error('未找到登录token'))
return
}
// 使用uni.uploadFile上传文件
uni.uploadFile({
url: 'http://127.0.0.1:8888/user/avatar', // 替换为你的实际上传接口
filePath: tempFilePath,
name: 'avatar',
header: {
'Authorization': `${token}`
},
success: (res) => {
uni.hideLoading()
console.log('上传响应:', res)
try {
const response = JSON.parse(res.data)
if (response.code === 0) {
resolve({
success: true,
data: response.data
})
} else {
resolve({
success: false,
message: response.msg || '上传失败'
})
}
} catch (error) {
console.error('解析上传响应失败:', error)
resolve({
success: false,
message: '上传响应解析失败'
})
}
},
fail: (error) => {
uni.hideLoading()
console.error('上传失败:', error)
reject(error)
}
})
})
}
/**
* @description 保存事件,用于保存用户头像昵称
*/
const save = async (finalAvatarUrl) => {
uni.showLoading({
title: '保存中...'
})
const data = {
nick_name: userName.value,
avatar: finalAvatarUrl
}
console.log('保存用户信息:', data)
const result = await api.updateUserInfo(data)
if (result.code === 0) {
// 更新成功后,获取最新用户信息并缓存
try {
console.log('用户信息更新成功,开始获取最新用户信息...')
const userInfoResult = await api.getUserInfo()
if (userInfoResult.code === 0 && userInfoResult.data) {
console.log('✅ 获取最新用户信息成功:', userInfoResult.data)
// 缓存用户信息到userInfo
uni.setStorageSync('userInfo', userInfoResult.data)
// 更新本地用户信息
userStore.updateUserInfo({
nickname: userInfoResult.data.nick_name || userInfoResult.data.nickname,
avatar: userInfoResult.data.avatar
})
uni.showToast({
title: '设置成功',
icon: 'success'
})
// 跳转到首页
setTimeout(() => {
uni.switchTab({
url: '/pages/home/home'
})
}, 1500)
} else {
throw new Error(userInfoResult.msg || '获取用户信息失败')
}
} catch (error) {
console.error('获取最新用户信息失败:', error)
// 如果获取失败,仍然显示设置成功,但使用更新后的数据
uni.showToast({
title: '设置成功',
icon: 'success'
})
// 使用更新后的数据缓存
const updatedUserInfo = {
ID: userStore.userInfo.userId,
nick_name: userName.value,
avatar: finalAvatarUrl,
phone: userStore.userInfo.phone || ''
}
uni.setStorageSync('userInfo', updatedUserInfo)
setTimeout(() => {
uni.switchTab({
url: '/pages/home/home'
})
}, 1500)
}
} else {
uni.hideLoading()
uni.showToast({
title: result.msg || '保存失败',
icon: 'none'
})
}
}
// 生命周期
onMounted(() => {
// 初始化随机昵称
userName.value = getRandomName(Math.floor(Math.random() * (6 - 2) + 3))
})
</script>
<style lang="scss" scoped>
.container {
width: 100vw;
height: 100vh;
background: #fff;
box-sizing: border-box;
padding: 0 30rpx;
.avatarUrl {
padding: 80rpx 0 40rpx;
background: #fff;
button {
background: #fff;
line-height: 80rpx;
height: auto;
border: none !important;
width: auto;
margin: 0;
display: flex;
border: none;
justify-content: center;
align-items: center;
&::after {
border: none;
}
.refreshIcon {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background-color: #ccc;
}
.jt {
width: 14rpx;
height: 28rpx;
}
}
}
.userName {
background: #fff;
padding: 20rpx 30rpx 80rpx;
display: flex;
align-items: center;
justify-content: center;
.weui-input {
padding-left: 60rpx;
}
}
.btn {
margin-top: 30rpx;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
src/static/images/diary.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
src/static/images/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -4,7 +4,7 @@ export const useUserStore = defineStore({
id: 'user',
state: () => {
return {
token: '',
token: uni.getStorageSync('token') || '', // 从缓存读取token
userInfo: {}, // 用户信息
appInfo: {}, // 应用信息
// 按钮权限编码
@@ -12,10 +12,107 @@ export const useUserStore = defineStore({
// 权限菜单
menuList: [],
// 权限路由
permissionRoutes: []
permissionRoutes: [],
// 是否已登录
isLoggedIn: !!uni.getStorageSync('token')
}
},
unistorage: true,
getters: {},
actions: {}
getters: {
// 获取用户昵称
nickname: (state) => state.userInfo.nickname || '未登录',
// 获取用户头像
avatar: (state) => state.userInfo.avatar || '/static/images/default-avatar.png',
// 获取用户ID
userId: (state) => state.userInfo.userId || '',
// 检查是否已登录
hasLogin: (state) => state.isLoggedIn && !!state.token,
// 检查是否首次登录
isFirstLogin: (state) => state.userInfo.isFirstLogin || false
},
actions: {
// 设置用户信息
setUserInfo(userInfo) {
this.userInfo = userInfo
this.isLoggedIn = true
},
// 设置token
setToken(token) {
this.token = token
// 同时保存到本地缓存
uni.setStorageSync('token', token)
},
// 登录
async login(loginData) {
try {
// 这里可以调用登录API
// const result = await userApi.login(loginData)
// 模拟登录成功
const mockUserInfo = {
userId: '10086',
nickname: '小美',
avatar: '/static/images/default-avatar.png',
level: 'LV.5 美食达人'
}
this.setUserInfo(mockUserInfo)
this.setToken('mock_token_' + Date.now())
return {
success: true,
data: mockUserInfo
}
} catch (error) {
console.error('登录失败:', error)
return {
success: false,
message: error.message || '登录失败'
}
}
},
// 登出
logout() {
this.token = ''
this.userInfo = {}
this.isLoggedIn = false
this.permissionCodes = []
this.menuList = []
this.permissionRoutes = []
// 清除本地缓存
uni.removeStorageSync('token')
},
// 更新用户信息
updateUserInfo(userInfo) {
this.userInfo = { ...this.userInfo, ...userInfo }
// 如果更新了昵称,清除首次登录状态
if (userInfo.nickname) {
this.userInfo.isFirstLogin = false
}
},
// 检查登录状态
checkLoginStatus() {
// 从缓存重新读取token
const cachedToken = uni.getStorageSync('token')
if (cachedToken && cachedToken !== this.token) {
this.token = cachedToken
this.isLoggedIn = true
}
return this.hasLogin
},
// 清除首次登录状态
clearFirstLogin() {
this.userInfo.isFirstLogin = false
}
}
})