Compare commits

..

4 Commits

Author SHA1 Message Date
Wenzhe 6e0be666fe v1.1.6
1、api调用库调整好http状态码处理逻辑
2、增加用户信息提交api类
3、完成用户信息提交处理,包括提交后不再弹出、重复提交、错误提交、完成提交
2026-02-02 14:56:25 +08:00
Wenzhe 831acc6a25 v1.1.5
1、scroll-view禁止下拉刷新
2、抽奖表单调整,弹窗层级不能超过toast
2026-02-02 13:55:54 +08:00
Wenzhe b4b874cdf2 v1.1.4
1、前门模块播放/停止按钮逻辑更换
2、抽奖对话框封装为组件形式,临时版本
2026-02-02 12:27:15 +08:00
Wenzhe 2995cdb3e2 v1.1.3
1、增加页面访问api
2、背景图片更换
3、福字摇晃幅度加大
2026-02-02 11:29:25 +08:00
18 changed files with 946 additions and 60 deletions

275
api/API_DOCUMENTATION.md Normal file
View File

@ -0,0 +1,275 @@
# H5海报项目 API 文档
本文档描述了H5海报项目的后端API接口供前端开发使用。
## 基础信息
- **Base URL**: `http://localhost:8080` (开发环境) / 生产环境使用相对路径
- **Content-Type**: `application/json`
- **超时时间**: 60秒
- **调试模式**: 通过配置文件中的 `debug` 字段控制,默认为 `true`
## UUID关联机制
本系统使用UUID来跟踪用户的完整操作流程
1. **页面访问** (`/api/page-visit`): 用户首次访问页面时生成唯一的UUID
2. **对联生成** (`/api/couplets`): 使用页面访问的UUID关联海报生成操作
3. **用户信息提交** (`/api/user-info`): 使用相同的UUID关联用户信息提交
这种机制允许系统追踪从页面访问到最终用户提交的完整用户行为路径。
## 调试模式配置
系统支持通过配置文件控制Gin框架的运行模式
- **debug: true** (默认): Gin运行在调试模式输出详细的错误信息和日志
- **debug: false**: Gin运行在发布模式性能更好错误信息更简洁
建议开发环境使用 `true`,生产环境使用 `false`
## API端点
### 1. 页面访问记录 - `/api/page-visit`
记录用户页面访问信息,用于统计分析。
#### 请求信息
- **方法**: `POST`
- **路径**: `/api/page-visit`
#### 请求参数
| 参数名 | 类型 | 必需 | 描述 |
|--------|------|------|------|
| user_agent | string | 是 | 用户浏览器User-Agent字符串 |
| page_name | string | 是 | 页面名称,用于标识访问的页面 |
#### 请求示例
```json
{
"user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X)",
"page_name": "home"
}
```
#### 响应信息
**成功响应** (200 OK)
```json
{
"message": "Page visit recorded successfully",
"id": 123,
"uuid": "550e8400-e29b-41d4-a716-446655440000"
}
```
**错误响应** (400 Bad Request)
```json
{
"error": "Invalid request data"
}
```
**错误响应** (409 Conflict)
```json
{
"error": "该手机号码已经提交过用户信息,请勿重复提交"
}
```
**错误响应** (409 Conflict)
```json
{
"error": "该页面访问已经提交过用户信息,请勿重复提交"
}
```
**错误响应** (500 Internal Server Error)
```json
{
"error": "Database not available"
}
```
#### 使用说明
- 每次用户访问页面时调用此API
- 系统会自动记录用户的IP地址
- 如果数据库不可用API会返回500错误
- 建议在页面加载完成后异步调用,不影响用户体验
- **重要**: 返回的UUID需要保存后续的couplets和user-info接口需要使用这个UUID进行关联
---
### 2. 用户信息保存 - `/api/user-info`
保存用户填写的个人信息,用于活动参与或后续联系。
#### 请求信息
- **方法**: `POST`
- **路径**: `/api/user-info`
#### 请求参数
| 参数名 | 类型 | 必需 | 描述 |
|--------|------|------|------|
| name | string | 是 | 用户姓名 |
| phone | string | 是 | 用户手机号码 |
| address | string | 是 | 用户地址 |
| msg | string | 是 | 用户留言或备注信息最多200字 |
| page_visit_uuid | string | 是 | 页面访问UUID用于关联页面访问记录 |
#### 请求示例
```json
{
"name": "张三",
"phone": "13800138000",
"address": "北京市朝阳区xxx街道xxx号",
"msg": "希望能够获得精美礼品,谢谢!",
"page_visit_uuid": "550e8400-e29b-41d4-a716-446655440000"
}
```
#### 响应信息
**成功响应** (200 OK)
```json
{
"message": "User info saved successfully",
"id": 456
}
```
**错误响应** (400 Bad Request)
```json
{
"error": "Invalid request data"
}
```
**错误响应** (500 Internal Server Error)
```json
{
"error": "Database not available"
}
```
#### 使用说明
- 用户在表单中填写信息后提交到此API
- 所有字段都是必填项
- msg字段最多支持200个字符用于用户留言或备注
- 系统会自动记录创建时间
- 建议在前端进行基础的数据验证(如手机号格式)
- **重要**: 必须使用之前从`/api/page-visit`获得的UUID作为`page_visit_uuid`参数
- **防重复提交**: 系统会检查手机号和页面访问UUID的唯一性如果发现重复提交会返回409错误
---
### 3. 对联海报生成 - `/api/couplets`
根据用户输入的两个汉字使用AI生成对联并创建海报。
#### 请求信息
- **方法**: `POST`
- **路径**: `/api/couplets`
#### 请求参数
| 参数名 | 类型 | 必需 | 描述 |
|--------|------|------|------|
| title | string | 是 | 两个汉字,用于生成对联(如"新春" |
| page_visit_uuid | string | 是 | 页面访问UUID用于关联页面访问记录 |
#### 请求示例
```json
{
"title": "新春",
"page_visit_uuid": "550e8400-e29b-41d4-a716-446655440000"
}
```
#### 响应信息
**成功响应** (200 OK)
```json
{
"share_url": "http://xcsq.wxinh5.host/share/abc123def456",
"poster_id": "abc123def456",
"image_url": "http://xcsq.wxinh5.host/posters/couplet_abc123def456.png"
}
```
**错误响应** (400 Bad Request)
```json
{
"error": "Invalid request data"
}
```
**错误响应** (500 Internal Server Error)
```json
{
"error": "Failed to generate couplet poster: API key is not configured"
}
```
#### 使用说明
- 用户需要输入两个汉字作为对联生成的主题
- 系统会调用AI生成对联上联、下联、横批
- 生成的对联会制作成海报图片
- 返回的`image_url`可以直接用于显示海报
- 返回的`share_url`可以用于分享功能
- 如果AI服务不可用系统会使用默认对联
- **重要**: 必须使用之前从`/api/page-visit`获得的UUID作为`page_visit_uuid`参数
---
## 错误处理
所有API在出错时都会返回相应的HTTP状态码和错误信息
- **400 Bad Request**: 请求参数错误或格式不正确
- **500 Internal Server Error**: 服务器内部错误或数据库不可用
错误响应格式:
```json
{
"error": "错误描述信息"
}
```
## 使用建议
1. **错误处理**: 前端应该适当处理API返回的错误信息给用户友好的提示
2. **重试机制**: 对于网络错误,可以考虑实现重试机制
3. **超时处理**: 设置合适的超时时间,避免用户长时间等待
4. **异步调用**: 建议使用异步方式调用API不影响用户体验
## 前端使用示例
可以参考项目中的API封装文件`frontend/src/utils/api.js`
```javascript
import axios from 'axios'
const api = axios.create({
baseURL: 'http://localhost:8080',
timeout: 60000,
headers: {
'Content-Type': 'application/json'
}
})
// 使用示例
const recordVisit = async () => {
try {
const response = await api.post('/api/page-visit', {
user_agent: navigator.userAgent,
page_name: 'current_page'
})
console.log('Visit recorded:', response.data)
} catch (error) {
console.error('Failed to record visit:', error)
}
}
```

56
api/api.js Normal file
View File

@ -0,0 +1,56 @@
import axios from 'axios'
const API_BASE_URL = import.meta.env.PROD ? '' : 'http://localhost:8080'
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 60000,
headers: {
'Content-Type': 'application/json'
}
})
export const generatePoster = async (posterData) => {
try {
const response = await api.post('/api/posters', posterData)
return response.data
} catch (error) {
throw new Error(error.response?.data?.error || '生成海报失败')
}
}
export const getPoster = async (posterId) => {
try {
const response = await api.get(`/api/posters/${posterId}`)
return response.data
} catch (error) {
throw new Error(error.response?.data?.error || '获取海报失败')
}
}
export const generateCoupletPoster = async (coupletData) => {
try {
const response = await api.post('/api/couplets', coupletData)
return response.data
} catch (error) {
throw new Error(error.response?.data?.error || '生成对联海报失败')
}
}
export const saveUserInfo = async (userInfo) => {
try {
const response = await api.post('/api/user-info', userInfo)
return response.data
} catch (error) {
throw new Error(error.response?.data?.error || '保存用户信息失败')
}
}
export const recordPageVisit = async (pageVisitData) => {
try {
const response = await api.post('/api/page-visit', pageVisitData)
return response.data
} catch (error) {
console.error('Failed to record page visit:', error)
}
}

