Files
happy-life-star/mini-program/src/pages/life-event/form.vue
T

898 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="record-page">
<view class="space-bg"></view>
<view class="safe-top" :style="{ height: safeAreaTop + 10 + 'px' }"></view>
<view class="header">
<view class="back-hit" @click="goBack"><view class="back-icon"></view></view>
<view class="heading">
<text class="title">记录人生经历</text>
<text class="title-star"></text>
</view>
<text class="subtitle">记录每一个重要时刻AI将帮你生成专属人生轨迹</text>
</view>
<scroll-view class="content" scroll-y :show-scrollbar="false">
<view class="form-card">
<view class="group time-group">
<view class="group-title">
<view class="clock-icon"></view>
<text>时间</text>
</view>
<view class="segmented">
<text
v-for="item in timeModes"
:key="item.value"
class="seg"
:class="{ active: form.timeMode === item.value }"
@click="switchTimeMode(item.value)"
>{{ item.label }}</text>
</view>
<picker
v-if="form.timeMode === 'date'"
mode="date"
:value="form.time"
@change="e => setDate(e.detail.value)"
>
<view class="picker-field">
<view class="calendar-icon"></view>
<text>{{ formatDate(form.time) }}</text>
<text class="chevron"></text>
</view>
</picker>
<picker
v-else-if="form.timeMode === 'month'"
mode="multiSelector"
:range="monthRange"
:value="monthPickerValue"
@change="onMonthChange"
>
<view class="picker-field">
<view class="calendar-icon"></view>
<text>{{ formatMonth(form.eventDateText) }}</text>
<text class="chevron"></text>
</view>
</picker>
<picker
v-else-if="form.timeMode === 'season'"
mode="multiSelector"
:range="seasonRange"
:value="seasonPickerValue"
@change="onSeasonChange"
>
<view class="picker-field">
<view class="calendar-icon"></view>
<text>{{ formatSeason(form.eventDateText) }}</text>
<text class="chevron"></text>
</view>
</picker>
<view v-else class="range-fields">
<picker mode="date" :value="form.time" @change="e => setRangeStart(e.detail.value)">
<view class="picker-field compact">
<view class="calendar-icon"></view>
<text class="range-date-text">{{ formatShortDate(form.time) }}</text>
</view>
</picker>
<text class="range-line"></text>
<picker mode="date" :value="form.endTime" @change="e => setRangeEnd(e.detail.value)">
<view class="picker-field compact">
<text class="range-date-text">{{ formatShortDate(form.endTime) }}</text>
<text class="chevron"></text>
</view>
</picker>
</view>
</view>
<view class="divider"></view>
<view class="group">
<view class="label-row">
<view class="group-title">
<view class="pen-icon"></view>
<text>事件标题</text>
<text class="field-star"></text>
</view>
<text class="count">{{ form.title.length }}/30</text>
</view>
<input
class="input-field"
maxlength="30"
v-model="form.title"
placeholder="给这段经历起个标题吧..."
placeholder-class="placeholder"
/>
</view>
<view class="group">
<view class="label-row">
<view class="group-title">
<view class="note-icon"></view>
<text>具体内容</text>
<text class="field-star"></text>
</view>
<text class="count">{{ form.content.length }}/500</text>
</view>
<view class="textarea-wrap">
<textarea
class="textarea-field"
maxlength="500"
v-model="form.content"
:placeholder="contentPlaceholder"
placeholder-class="placeholder"
/>
<view class="ai-btn" @click="assistWrite">
<text class="sparkle"></text>
<text>AI 帮我写</text>
</view>
</view>
</view>
<view class="group tag-section">
<view class="label-row">
<view class="group-title">
<view class="heart-icon"></view>
<text>相关标签</text>
<text class="optional">可多选</text>
</view>
<view class="custom-tag" @click="addCustomTag"> 自定义标签</view>
</view>
<view class="tag-grid">
<text
v-for="tag in tags"
:key="tag"
class="tag"
:class="{ active: form.tags.includes(tag) }"
@click="toggleTag(tag)"
>{{ tag }}</text>
</view>
</view>
<button class="submit-btn" :loading="saving" @click="submit">
<text class="submit-title"> 提交记录</text>
<text class="submit-sub">记录后可在时间轴查看</text>
</button>
<view class="planet"></view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { useAppStore } from '../../stores/app.js'
const store = useAppStore()
const safeAreaTop = ref(20)
const saving = ref(false)
const currentYear = new Date().getFullYear()
const form = reactive({
id: '',
title: '',
time: new Date().toISOString().slice(0, 10),
endTime: new Date().toISOString().slice(0, 10),
timeMode: 'date',
eventDateText: new Date().toISOString().slice(0, 10),
content: '',
tags: [],
eventType: 'daily_log'
})
const timeModes = [
{ label: '具体日期', value: 'date' },
{ label: '年月', value: 'month' },
{ label: '季节', value: 'season' },
{ label: '时间轴范围', value: 'range' }
]
const years = Array.from({ length: 81 }, (_, index) => String(currentYear - 60 + index))
const months = Array.from({ length: 12 }, (_, index) => `${String(index + 1).padStart(2, '0')}`)
const seasons = [
{ label: '春季', value: 'spring' },
{ label: '夏季', value: 'summer' },
{ label: '秋季', value: 'autumn' },
{ label: '冬季', value: 'winter' }
]
const monthRange = [years, months]
const seasonRange = [years, seasons.map(item => item.label)]
const tags = ref(['成长', '学习', '工作', '旅行', '感情', '家庭', '友情', '挑战', '突破', '收获', '感动', '迷茫'])
const contentPlaceholder = '请详细记录这段经历的背景、发生的事情、你的感受和收获...'
const monthPickerValue = computed(() => {
const [year, month] = (form.eventDateText || form.time).split('-')
return [
Math.max(0, years.indexOf(year)),
Math.max(0, Number(month || '01') - 1)
]
})
const seasonPickerValue = computed(() => {
const [year, season = 'spring'] = (form.eventDateText || `${currentYear}-spring`).split('-')
return [
Math.max(0, years.indexOf(year)),
Math.max(0, seasons.findIndex(item => item.value === season))
]
})
onMounted(() => {
const info = uni.getWindowInfo()
safeAreaTop.value = info.safeAreaInsets?.top || info.statusBarHeight || 20
const pages = getCurrentPages()
const id = pages[pages.length - 1]?.options?.id || ''
if (id) loadEvent(id)
})
const switchTimeMode = (mode) => {
form.timeMode = mode
if (mode === 'date') form.eventDateText = form.time
if (mode === 'month') form.eventDateText = form.time.slice(0, 7)
if (mode === 'season') form.eventDateText = `${form.time.slice(0, 4)}-spring`
if (mode === 'range') {
form.eventDateText = `${form.time}${form.endTime || form.time}`
if (!form.endTime) form.endTime = form.time
}
}
const setDate = (value) => {
form.time = value
form.eventDateText = value
}
const setRangeStart = (value) => {
form.time = value
form.eventDateText = `${form.time}${form.endTime || form.time}`
}
const setRangeEnd = (value) => {
form.endTime = value
form.eventDateText = `${form.time}${form.endTime}`
}
const onMonthChange = (event) => {
const [yearIndex, monthIndex] = event.detail.value
const year = years[yearIndex]
const month = String(monthIndex + 1).padStart(2, '0')
form.time = `${year}-${month}-01`
form.eventDateText = `${year}-${month}`
}
const onSeasonChange = (event) => {
const [yearIndex, seasonIndex] = event.detail.value
const year = years[yearIndex]
const season = seasons[seasonIndex]
const monthMap = { spring: '03', summer: '06', autumn: '09', winter: '12' }
form.time = `${year}-${monthMap[season.value]}-01`
form.eventDateText = `${year}-${season.value}`
}
const formatDate = (value) => {
if (!value) return '选择日期'
const [year, month, day] = value.split('-')
return `${year}${month}${day}`
}
const formatShortDate = (value) => {
if (!value) return '选择日期'
return String(value).replaceAll('-', '.')
}
const formatMonth = (value) => {
const [year, month] = (value || form.time.slice(0, 7)).split('-')
return `${year}${month}`
}
const formatSeason = (value) => {
const [year, season = 'spring'] = (value || `${currentYear}-spring`).split('-')
const label = seasons.find(item => item.value === season)?.label || '春季'
return `${year}${label}`
}
const toggleTag = (tag) => {
const index = form.tags.indexOf(tag)
if (index >= 0) form.tags.splice(index, 1)
else form.tags.push(tag)
}
const addCustomTag = () => {
uni.showModal({
title: '自定义标签',
editable: true,
placeholderText: '输入标签名称',
success: (res) => {
const value = String(res.content || '').trim()
if (!res.confirm || !value) return
if (!tags.value.includes(value)) tags.value.push(value)
if (!form.tags.includes(value)) form.tags.push(value)
}
})
}
const loadEvent = async (id) => {
if (!store.events?.length) await store.fetchEvents()
const event = store.getEventById(id)
if (!event) return
form.id = event.id
form.title = event.title || ''
form.time = event.time || new Date().toISOString().slice(0, 10)
form.endTime = event.endTime || form.time
form.timeMode = event.timeMode || 'date'
form.eventDateText = event.eventDateText || form.time
form.content = event.content || ''
form.tags = Array.isArray(event.tags) ? [...event.tags] : []
form.eventType = event.eventType || 'daily_log'
form.tags.forEach(tag => {
if (!tags.value.includes(tag)) tags.value.push(tag)
})
}
const assistWrite = async () => {
const result = await store.assistEventWriting({ ...form })
if (!result.success) {
uni.showToast({ title: result.error || 'AI 帮写失败', icon: 'none' })
return
}
if (result.data?.content) form.content = result.data.content
if (Array.isArray(result.data?.tags)) {
result.data.tags.forEach(tag => {
if (!tags.value.includes(tag)) tags.value.push(tag)
if (!form.tags.includes(tag)) form.tags.push(tag)
})
}
uni.showToast({ title: result.data?.placeholder ? 'AI 占位内容已生成' : 'AI 已帮你优化', icon: 'none' })
}
const submit = async () => {
if (!form.title || !form.content || saving.value) {
uni.showToast({ title: '请填写标题和内容', icon: 'none' })
return
}
saving.value = true
const payload = { ...form, aiFeedback: buildAiFeedback() }
const result = form.id ? await store.updateEvent(payload) : await store.createEvent(payload)
saving.value = false
if (result.success) {
uni.navigateBack()
} else {
uni.showToast({ title: result.error || '保存失败', icon: 'none' })
}
}
const buildAiFeedback = () => {
return [
'## 心理状态解读',
'你正在认真整理这段经历,它会成为后续人生剧本的重要素材。',
'',
'---',
'',
'## 成长意义',
'- 这段记录体现了你的自我观察能力。',
'- 保留当时的细节,会帮助 AI 更准确地理解你的生命轨迹。'
].join('\n')
}
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>
.record-page {
position: relative;
height: 100vh;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
color: #fff;
background: #050717;
}
.space-bg {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 20% 4%, rgba(63, 91, 255, 0.22), transparent 28%),
radial-gradient(circle at 88% 92%, rgba(136, 63, 255, 0.36), transparent 30%),
linear-gradient(180deg, #070b25 0%, #08031d 52%, #05020f 100%);
}
.space-bg::after {
content: '';
position: absolute;
inset: 0;
opacity: 0.3;
background-image:
radial-gradient(circle, rgba(255,255,255,0.7) 0 1rpx, transparent 2rpx),
radial-gradient(circle, rgba(165,105,255,0.52) 0 1rpx, transparent 2rpx);
background-size: 160rpx 190rpx, 250rpx 220rpx;
background-position: 44rpx 44rpx, 10rpx 112rpx;
}
.safe-top,
.header,
.content {
position: relative;
z-index: 1;
}
.safe-top,
.header {
flex-shrink: 0;
}
.header {
min-height: 154rpx;
display: grid;
grid-template-columns: 90rpx 1fr 90rpx;
grid-template-rows: auto auto;
align-items: center;
padding: 4rpx 32rpx 24rpx;
box-sizing: border-box;
}
.back-hit {
grid-row: 1 / 3;
width: 68rpx;
height: 68rpx;
display: flex;
align-items: center;
}
.back-icon {
width: 28rpx;
height: 28rpx;
border-left: 6rpx solid #fff;
border-bottom: 6rpx solid #fff;
transform: rotate(45deg);
margin-left: 12rpx;
}
.heading {
display: flex;
justify-content: center;
align-items: center;
gap: 14rpx;
}
.title {
font-size: 42rpx;
font-weight: 900;
}
.title-star,
.field-star {
color: #ffd36e;
text-shadow: 0 0 18rpx rgba(255, 198, 85, 0.8);
}
.subtitle {
grid-column: 2;
display: block;
margin-top: 22rpx;
color: rgba(210, 194, 242, 0.78);
font-size: 25rpx;
text-align: center;
}
.content {
flex: 1;
height: 0;
min-height: 0;
padding: 0 28rpx 28rpx;
box-sizing: border-box;
}
.form-card {
position: relative;
overflow: hidden;
border-radius: 30rpx;
padding: 36rpx 26rpx 84rpx;
border: 2rpx solid rgba(145, 70, 255, 0.76);
background:
radial-gradient(circle at 52% 8%, rgba(126, 72, 255, 0.13), transparent 35%),
rgba(12, 13, 46, 0.72);
box-shadow: 0 0 30rpx rgba(156, 73, 255, 0.24), inset 0 0 34rpx rgba(143, 92, 255, 0.08);
}
.group + .group {
margin-top: 48rpx;
}
.group-title,
.label-row {
display: flex;
align-items: center;
}
.label-row {
justify-content: space-between;
}
.group-title {
gap: 14rpx;
color: #d7bdff;
font-size: 29rpx;
font-weight: 900;
}
.clock-icon,
.pen-icon,
.note-icon,
.heart-icon,
.calendar-icon {
position: relative;
width: 32rpx;
height: 32rpx;
color: #a855ff;
flex-shrink: 0;
}
.clock-icon {
border: 4rpx solid currentColor;
border-radius: 50%;
}
.clock-icon::before {
content: '';
position: absolute;
left: 13rpx;
top: 5rpx;
width: 4rpx;
height: 11rpx;
background: currentColor;
}
.clock-icon::after {
content: '';
position: absolute;
left: 13rpx;
top: 14rpx;
width: 10rpx;
height: 4rpx;
background: currentColor;
}
.pen-icon {
border-left: 5rpx solid currentColor;
border-bottom: 5rpx solid currentColor;
transform: skew(-12deg);
}
.pen-icon::after {
content: '';
position: absolute;
left: 4rpx;
bottom: 7rpx;
width: 31rpx;
height: 5rpx;
border-radius: 999rpx;
background: currentColor;
transform: rotate(-45deg);
}
.note-icon {
border: 4rpx solid currentColor;
border-radius: 5rpx;
box-sizing: border-box;
}
.note-icon::after {
content: '';
position: absolute;
right: -5rpx;
bottom: 4rpx;
width: 15rpx;
height: 5rpx;
border-radius: 999rpx;
background: currentColor;
transform: rotate(-45deg);
}
.heart-icon::before,
.heart-icon::after {
content: '';
position: absolute;
top: 6rpx;
width: 18rpx;
height: 26rpx;
border: 4rpx solid currentColor;
border-bottom: 0;
border-radius: 18rpx 18rpx 0 0;
}
.heart-icon::before {
left: 2rpx;
transform: rotate(-45deg);
}
.heart-icon::after {
right: 2rpx;
transform: rotate(45deg);
}
.segmented {
height: 68rpx;
margin-top: 26rpx;
border-radius: 999rpx;
padding: 5rpx;
display: grid;
grid-template-columns: repeat(4, 1fr);
border: 1rpx solid rgba(151, 111, 255, 0.3);
background: rgba(8, 10, 34, 0.72);
}
.seg {
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
color: rgba(224, 214, 243, 0.78);
font-size: 22rpx;
font-weight: 600;
}
.seg.active {
color: #fff;
background: linear-gradient(135deg, #a843ff, #7b31ff);
box-shadow: 0 0 26rpx rgba(168, 67, 255, 0.58);
}
.picker-field,
.input-field,
.textarea-field {
width: 100%;
box-sizing: border-box;
border-radius: 22rpx;
border: 1rpx solid rgba(151, 111, 255, 0.28);
background: rgba(12, 15, 46, 0.74);
color: #fff;
font-size: 25rpx;
}
.picker-field {
height: 78rpx;
margin-top: 26rpx;
padding: 0 22rpx;
display: flex;
align-items: center;
gap: 14rpx;
}
.picker-field text:nth-child(2) {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.picker-field.compact {
margin-top: 0;
min-width: 0;
height: 72rpx;
padding: 0 16rpx;
}
.calendar-icon {
width: 28rpx;
height: 28rpx;
border: 4rpx solid #a855ff;
border-radius: 6rpx;
box-sizing: border-box;
}
.calendar-icon::before {
content: '';
position: absolute;
left: 3rpx;
right: 3rpx;
top: 8rpx;
height: 3rpx;
background: #a855ff;
}
.chevron {
flex: none !important;
color: rgba(224, 214, 243, 0.7);
font-size: 42rpx;
line-height: 1;
}
.range-fields {
margin-top: 26rpx;
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
gap: 10rpx;
}
.range-fields picker {
min-width: 0;
}
.range-line {
color: rgba(224, 214, 243, 0.7);
font-size: 22rpx;
font-weight: 700;
}
.range-date-text {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.divider {
height: 1rpx;
margin: 34rpx 34rpx 34rpx;
background: linear-gradient(90deg, transparent, rgba(153, 89, 255, 0.34), transparent);
}
.count,
.optional,
.custom-tag {
color: #b58bff;
font-size: 22rpx;
font-weight: 600;
}
.optional {
color: rgba(217, 201, 244, 0.78);
}
.input-field {
height: 92rpx;
margin-top: 22rpx;
padding: 0 24rpx;
}
.textarea-wrap {
position: relative;
margin-top: 22rpx;
}
.textarea-field {
height: 276rpx;
padding: 26rpx 24rpx 82rpx;
line-height: 1.6;
}
.ai-btn,
.custom-tag {
border-radius: 999rpx;
border: 1rpx solid rgba(151, 111, 255, 0.34);
background: rgba(27, 18, 64, 0.72);
}
.ai-btn {
position: absolute;
right: 18rpx;
bottom: 18rpx;
height: 54rpx;
padding: 0 18rpx;
display: flex;
align-items: center;
gap: 8rpx;
color: #d5b3ff;
font-size: 23rpx;
}
.sparkle {
color: #d783ff;
}
.custom-tag {
height: 50rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
}
.tag-section {
margin-top: 52rpx !important;
}
.tag-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 18rpx 16rpx;
margin-top: 28rpx;
}
.tag {
height: 58rpx;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
color: rgba(224, 214, 243, 0.74);
font-size: 22rpx;
border: 1rpx solid rgba(151, 111, 255, 0.2);
background: rgba(20, 22, 58, 0.58);
}
.tag.active {
color: #fff;
border-color: rgba(192, 100, 255, 0.86);
background: rgba(137, 51, 255, 0.35);
box-shadow: 0 0 22rpx rgba(168, 67, 255, 0.34);
}
.submit-btn {
position: relative;
z-index: 1;
width: 100%;
height: 116rpx;
margin-top: 38rpx;
border: 0;
border-radius: 30rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
background: linear-gradient(135deg, #a843ff, #7330ff 58%, #3121ed);
box-shadow: 0 0 34rpx rgba(168, 67, 255, 0.55);
}
.submit-btn::after {
border: 0;
}
.submit-title,
.submit-sub {
display: block;
color: #fff;
}
.submit-title {
font-size: 31rpx;
font-weight: 900;
}
.submit-sub {
margin-top: 8rpx;
color: rgba(255, 255, 255, 0.72);
font-size: 23rpx;
font-weight: 500;
}
.planet {
position: absolute;
right: -26rpx;
bottom: -44rpx;
width: 166rpx;
height: 112rpx;
border-radius: 50%;
border: 3rpx solid rgba(177, 89, 255, 0.62);
box-shadow: 0 0 38rpx rgba(171, 72, 255, 0.48);
transform: rotate(-22deg);
opacity: 0.7;
}
.planet::before {
content: '';
position: absolute;
left: -20rpx;
right: -20rpx;
top: 48rpx;
height: 18rpx;
border-radius: 50%;
border: 4rpx solid rgba(195, 111, 255, 0.66);
}
.placeholder {
color: rgba(213, 199, 239, 0.52);
}
</style>