feat: 修复 Redis 超时问题、固定小程序端口、新增人生事件模块及优化多个页面

- 修复 Redis 超时:添加 commons-pool2 依赖,启用 Lettuce 连接池,超时提升至 15s
- 固定 mini-program H5 端口为 5175,避免与 web 项目端口冲突
- 新增人生事件(life-event)模块:表单和详情页面
- 新增 EpicScript 灵感接口(Controller/Service/DTO)
- 优化登录、引导、主页、记录、剧本详情等多个页面
- 优化服务管理脚本和 Nginx 配置

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 11:38:35 +08:00
parent 507d1ebdab
commit 60c63850ee
36 changed files with 4545 additions and 3043 deletions
+404
View File
@@ -0,0 +1,404 @@
<template>
<view class="event-form-page">
<view class="space-bg"></view>
<view class="top-safe" :style="{ height: safeAreaTop + 'px' }"></view>
<view class="header">
<text class="back" @click="goBack"></text>
<view>
<text class="title">记录人生经历 </text>
<text class="subtitle">记录每一个重要时刻AI将帮你生成专属人生轨迹</text>
</view>
<text class="save" @click="submit">保存</text>
</view>
<scroll-view class="content" scroll-y :show-scrollbar="false">
<view class="form-card kos-card">
<view class="group">
<view class="group-title">
<text class="group-icon"></text>
<text>时间</text>
</view>
<view class="segmented">
<text v-for="item in timeModes" :key="item" class="seg active-first">{{ item }}</text>
</view>
<picker mode="date" :value="form.time" @change="e => form.time = e.detail.value">
<view class="date-picker field-box">
<text>{{ formatDate(form.time) }}</text>
<text class="chevron"></text>
</view>
</picker>
</view>
<view class="group">
<view class="label-row">
<view class="group-title">
<text class="group-icon"></text>
<text>事件标题 </text>
</view>
<text class="count">{{ form.title.length }}/30</text>
</view>
<input class="field-box input" maxlength="30" v-model="form.title" placeholder="给这段经历起个标题吧..." placeholder-class="placeholder" />
</view>
<view class="group">
<view class="label-row">
<view class="group-title">
<text class="group-icon"></text>
<text>具体内容 </text>
</view>
<text class="count">{{ form.content.length }}/500</text>
</view>
<view class="textarea-wrap">
<textarea class="textarea" maxlength="500" v-model="form.content" placeholder="请详细记录这段经历的背景、发生的事情、你的感受和收获..." placeholder-class="placeholder" />
<view class="ai-btn kos-pill" @click="assistWrite"> AI 帮我写</view>
</view>
</view>
<view class="group">
<view class="label-row">
<view class="group-title">
<text class="group-icon"></text>
<text>相关标签</text>
</view>
<view class="custom-tag kos-pill"> 自定义标签</view>
</view>
<view class="tag-grid">
<text
v-for="tag in tags"
:key="tag"
class="tag kos-pill"
:class="{ active: form.tags.includes(tag) }"
@click="toggleTag(tag)"
>{{ tag }}</text>
</view>
</view>
<button class="submit kos-primary" :loading="saving" @click="submit">
<text> 提交记录</text>
<text class="submit-sub">记录后可在时间轴查看</text>
</button>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { useAppStore } from '../../stores/app.js'
const store = useAppStore()
const safeAreaTop = ref(20)
const saving = ref(false)
const form = reactive({
title: '',
time: new Date().toISOString().slice(0, 10),
content: '',
tags: [],
eventType: 'daily_log'
})
const timeModes = ['具体日期', '年月', '季节', '时间轴范围']
const tags = ['成长', '学习', '工作', '旅行', '感情', '家庭', '友情', '挑战', '突破', '收获', '感动', '迷茫']
onMounted(() => {
const info = uni.getWindowInfo()
safeAreaTop.value = info.safeAreaInsets?.top || info.statusBarHeight || 20
})
const formatDate = (value) => {
if (!value) return '选择具体日期'
const [year, month, day] = value.split('-')
return `${year}${month}${day}`
}
const toggleTag = (tag) => {
const index = form.tags.indexOf(tag)
if (index >= 0) form.tags.splice(index, 1)
else form.tags.push(tag)
}
const assistWrite = () => {
if (!form.content) {
form.content = '那一天,我清楚地感受到自己正在经历一次变化。事情本身也许并不宏大,但它让我重新看见了自己的选择、情绪和力量。'
}
}
const submit = async () => {
if (!form.title || !form.content || saving.value) {
uni.showToast({ title: '请填写标题和内容', icon: 'none' })
return
}
saving.value = true
const result = await store.createEvent({ ...form, aiFeedback: buildAiFeedback() })
saving.value = false
if (result.success) {
uni.navigateBack()
} else {
uni.showToast({ title: result.error || '保存失败', icon: 'none' })
}
}
const buildAiFeedback = () => {
return '这段经历体现了你的自我观察能力。它可能意味着你正在从旧的反应模式里走出来,开始更主动地理解自己的处境。建议保留当时的细节,因为这些细节会成为后续人生剧本的重要素材。'
}
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>
.event-form-page {
position: relative;
height: 100vh;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
color: #fff;
background: #050615;
}
.space-bg {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 86% 88%, rgba(126, 57, 255, 0.34), transparent 30%),
radial-gradient(circle at 18% 8%, rgba(63, 120, 255, 0.18), transparent 26%),
linear-gradient(180deg, #07091d 0%, #07031a 52%, #04020e 100%);
}
.top-safe,
.header,
.content {
position: relative;
z-index: 1;
}
.top-safe,
.header {
flex-shrink: 0;
}
.header {
min-height: 138rpx;
display: grid;
grid-template-columns: 72rpx 1fr 72rpx;
align-items: center;
padding: 0 28rpx;
}
.back {
color: #fff;
font-size: 70rpx;
}
.title,
.subtitle {
display: block;
text-align: center;
}
.title {
color: #fff;
font-size: 36rpx;
font-weight: 900;
}
.subtitle {
margin-top: 14rpx;
color: rgba(218, 204, 243, 0.72);
font-size: 24rpx;
}
.save {
text-align: right;
color: #c06dff;
font-size: 27rpx;
}
.content {
flex: 1;
height: 0;
min-height: 0;
box-sizing: border-box;
padding: 0 28rpx 34rpx;
}
.form-card {
border-radius: 34rpx;
padding: 30rpx;
}
.group + .group {
margin-top: 34rpx;
}
.group-title,
.label-row {
display: flex;
align-items: center;
}
.label-row {
justify-content: space-between;
}
.group-title {
gap: 14rpx;
color: #d4b7ff;
font-size: 31rpx;
font-weight: 900;
}
.group-icon {
color: #a855ff;
font-size: 38rpx;
}
.count,
.custom-tag {
color: #b58bff;
font-size: 23rpx;
}
.custom-tag {
height: 50rpx;
padding: 0 18rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
}
.segmented {
height: 66rpx;
margin-top: 24rpx;
border-radius: 999rpx;
padding: 5rpx;
display: grid;
grid-template-columns: repeat(4, 1fr);
border: 1rpx solid rgba(151, 111, 255, 0.26);
background: rgba(11, 12, 38, 0.7);
}
.seg {
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
color: rgba(224, 214, 243, 0.72);
font-size: 23rpx;
}
.seg:first-child {
color: #fff;
background: linear-gradient(135deg, #a843ff, #7630ff);
box-shadow: 0 0 24rpx rgba(168, 67, 255, 0.5);
}
.field-box,
.textarea {
width: 100%;
box-sizing: border-box;
border-radius: 22rpx;
border: 1rpx solid rgba(151, 111, 255, 0.24);
background: rgba(12, 15, 46, 0.72);
color: #fff;
font-size: 27rpx;
}
.date-picker {
height: 78rpx;
margin-top: 22rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.chevron {
color: rgba(224, 214, 243, 0.62);
font-size: 52rpx;
}
.input {
height: 82rpx;
margin-top: 18rpx;
padding: 0 24rpx;
}
.textarea-wrap {
position: relative;
margin-top: 18rpx;
}
.textarea {
height: 258rpx;
padding: 24rpx;
line-height: 1.6;
}
.ai-btn {
position: absolute;
right: 18rpx;
bottom: 18rpx;
height: 54rpx;
padding: 0 18rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
color: #d5b3ff;
font-size: 23rpx;
}
.tag-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16rpx;
margin-top: 22rpx;
}
.tag {
height: 58rpx;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
color: rgba(224, 214, 243, 0.68);
font-size: 24rpx;
}
.tag.active {
color: #fff;
border-color: rgba(192, 100, 255, 0.84);
background: rgba(137, 51, 255, 0.35);
box-shadow: 0 0 22rpx rgba(168, 67, 255, 0.34);
}
.submit {
width: 100%;
height: 112rpx;
margin-top: 38rpx;
border-radius: 28rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.submit text {
display: block;
color: #fff;
font-size: 31rpx;
font-weight: 900;
}
.submit-sub {
margin-top: 8rpx;
color: rgba(255, 255, 255, 0.72) !important;
font-size: 23rpx !important;
font-weight: 500 !important;
}
</style>