166
api/request.js Normal file
View File

@ -0,0 +1,166 @@
/**
* API 请求封装
* 基于 uni.request 封装支持拦截器错误处理等
*/
// API 基础配置
const BASE_URL = 'http://xcsq.wxinh5.host'
// 请求拦截器
const requestInterceptor = (config) => {
// 可以在这里添加全局请求头、token 等
return config
}
// 响应拦截器
const responseInterceptor = (response) => {
// 统一处理响应数据
return response
}
// 错误处理
const handleError = (error) => {
console.error('API 请求错误:', error)
// 根据错误类型进行处理
if (error.errMsg && error.errMsg.includes('timeout')) {
uni.showToast({
title: '请求超时,请稍后重试',
icon: 'none',
duration: 2000
})
} else if (error.errMsg && error.errMsg.includes('fail')) {
uni.showToast({
title: '网络请求失败,请检查网络',
icon: 'none',
duration: 2000
})
}
return Promise.reject(error)
}
/**
* 通用请求方法
* @param {Object} options - 请求配置
* @returns {Promise} - 返回 Promise
*/
export const request = (options = {}) => {
return new Promise((resolve, reject) => {
// 合并配置
const config = {
url: '',
method: 'POST',
data: {},
header: {
'Content-Type': 'application/json'
},
timeout: 10000,
...options
}
// 处理完整 URL
if (!config.url.startsWith('http')) {
config.url = `${BASE_URL}${config.url}`
}
// 应用请求拦截器
const finalConfig = requestInterceptor(config)
// 执行请求
uni.request({
...finalConfig,
success: (response) => {
// 应用响应拦截器
const result = responseInterceptor(response)
// 根据 HTTP 状态码处理
if (response.statusCode >= 200 && response.statusCode < 300) {
resolve(result.data)
} else {
// 创建包含状态码和响应数据的错误对象
const error = new Error(`HTTP ${response.statusCode}: ${response.errMsg || '请求失败'}`)
error.statusCode = response.statusCode
error.response = response
error.data = result.data || response.data
reject(error)
}
},
fail: (error) => {
handleError(error).catch(reject)
}
})
})
}
/**
* GET 请求
* @param {string} url - 请求地址
* @param {Object} params - 请求参数
* @param {Object} options - 其他配置
* @returns {Promise}
*/
export const get = (url, params = {}, options = {}) => {
return request({
url,
method: 'GET',
data: params,
...options
})
}
/**
* POST 请求
* @param {string} url - 请求地址
* @param {Object} data - 请求数据
* @param {Object} options - 其他配置
* @returns {Promise}
*/
export const post = (url, data = {}, options = {}) => {
return request({
url,
method: 'POST',
data,
...options
})
}
/**
* PUT 请求
* @param {string} url - 请求地址
* @param {Object} data - 请求数据
* @param {Object} options - 其他配置
* @returns {Promise}
*/
export const put = (url, data = {}, options = {}) => {
return request({
url,
method: 'PUT',
data,
...options
})
}
/**
* DELETE 请求
* @param {string} url - 请求地址
* @param {Object} params - 请求参数
* @param {Object} options - 其他配置
* @returns {Promise}
*/
export const del = (url, params = {}, options = {}) => {
return request({
url,
method: 'DELETE',
data: params,
...options
})
}
export default {
request,
get,
post,
put,
del
}

