去掉协会 WebView 和魔方账密页,微信登录与咨询接口共用同一 token 和拦截器。 Co-authored-by: Cursor <cursoragent@cursor.com>
111 lines
3.4 KiB
Vue
111 lines
3.4 KiB
Vue
<template>
|
||
<yxg-page title="成长计划">
|
||
<view class="card">
|
||
<text class="lbl">新计划标题</text>
|
||
<input class="inp" v-model="title" maxlength="40" placeholder="例如:每晚 11 点前放下手机" />
|
||
<text class="lbl">焦点(可选)</text>
|
||
<input class="inp" v-model="focus" maxlength="40" placeholder="例如:保护睡眠与情绪" />
|
||
<view class="btn" :class="{ off: saving || !title.trim() }" @click="create"><text>创建计划</text></view>
|
||
<text v-if="err" class="err">{{ err }}</text>
|
||
</view>
|
||
<text v-if="loading" class="hint">加载中…</text>
|
||
<view v-for="p in plans" :key="p.id" class="card">
|
||
<text class="h1">{{ p.title }}</text>
|
||
<text v-if="p.focus" class="meta">{{ p.focus }}</text>
|
||
<view class="btn ghost" @click="checkin(p.id)">
|
||
<text>{{ checking === p.id ? '记录中…' : '今日打卡' }}</text>
|
||
</view>
|
||
<view v-for="c in (checkins[p.id] || [])" :key="c.id" class="ck">
|
||
<text>{{ c.day }}{{ c.note ? ' · ' + c.note : '' }}</text>
|
||
</view>
|
||
</view>
|
||
<text v-if="!loading && !plans.length" class="hint">还没有计划,先创建一个小目标吧。</text>
|
||
</yxg-page>
|
||
</template>
|
||
|
||
<script setup>
|
||
import YxgPage from '@/components/yxg/yxg-page.vue'
|
||
import { yxgApi } from '@/util/yxgApi.js'
|
||
import { ensureAccount } from '@/util/yxgAuth.js'
|
||
|
||
const plans = ref([])
|
||
const checkins = ref({})
|
||
const title = ref('')
|
||
const focus = ref('')
|
||
const loading = ref(true)
|
||
const saving = ref(false)
|
||
const checking = ref('')
|
||
const err = ref('')
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
if (!(await ensureAccount('/growth-plan'))) {
|
||
loading.value = false
|
||
return
|
||
}
|
||
try {
|
||
const res = await yxgApi.listGrowthPlans()
|
||
plans.value = res.items || []
|
||
const next = {}
|
||
for (const p of plans.value) {
|
||
const ck = await yxgApi.listGrowthCheckins(p.id)
|
||
next[p.id] = ck.items || []
|
||
}
|
||
checkins.value = next
|
||
} catch (e) {
|
||
err.value = e instanceof Error ? e.message : '加载失败'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function create() {
|
||
if (saving.value || !title.value.trim()) return
|
||
saving.value = true
|
||
err.value = ''
|
||
try {
|
||
await yxgApi.createGrowthPlan({ title: title.value.trim(), focus: focus.value.trim() || undefined })
|
||
title.value = ''
|
||
focus.value = ''
|
||
await load()
|
||
} catch (e) {
|
||
err.value = e instanceof Error ? e.message : '创建失败'
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
async function checkin(id) {
|
||
if (checking.value) return
|
||
checking.value = id
|
||
try {
|
||
await yxgApi.createGrowthCheckin(id, {})
|
||
await load()
|
||
} catch (e) {
|
||
uni.showToast({ title: e.message || '打卡失败', icon: 'none' })
|
||
} finally {
|
||
checking.value = ''
|
||
}
|
||
}
|
||
|
||
onShow(load)
|
||
</script>
|
||
|
||
<style>
|
||
page { background-color: #ffd1c7; }
|
||
</style>
|
||
<style scoped>
|
||
.card { margin: 12px 16px; padding: 16px; background: #fff; border-radius: 16px; }
|
||
.lbl { display: block; font-size: 12px; color: #999; margin: 8px 0 6px; }
|
||
.inp { width: 100%; height: 42px; background: #fdfaf8; border-radius: 12px; padding: 0 12px; box-sizing: border-box; }
|
||
.h1 { display: block; font-size: 16px; font-weight: 700; color: #333; }
|
||
.meta, .hint, .err, .ck { display: block; margin-top: 6px; font-size: 13px; color: #666; }
|
||
.err { color: #e54d42; }
|
||
.btn {
|
||
margin-top: 12px; height: 42px; border-radius: 999px; background: #e54d42; color: #fff;
|
||
display: flex; align-items: center; justify-content: center; font-weight: 700;
|
||
}
|
||
.btn.ghost { background: #f7f2ef; color: #8a4a3a; }
|
||
.btn.off { opacity: 0.45; }
|
||
</style>
|