🎨 优化核心组件,优化登录和首页
29
src/App.vue
@@ -1,12 +1,37 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useSystemStore } from '@/store/system.js'
|
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 systemStore = useSystemStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
onLaunch(() => {
|
onLaunch(() => {
|
||||||
systemStore.init()
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@@ -20,7 +45,7 @@ onLaunch(() => {
|
|||||||
}
|
}
|
||||||
//主题scss
|
//主题scss
|
||||||
.red-theme {
|
.red-theme {
|
||||||
@import 'styles/themes/red-theme.scss';
|
@import 'styles/themes/default-theme.scss';
|
||||||
}
|
}
|
||||||
</style>
|
</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",
|
"name" : "xdd-menu",
|
||||||
"appid" : "",
|
"appid" : "__UNI__3FD9A6A",
|
||||||
"description" : "",
|
"description" : "",
|
||||||
"versionName" : "1.0.0",
|
"versionName" : "1.0.0",
|
||||||
"versionCode" : "100",
|
"versionCode" : "100",
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
},
|
},
|
||||||
"quickapp" : {},
|
"quickapp" : {},
|
||||||
"mp-weixin" : {
|
"mp-weixin" : {
|
||||||
"appid" : "",
|
"appid" : "wx8ed262fbd9eaaf74",
|
||||||
"mergeVirtualHostAttributes" : true,
|
"mergeVirtualHostAttributes" : true,
|
||||||
"setting" : {
|
"setting" : {
|
||||||
"urlCheck" : false,
|
"urlCheck" : false,
|
||||||
|
|||||||
111
src/pages.json
@@ -2,7 +2,6 @@
|
|||||||
"easycom": {
|
"easycom": {
|
||||||
"autoscan": true,
|
"autoscan": true,
|
||||||
"custom": {
|
"custom": {
|
||||||
// uni-ui 规则如下配置
|
|
||||||
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
|
"^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",
|
||||||
"^u-([^-].*)": "uview-plus/components/u-$1/u-$1.vue",
|
"^u-([^-].*)": "uview-plus/components/u-$1/u-$1.vue",
|
||||||
@@ -12,59 +11,135 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pages": [
|
"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",
|
"path": "pages/home/home",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "",
|
"navigationBarTitleText": "我们的纪念日",
|
||||||
|
"enablePullDownRefresh": false,
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/recipe/recipe",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "私人菜谱",
|
||||||
"enablePullDownRefresh": false
|
"enablePullDownRefresh": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/components/components",
|
"path": "pages/recipe/create",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "",
|
"navigationBarTitleText": "创建菜品",
|
||||||
"enablePullDownRefresh": false
|
"enablePullDownRefresh": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/functions/functions",
|
"path": "pages/recipe/detail",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "",
|
"navigationBarTitleText": "菜品详情",
|
||||||
"enablePullDownRefresh": false
|
"enablePullDownRefresh": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/uploadDemo/uploadDemo",
|
"path": "pages/order/order",
|
||||||
"style": {
|
"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
|
"enablePullDownRefresh": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
"navigationBarBackgroundColor": "#fff",
|
"navigationBarBackgroundColor": "#fff",
|
||||||
"navigationBarTitleText": "订单中心",
|
"navigationBarTitleText": "情侣菜谱",
|
||||||
"navigationStyle": "custom",
|
"navigationStyle": "default",
|
||||||
"navigationBarTextStyle": "black"
|
"navigationBarTextStyle": "black"
|
||||||
},
|
},
|
||||||
"tabBar": {
|
"tabBar": {
|
||||||
"color": "#333333",
|
"color": "#999999",
|
||||||
"selectedColor": "#2E69FF",
|
"selectedColor": "#FF6B9D",
|
||||||
"borderStyle": "white",
|
"borderStyle": "white",
|
||||||
"backgroundColor": "#ffffff",
|
"backgroundColor": "#ffffff",
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"pagePath": "pages/home/home",
|
"pagePath": "pages/home/home",
|
||||||
"text": "首页"
|
"text": "首页",
|
||||||
|
"iconPath": "./static/images/home.png",
|
||||||
|
"selectedIconPath": "./static/images/home-active.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pagePath": "pages/components/components",
|
"pagePath": "pages/recipe/recipe",
|
||||||
"text": "组件"
|
"text": "菜谱",
|
||||||
|
"iconPath": "./static/images/recipe.png",
|
||||||
|
"selectedIconPath": "./static/images/recipe-active.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pagePath": "pages/functions/functions",
|
"pagePath": "pages/diary/diary",
|
||||||
"text": "功能"
|
"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>
|
<template>
|
||||||
<custom-page>
|
<view class="home-container">
|
||||||
<custom-head title-text="演示界面"></custom-head>
|
<!-- 自定义头部 -->
|
||||||
<!-- 使用scroll-page 组件的话,需要加上content-container才能让里面的元素继承外部的高度-->
|
<view class="custom-header">
|
||||||
<scroll-page class="content-container">
|
<view class="status-bar"></view>
|
||||||
<div class="h-[3000rpx]">自适应的内容滚动区域,兼容IOS下不显示滚动条</div>
|
<view class="header-content">
|
||||||
</scroll-page>
|
<view class="header-title">情侣菜谱</view>
|
||||||
<view class="bg-red-500 h-[200rpx]">占位盒子</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>
|
</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',
|
id: 'user',
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
token: '',
|
token: uni.getStorageSync('token') || '', // 从缓存读取token
|
||||||
userInfo: {}, // 用户信息
|
userInfo: {}, // 用户信息
|
||||||
appInfo: {}, // 应用信息
|
appInfo: {}, // 应用信息
|
||||||
// 按钮权限编码
|
// 按钮权限编码
|
||||||
@@ -12,10 +12,107 @@ export const useUserStore = defineStore({
|
|||||||
// 权限菜单
|
// 权限菜单
|
||||||
menuList: [],
|
menuList: [],
|
||||||
// 权限路由
|
// 权限路由
|
||||||
permissionRoutes: []
|
permissionRoutes: [],
|
||||||
|
// 是否已登录
|
||||||
|
isLoggedIn: !!uni.getStorageSync('token')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
unistorage: true,
|
unistorage: true,
|
||||||
getters: {},
|
getters: {
|
||||||
actions: {}
|
// 获取用户昵称
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||