diff --git a/api/API_DOCUMENTATION.md b/api/API_DOCUMENTATION.md new file mode 100644 index 0000000..c3327bd --- /dev/null +++ b/api/API_DOCUMENTATION.md @@ -0,0 +1,232 @@ +# H5海报项目 API 文档 + +本文档描述了H5海报项目的后端API接口,供前端开发使用。 + +## 基础信息 + +- **Base URL**: `http://localhost:8080` (开发环境) / 生产环境使用相对路径 +- **Content-Type**: `application/json` +- **超时时间**: 60秒 + +## 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 +} +``` + +**错误响应** (400 Bad Request) +```json +{ + "error": "Invalid request data" +} +``` + +**错误响应** (500 Internal Server Error) +```json +{ + "error": "Database not available" +} +``` + +#### 使用说明 +- 每次用户访问页面时调用此API +- 系统会自动记录用户的IP地址 +- 如果数据库不可用,API会返回500错误 +- 建议在页面加载完成后异步调用,不影响用户体验 + +--- + +### 2. 用户信息保存 - `/api/user-info` + +保存用户填写的个人信息,用于活动参与或后续联系。 + +#### 请求信息 +- **方法**: `POST` +- **路径**: `/api/user-info` + +#### 请求参数 + +| 参数名 | 类型 | 必需 | 描述 | +|--------|------|------|------| +| name | string | 是 | 用户姓名 | +| phone | string | 是 | 用户手机号码 | +| address | string | 是 | 用户地址 | +| msg | string | 是 | 用户留言或备注信息,最多200字 | + +#### 请求示例 +```json +{ + "name": "张三", + "phone": "13800138000", + "address": "北京市朝阳区xxx街道xxx号", + "msg": "希望能够获得精美礼品,谢谢!" +} +``` + +#### 响应信息 + +**成功响应** (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个字符,用于用户留言或备注 +- 系统会自动记录创建时间 +- 建议在前端进行基础的数据验证(如手机号格式) + +--- + +### 3. 对联海报生成 - `/api/couplets` + +根据用户输入的两个汉字,使用AI生成对联并创建海报。 + +#### 请求信息 +- **方法**: `POST` +- **路径**: `/api/couplets` + +#### 请求参数 + +| 参数名 | 类型 | 必需 | 描述 | +|--------|------|------|------| +| title | string | 是 | 两个汉字,用于生成对联(如"新春") | + +#### 请求示例 +```json +{ + "title": "新春" +} +``` + +#### 响应信息 + +**成功响应** (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在出错时都会返回相应的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) + } +} +``` \ No newline at end of file diff --git a/api/api.js b/api/api.js new file mode 100644 index 0000000..cd1127d --- /dev/null +++ b/api/api.js @@ -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) + } +} \ No newline at end of file diff --git a/api/request.js b/api/request.js new file mode 100644 index 0000000..6a216d9 --- /dev/null +++ b/api/request.js @@ -0,0 +1,161 @@ +/** + * 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 { + reject(new Error(`HTTP ${response.statusCode}: ${response.errMsg || '请求失败'}`)) + } + }, + 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 +} diff --git a/api/visit.js b/api/visit.js new file mode 100644 index 0000000..95cb783 --- /dev/null +++ b/api/visit.js @@ -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 +} diff --git a/components/FuClickArea.vue b/components/FuClickArea.vue index 26e0da2..7951a54 100644 --- a/components/FuClickArea.vue +++ b/components/FuClickArea.vue @@ -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); } } diff --git a/components/SinglePageContainer.vue b/components/SinglePageContainer.vue index 051d81a..1e47861 100644 --- a/components/SinglePageContainer.vue +++ b/components/SinglePageContainer.vue @@ -2,6 +2,7 @@ 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 LongImageViewer from './LongImageViewer.vue' import MediaPlayer from './MediaPlayer.vue' import QianmenScene from './QianmenScene.vue' @@ -146,6 +147,12 @@ const collectedSeals = computed(() => { // 组件挂载后初始化 onMounted(() => { + // 记录页面访问 + recordPageVisit({ + user_agent: navigator.userAgent, + page_name: 'home' + }) + // 检查并初始化场景交互状态 scenes.value.forEach((scene, index) => { if (index > 0 && index < scenes.value.length) { diff --git a/static/bg/bg2.jpg b/static/bg/bg2.jpg index eda16a2..d9a4e73 100644 Binary files a/static/bg/bg2.jpg and b/static/bg/bg2.jpg differ diff --git a/static/bg/bg4.jpg b/static/bg/bg4.jpg index 239bfd4..2b1309b 100644 Binary files a/static/bg/bg4.jpg and b/static/bg/bg4.jpg differ diff --git a/static/bg/bg5.jpg b/static/bg/bg5.jpg index e4f8334..827f951 100644 Binary files a/static/bg/bg5.jpg and b/static/bg/bg5.jpg differ