🎨 优化核心组件,优化登录和首页
29
src/App.vue
@@ -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>
|
||||
|
||||
|
||||
111
src/api/http.js
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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,
|
||||
|
||||
111
src/pages.json
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
577
src/pages/checkin/checkin.vue
Normal 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>
|
||||
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
567
src/pages/profile/profile.vue
Normal 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
@@ -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
@@ -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
@@ -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>
|
||||
BIN
src/static/images/checkin-active.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
src/static/images/checkin.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
src/static/images/diary-active.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/static/images/diary.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src/static/images/home-active.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src/static/images/home.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src/static/images/profile-active.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/static/images/profile.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/static/images/recipe-active.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
src/static/images/recipe.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||