feat: 梦想清单页与戒烟相关 API
- 梦想清单:系统导航栏、浮动添加、图标来自后台预设 - dream-presets API、pages.json 导航样式 Made-with: Cursor
This commit is contained in:
@@ -114,6 +114,11 @@ export function upsertQuitCheckinProfile(data) {
|
|||||||
return request.request({ url: '/profile', method: 'POST', data, baseUrl: BASE_URL_V2 })
|
return request.request({ url: '/profile', method: 'POST', data, baseUrl: BASE_URL_V2 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 预设梦想目标
|
||||||
|
export function listDreamPresets() {
|
||||||
|
return request.request({ url: '/dream-presets', method: 'GET', baseUrl: BASE_URL_V2 })
|
||||||
|
}
|
||||||
|
|
||||||
// 梦想目标 API
|
// 梦想目标 API
|
||||||
export function listRewardGoals(status = 'all') {
|
export function listRewardGoals(status = 'all') {
|
||||||
return request.request({ url: '/reward-goals', method: 'GET', data: { status }, baseUrl: BASE_URL_V2 })
|
return request.request({ url: '/reward-goals', method: 'GET', data: { status }, baseUrl: BASE_URL_V2 })
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
{
|
{
|
||||||
"path": "pages/dream-goals/index",
|
"path": "pages/dream-goals/index",
|
||||||
"style": {
|
"style": {
|
||||||
|
"navigationStyle": "default",
|
||||||
"navigationBarTitleText": "梦想清单"
|
"navigationBarTitleText": "梦想清单"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+80
-191
@@ -1,15 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page">
|
<view class="page">
|
||||||
<view class="header">
|
|
||||||
<view class="header-left" @tap="goBack">
|
|
||||||
<text class="header-back">←</text>
|
|
||||||
</view>
|
|
||||||
<text class="header-title">梦想清单</text>
|
|
||||||
<view class="header-right" @tap="openCreate">
|
|
||||||
<text class="header-add">+</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="saved-banner">
|
<view class="saved-banner">
|
||||||
<view class="saved-banner-inner">
|
<view class="saved-banner-inner">
|
||||||
<text class="saved-label">戒烟已省下</text>
|
<text class="saved-label">戒烟已省下</text>
|
||||||
@@ -63,9 +53,14 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 创建/编辑弹窗 -->
|
<!-- 浮动添加按钮 -->
|
||||||
<view v-if="showForm" class="modal-mask" @tap.self="showForm = false">
|
<view class="fab" @tap="openCreate">
|
||||||
<view class="modal-body">
|
<text class="fab-icon">+</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 创建弹窗 -->
|
||||||
|
<view v-if="showForm" class="modal-mask" @tap="closeForm">
|
||||||
|
<view class="modal-body" @tap.stop>
|
||||||
<text class="modal-title">添加梦想目标</text>
|
<text class="modal-title">添加梦想目标</text>
|
||||||
|
|
||||||
<view class="form-group">
|
<view class="form-group">
|
||||||
@@ -80,34 +75,29 @@
|
|||||||
|
|
||||||
<view class="form-group">
|
<view class="form-group">
|
||||||
<text class="form-label">选择图标</text>
|
<text class="form-label">选择图标</text>
|
||||||
<view class="icon-grid">
|
<view v-if="iconsLoading" class="icons-loading">
|
||||||
|
<text class="icons-loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="iconList.length === 0" class="icons-empty">
|
||||||
|
<text class="icons-empty-text">暂无图标</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="icon-grid">
|
||||||
<view
|
<view
|
||||||
v-for="icon in systemIcons"
|
v-for="icon in iconList"
|
||||||
:key="icon.key"
|
:key="icon.id"
|
||||||
class="icon-item"
|
class="icon-item"
|
||||||
:class="{ 'icon-item-active': form.coverImage === icon.key }"
|
:class="{ 'icon-item-active': form.coverImage === icon.cover_image }"
|
||||||
@tap="form.coverImage = icon.key"
|
@tap.stop="form.coverImage = icon.cover_image"
|
||||||
>
|
>
|
||||||
<text class="icon-emoji">{{ icon.emoji }}</text>
|
<image v-if="icon.cover_image && !icon.cover_image.startsWith('icon:')" :src="icon.cover_image" mode="aspectFill" class="icon-img" />
|
||||||
<text class="icon-name">{{ icon.name }}</text>
|
<text v-else class="icon-emoji">{{ (icon.cover_image || '').replace('icon:', '') || '🎁' }}</text>
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-group">
|
|
||||||
<text class="form-label">或上传图片</text>
|
|
||||||
<view class="upload-area" @tap="chooseImage">
|
|
||||||
<image v-if="form.uploadedUrl" :src="form.uploadedUrl" mode="aspectFill" class="upload-preview" />
|
|
||||||
<view v-else class="upload-placeholder">
|
|
||||||
<text class="upload-icon">📷</text>
|
|
||||||
<text class="upload-text">选择图片(≤3MB)</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="modal-actions">
|
<view class="modal-actions">
|
||||||
<text class="modal-btn modal-btn-cancel" @tap="showForm = false">取消</text>
|
<text class="modal-btn modal-btn-cancel" @tap.stop="closeForm">取消</text>
|
||||||
<text class="modal-btn modal-btn-confirm" @tap="submitGoal">确定</text>
|
<text class="modal-btn modal-btn-confirm" @tap.stop="submitGoal">确定</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -117,7 +107,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import * as api from '@/api'
|
import * as api from '@/api'
|
||||||
import { getUploadToken } from '@/api/auth'
|
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const goals = ref([])
|
const goals = ref([])
|
||||||
@@ -125,28 +114,10 @@ const currentTab = ref('active')
|
|||||||
const showForm = ref(false)
|
const showForm = ref(false)
|
||||||
const savedMoneyCent = ref(0)
|
const savedMoneyCent = ref(0)
|
||||||
const dailySaveCent = ref(0)
|
const dailySaveCent = ref(0)
|
||||||
|
const iconList = ref([])
|
||||||
|
const iconsLoading = ref(false)
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({ title: '', priceYuan: '', coverImage: '' })
|
||||||
title: '',
|
|
||||||
priceYuan: '',
|
|
||||||
coverImage: '',
|
|
||||||
uploadedUrl: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const systemIcons = [
|
|
||||||
{ key: 'icon:🎧', emoji: '🎧', name: '耳机' },
|
|
||||||
{ key: 'icon:👟', emoji: '👟', name: '球鞋' },
|
|
||||||
{ key: 'icon:📱', emoji: '📱', name: '手机' },
|
|
||||||
{ key: 'icon:⌚', emoji: '⌚', name: '手表' },
|
|
||||||
{ key: 'icon:🎮', emoji: '🎮', name: '游戏机' },
|
|
||||||
{ key: 'icon:📷', emoji: '📷', name: '相机' },
|
|
||||||
{ key: 'icon:💻', emoji: '💻', name: '电脑' },
|
|
||||||
{ key: 'icon:🎸', emoji: '🎸', name: '吉他' },
|
|
||||||
{ key: 'icon:🏖️', emoji: '🏖️', name: '旅行' },
|
|
||||||
{ key: 'icon:🎂', emoji: '🎂', name: '蛋糕' },
|
|
||||||
{ key: 'icon:🎁', emoji: '🎁', name: '礼物' },
|
|
||||||
{ key: 'icon:🚲', emoji: '🚲', name: '自行车' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const savedMoneyYuan = computed(() => Math.round(savedMoneyCent.value / 100))
|
const savedMoneyYuan = computed(() => Math.round(savedMoneyCent.value / 100))
|
||||||
|
|
||||||
@@ -167,69 +138,24 @@ function getEtaText(goal) {
|
|||||||
return `约 ${daysNeeded} 天可达成`
|
return `约 ${daysNeeded} 天可达成`
|
||||||
}
|
}
|
||||||
|
|
||||||
function goBack() {
|
async function openCreate() {
|
||||||
uni.navigateBack()
|
form.value = { title: '', priceYuan: '', coverImage: '' }
|
||||||
}
|
|
||||||
|
|
||||||
function openCreate() {
|
|
||||||
form.value = { title: '', priceYuan: '', coverImage: '', uploadedUrl: '' }
|
|
||||||
showForm.value = true
|
showForm.value = true
|
||||||
}
|
if (iconList.value.length === 0) {
|
||||||
|
iconsLoading.value = true
|
||||||
async function chooseImage() {
|
|
||||||
uni.chooseImage({
|
|
||||||
count: 1,
|
|
||||||
sizeType: ['compressed'],
|
|
||||||
sourceType: ['album', 'camera'],
|
|
||||||
success: async (res) => {
|
|
||||||
const tempPath = res.tempFilePaths[0]
|
|
||||||
const fileSize = res.tempFiles[0].size
|
|
||||||
if (fileSize > 3 * 1024 * 1024) {
|
|
||||||
uni.showToast({ title: '图片不能超过 3MB', icon: 'none' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const tokenRes = await getUploadToken(res.tempFiles[0].name || 'image.jpg')
|
const res = await api.listDreamPresets()
|
||||||
const tokenData = tokenRes.data
|
iconList.value = res.data?.items || []
|
||||||
if (tokenData.oss_access_key_id) {
|
|
||||||
await ossUpload(tokenData, tempPath)
|
|
||||||
} else {
|
|
||||||
uni.showToast({ title: '暂不支持当前上传方式', icon: 'none' })
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('upload error:', e)
|
console.error('fetch icons error:', e)
|
||||||
uni.showToast({ title: '上传失败', icon: 'none' })
|
} finally {
|
||||||
|
iconsLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ossUpload(tokenData, filePath) {
|
function closeForm() {
|
||||||
return new Promise((resolve, reject) => {
|
showForm.value = false
|
||||||
uni.uploadFile({
|
|
||||||
url: tokenData.upload_url,
|
|
||||||
filePath,
|
|
||||||
name: 'file',
|
|
||||||
formData: {
|
|
||||||
key: tokenData.key,
|
|
||||||
OSSAccessKeyId: tokenData.oss_access_key_id,
|
|
||||||
policy: tokenData.oss_policy,
|
|
||||||
Signature: tokenData.oss_signature,
|
|
||||||
success_action_status: '200'
|
|
||||||
},
|
|
||||||
success: (uploadRes) => {
|
|
||||||
if (uploadRes.statusCode === 200) {
|
|
||||||
const url = `${tokenData.cdn_domain}/${tokenData.key}`
|
|
||||||
form.value.uploadedUrl = url
|
|
||||||
form.value.coverImage = ''
|
|
||||||
resolve(url)
|
|
||||||
} else {
|
|
||||||
reject(new Error('上传失败'))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fail: reject
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitGoal() {
|
async function submitGoal() {
|
||||||
@@ -243,13 +169,11 @@ async function submitGoal() {
|
|||||||
uni.showToast({ title: '请输入有效价格', icon: 'none' })
|
uni.showToast({ title: '请输入有效价格', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const coverImage = form.value.uploadedUrl || form.value.coverImage || ''
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.createRewardGoal({
|
await api.createRewardGoal({
|
||||||
title,
|
title,
|
||||||
target_amount_cent: Math.round(price * 100),
|
target_amount_cent: Math.round(price * 100),
|
||||||
cover_image: coverImage
|
cover_image: form.value.coverImage || ''
|
||||||
})
|
})
|
||||||
showForm.value = false
|
showForm.value = false
|
||||||
uni.showToast({ title: '添加成功', icon: 'success' })
|
uni.showToast({ title: '添加成功', icon: 'success' })
|
||||||
@@ -313,46 +237,11 @@ onMounted(async () => {
|
|||||||
.page {
|
.page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #F8FAFB;
|
background: #F8FAFB;
|
||||||
padding-bottom: 60rpx;
|
padding-bottom: 140rpx;
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 24rpx 28rpx;
|
|
||||||
padding-top: calc(env(safe-area-inset-top) + 24rpx);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-back {
|
|
||||||
font-size: 36rpx;
|
|
||||||
color: #374151;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-add {
|
|
||||||
font-size: 40rpx;
|
|
||||||
color: #14936d;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left, .header-right {
|
|
||||||
width: 60rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-right {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.saved-banner {
|
.saved-banner {
|
||||||
|
margin-top: 20rpx;
|
||||||
margin: 0 24rpx 20rpx;
|
margin: 0 24rpx 20rpx;
|
||||||
border-radius: 28rpx;
|
border-radius: 28rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -644,21 +533,32 @@ onMounted(async () => {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icons-loading, .icons-empty {
|
||||||
|
padding: 24rpx 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icons-loading-text, .icons-empty-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
.icon-grid {
|
.icon-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(5, 1fr);
|
||||||
gap: 16rpx;
|
gap: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-item {
|
.icon-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6rpx;
|
justify-content: center;
|
||||||
padding: 16rpx 8rpx;
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
border-radius: 16rpx;
|
border-radius: 16rpx;
|
||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
border: 2rpx solid transparent;
|
border: 2rpx solid transparent;
|
||||||
|
overflow: hidden;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,45 +568,12 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon-emoji {
|
.icon-emoji {
|
||||||
font-size: 36rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-name {
|
|
||||||
font-size: 20rpx;
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-area {
|
|
||||||
width: 100%;
|
|
||||||
height: 200rpx;
|
|
||||||
border-radius: 16rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #f9fafb;
|
|
||||||
border: 2rpx dashed rgba(15, 23, 42, 0.12);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-preview {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-placeholder {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-icon {
|
|
||||||
font-size: 40rpx;
|
font-size: 40rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-text {
|
.icon-img {
|
||||||
font-size: 22rpx;
|
width: 100%;
|
||||||
color: #9ca3af;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-actions {
|
.modal-actions {
|
||||||
@@ -734,4 +601,26 @@ onMounted(async () => {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
box-shadow: 0 8rpx 20rpx rgba(20, 147, 109, 0.2);
|
box-shadow: 0 8rpx 20rpx rgba(20, 147, 109, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fab {
|
||||||
|
position: fixed;
|
||||||
|
right: 40rpx;
|
||||||
|
bottom: calc(env(safe-area-inset-bottom) + 60rpx);
|
||||||
|
width: 100rpx;
|
||||||
|
height: 100rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #14936d, #10B981);
|
||||||
|
box-shadow: 0 12rpx 30rpx rgba(20, 147, 109, 0.35);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user