22
api/user.js Normal file
View File

@ -0,0 +1,22 @@
/**
* 用户信息相关 API
*/
import { post } from './request.js'
/**
* 保存用户信息
* @param {Object} data - 用户数据
* @param {string} data.name - 用户姓名
* @param {string} data.phone - 用户手机号码
* @param {string} data.address - 用户地址
* @param {string} data.msg - 用户留言
* @param {string} data.page_visit_uuid - 页面访问UUID
* @returns {Promise}
*/
export const saveUserInfo = (data) => {
return post('/api/user-info', data)
}
export default {
saveUserInfo
}

27
api/visit.js Normal file
View File

@ -0,0 +1,27 @@
/**
* 页面访问相关 API
*/
import { post } from './request.js'
/**
* 记录页面访问
* @param {Object} data - 访问数据
* @param {string} data.page - 页面标识
* @param {string} data.source - 访问来源
* @param {Object} data.extra - 额外数据
* @returns {Promise}
*/
export const recordPageVisit = (data = {}) => {
const defaultData = {
page: 'index',
timestamp: Date.now(),
...data
}
return post('/api/page-visit', defaultData)
}
export default {
recordPageVisit
}

View File

@ -122,12 +122,25 @@ const props = defineProps({
longfusi: false,
dongzhimen: false
})
},
hasSubmitted: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['lottery', 'couplet', 'restart'])
const handleLottery = () => {
//
if (props.hasSubmitted) {
uni.showToast({
title: '您已经提交过信息了,请勿重复提交',
icon: 'none',
duration: 2000
})
return
}
emit('lottery')
}

View File

@ -109,10 +109,10 @@ onMounted(() => {
transform: translateX(-50%) rotate(0deg);
}
25% {
transform: translateX(-50%) rotate(-3deg);
transform: translateX(-50%) rotate(-6deg);
}
75% {
transform: translateX(-50%) rotate(3deg);
transform: translateX(-50%) rotate(6deg);
}
}

View File

@ -0,0 +1,305 @@
<script setup>
import { ref, watch, nextTick } from 'vue'
const props = defineProps({
visible: {
type: Boolean,
default: false
},
hasSubmitted: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['close', 'submit'])
//
const formData = ref({
name: '',
phone: '',
address: '',
msg: ''
})
// visible
watch(() => props.visible, (newVal) => {
if (newVal) {
formData.value = {
name: '',
phone: '',
address: '',
msg: ''
}
}
})
//
const handleClose = () => {
emit('close')
}
//
const showToast = (title) => {
nextTick(() => {
uni.showToast({
title,
icon: 'none',
duration: 2000
})
})
}
//
const handleSubmit = () => {
//
if (props.hasSubmitted) {
showToast('您已经提交过信息了,请勿重复提交')
return
}
console.log('提交表单', formData.value)
//
console.log('表单数据:', formData.value)
if (!formData.value.name || !formData.value.phone || !formData.value.address || !formData.value.msg) {
showToast('请填写完整信息')
return
}
//
const phoneRegex = /^1[3-9]\d{9}$/
const phone = formData.value.phone.trim()
console.log('手机号验证:', phone, '长度:', phone.length, '匹配结果:', phoneRegex.test(phone))
if (!phoneRegex.test(phone)) {
showToast('请输入正确的手机号')
return
}
//
if (!formData.value.msg || formData.value.msg.trim() === '') {
showToast('请输入留言')
return
}
//
const msgLength = formData.value.msg.length
if (msgLength > 200) {
showToast('留言最多200个汉字')
return
}
//
const submitData = {
name: formData.value.name.trim(),
phone: formData.value.phone.trim(),
address: formData.value.address.trim(),
msg: formData.value.msg.trim()
}
emit('submit', submitData)
}
</script>
<template>
<div class="info-page" v-if="visible">
<!-- 背景图片 -->
<div class="bg-container">
<img src="/static/info/info_bg.jpg" class="bg-image" mode="aspectFill" />
</div>
<!-- 返回按钮 -->
<div class="back-btn" @click="handleClose">
<img src="/static/images/btn_back.png" class="back-icon" />
</div>
<!-- 标题 -->
<div class="title-container">
<img src="/static/info/info_title.png" class="title-image" />
</div>
<!-- 信息提示 -->
<div class="tips-container">
<img src="/static/info/info_tips.png" class="tips-image" />
</div>
<!-- 表单区域 -->
<div class="form-container">
<div class="form-item">
<input
v-model="formData.name"
type="text"
placeholder="请输入您的姓名"
class="form-input"
maxlength="30"
required
/>
</div>
<div class="form-item">
<input
v-model="formData.phone"
type="tel"
placeholder="请输入您的联系电话"
class="form-input"
maxlength="11"
required
/>
</div>
<div class="form-item">
<input
v-model="formData.address"
type="text"
placeholder="请输入您的邮寄地址"
class="form-input"
maxlength="100"
required
/>
</div>
<div class="form-item">
<textarea
v-model="formData.msg"
placeholder="请输入您的留言"
class="form-textarea"
maxlength="200"
required
></textarea>
<span class="char-count">{{ formData.msg.length }}/200</span>
</div>
</div>
<!-- 提交按钮 -->
<div class="submit-container">
<img
src="/static/images/btn_submit.png"
class="submit-btn-image"
@click="handleSubmit"
/>
</div>
</div>
</template>
<style scoped>
.info-page {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 100;
overflow: hidden;
}
/* 背景图片 */
.bg-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
}
.bg-image {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 返回按钮 */
.back-btn {
position: absolute;
top: 30rpx;
left: 30rpx;
z-index: 10;
}
.back-icon {
width: 68rpx;
height: 68rpx;
}
/* 标题 */
.title-container {
display: flex;
justify-content: center;
padding-top: 100rpx;
margin-bottom: 20rpx;
}
.title-image {
width: 544rpx;
height: 213rpx;
}
/* 信息提示 */
.tips-container {
display: flex;
justify-content: center;
margin-bottom: 30rpx;
}
.tips-image {
width: 697rpx;
height: 258rpx;
}
/* 表单区域 */
.form-container {
padding: 0 50rpx;
}
.form-item {
margin-bottom: 16rpx;
position: relative;
}
.form-input,
.form-textarea {
width: 100%;
padding: 20rpx;
border: none;
border-radius: 10rpx;
font-size: 28rpx;
background-color: #fff;
box-sizing: border-box;
}
.form-input {
height: 70rpx;
padding: 16rpx 20rpx;
}
.form-textarea {
height: 150rpx;
padding: 16rpx 20rpx;
resize: none;
}
.char-count {
position: absolute;
right: 20rpx;
bottom: 20rpx;
color: #999;
font-size: 24rpx;
}
/* 提交按钮 */
.submit-container {
display: flex;
justify-content: center;
margin-top: 30rpx;
}
.submit-btn-image {
width: 306rpx;
height: 97rpx;
cursor: pointer;
}
.submit-btn-image:active {
transform: scale(0.98);
}
</style>

View File

@ -175,7 +175,7 @@ onUnmounted(() => {
<!-- 音乐控制按钮 -->
<div class="music-control" @click="toggleMusic">
<img
src="/static/images/icon_music.png"
:src="isMusicPlaying ? '/static/images/icon_music2.png' : '/static/images/icon_music1.png'"
alt="音乐控制"
class="music-icon"
:class="{ 'playing': isMusicPlaying }"

View File

@ -2,6 +2,8 @@
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue'
import { useSceneStore } from '../store/scene'
import { useCollectionStore } from '../store/collection'
import { recordPageVisit } from '../api/visit.js'
import { saveUserInfo } from '../api/user.js'
import LongImageViewer from './LongImageViewer.vue'
import MediaPlayer from './MediaPlayer.vue'
import QianmenScene from './QianmenScene.vue'
@ -10,6 +12,7 @@ import LongfusiScene from './LongfusiScene.vue'
import WangfujingScene from './WangfujingScene.vue'
import ChongwenScene from './ChongwenScene.vue'
import EndPage from './EndPage.vue'
import LotteryFormModal from './LotteryFormModal.vue'
const sceneStore = useSceneStore()
const collectionStore = useCollectionStore()
@ -40,12 +43,11 @@ const showCoupletDisplay = ref(false)
const coupletKeyword = ref('')
//
const generatedCouplet = ref(null)
//
const lotteryForm = ref({
name: '',
phone: '',
address: ''
})
// 访UUID
const pageVisitUuid = ref('')
//
const hasSubmittedUserInfo = ref(false)
//
const recommendedKeywords = ref(['吉祥', '如意', '平安', '健康', '幸福', '快乐', '富贵', '安康'])
@ -62,7 +64,6 @@ const scenes = ref([
id: 'dongzhimen',
name: '东直门商圈',
description: '京城东部的交通枢纽和商业中心',
image: 'https://picsum.photos/id/1019/750/1600',
interactionTip: '移动烤鸭,领取团圆福筷',
collectedItem: '团圆福筷'
},
@ -70,7 +71,6 @@ const scenes = ref([
id: 'longfusi',
name: '隆福寺商圈',
description: '传统文化与现代艺术融合的潮流地标',
image: 'https://picsum.photos/id/1018/750/1600',
interactionTip: '点击文创物品,点亮文化福灯',
collectedItem: '文化福灯'
},
@ -78,7 +78,6 @@ const scenes = ref([
id: 'wangfujing',
name: '王府井商圈',
description: '北京最繁华的商业中心之一',
image: 'https://picsum.photos/id/1018/750/1600',
interactionTip: '双指放大,收集金袋福卡',
collectedItem: '金袋福卡'
},
@ -86,7 +85,6 @@ const scenes = ref([
id: 'chongwen',
name: '崇文门商圈',
description: '融合传统文化与现代商业的活力区域',
image: 'https://picsum.photos/id/1016/750/1600',
interactionTip: '滑动探索商圈,收集国潮福字',
collectedItem: '国潮福字'
},
@ -94,8 +92,6 @@ const scenes = ref([
id: 'qianmen',
name: '前门商圈',
description: '北京最具历史文化底蕴的商圈之一',
image: 'https://picsum.photos/id/1015/750/1600',
thumbnail: 'https://picsum.photos/id/1015/200/100',
interactionTip: '点击京韵大鼓,收集非遗福印',
collectedItem: '非遗福印'
},
@ -146,6 +142,19 @@ const collectedSeals = computed(() => {
//
onMounted(() => {
// 访
recordPageVisit({
user_agent: navigator.userAgent,
page_name: 'home'
}).then(res => {
if (res && res.uuid) {
pageVisitUuid.value = res.uuid
console.log('页面访问UUID:', res.uuid)
}
}).catch(err => {
console.log('页面访问记录失败:', err)
})
//
scenes.value.forEach((scene, index) => {
if (index > 0 && index < scenes.value.length) {
@ -337,49 +346,80 @@ const openLotteryForm = () => {
showLotteryForm.value = true
}
//
const closeLotteryForm = () => {
showLotteryForm.value = false
}
//
const submitLotteryForm = () => {
//
if (!lotteryForm.value.name || !lotteryForm.value.phone || !lotteryForm.value.address) {
const submitLotteryForm = async (formData) => {
//
if (hasSubmittedUserInfo.value) {
uni.showToast({
title: '请填写完整信息',
title: '您已经提交过信息了,请勿重复提交',
icon: 'none',
duration: 2000
})
return
}
//
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(lotteryForm.value.phone)) {
// UUID
if (!pageVisitUuid.value) {
uni.showToast({
title: '请输入正确的手机号',
title: '系统错误,请刷新页面重试',
icon: 'none',
duration: 2000
})
return
}
//
uni.showLoading({
title: '提交中...',
mask: true
})
setTimeout(() => {
try {
//
await saveUserInfo({
name: formData.name,
phone: formData.phone,
address: formData.address,
msg: formData.msg,
page_visit_uuid: pageVisitUuid.value
})
//
hasSubmittedUserInfo.value = true
uni.hideLoading()
uni.showToast({
title: '报名成功!我们将在活动结束后通知您抽奖结果',
title: '提交成功!我们将在活动结束后通知您抽奖结果',
duration: 2000
})
showLotteryForm.value = false
//
lotteryForm.value = {
name: '',
phone: '',
address: ''
} catch (error) {
uni.hideLoading()
console.error('提交用户信息失败:', error)
//
let errorMessage = '提交失败,请稍后重试'
//
if (error.data && error.data.error) {
errorMessage = error.data.error
} else if (error.response && error.response.data && error.response.data.error) {
errorMessage = error.response.data.error
} else if (error.message) {
//
errorMessage = '提交失败,请稍后重试'
}
uni.showToast({
title: errorMessage,
icon: 'none',
duration: 2000
})
}
}, 1500)
}
// AI
@ -481,8 +521,10 @@ onUnmounted(() => {
scroll-y="true"
scroll-with-animation="true"
enhanced="true"
refresher-enabled="false"
:refresher-enabled="false"
:refresher-triggered="false"
show-scrollbar="false"
:bounce="false"
@scroll="handleScroll"
:scroll-into-view="homeSectionId"
>
@ -494,6 +536,7 @@ onUnmounted(() => {
:collected-count="collectedItems.length"
:total-count="scenes.length - 1"
:collected-seals="collectedSeals"
:has-submitted="hasSubmittedUserInfo"
@lottery="openLotteryForm"
@couplet="openAICoupletForm"
@restart="scrollToTop"
@ -573,33 +616,12 @@ onUnmounted(() => {
</div> -->
<!-- 抽奖留资弹窗 -->
<div class="form-modal" v-if="showLotteryForm">
<div class="modal-overlay" @click="showLotteryForm = false"></div>
<div class="modal-content">
<div class="modal-header">
<h3>参与抽奖</h3>
<button class="close-btn" @click="showLotteryForm = false">×</button>
</div>
<div class="modal-body">
<p class="modal-description">请填写以下信息参与线下抽奖活动</p>
<div class="form-group">
<label>姓名</label>
<input v-model="lotteryForm.name" type="text" placeholder="请输入您的姓名" class="form-input" />
</div>
<div class="form-group">
<label>联系电话</label>
<input v-model="lotteryForm.phone" type="tel" placeholder="请输入您的手机号" class="form-input" />
</div>
<div class="form-group">
<label>邮寄地址</label>
<textarea v-model="lotteryForm.address" placeholder="请输入您的邮寄地址" class="form-textarea" rows="3"></textarea>
</div>
<div class="form-actions">
<button class="submit-btn" @click="submitLotteryForm">提交报名</button>
</div>
</div>
</div>
</div>
<LotteryFormModal
:visible="showLotteryForm"
:has-submitted="hasSubmittedUserInfo"
@close="closeLotteryForm"
@submit="submitLotteryForm"
/>
<!-- AI春联生成弹窗 -->
<div class="form-modal" v-if="showAICoupletForm">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 KiB

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 612 KiB

After

Width:  |  Height:  |  Size: 535 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
static/info/info_bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
static/info/info_tips.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
static/info/info_title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB