优化和功能完善
@@ -0,0 +1,321 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
---
|
||||
|
||||
## 项目概述
|
||||
|
||||
情绪博物馆 (Emotion Museum) 是一款基于 AI 技术的心理健康应用,通过智能对话、情绪分析、个性化成长方案等功能,帮助用户建立健康的情绪管理习惯。
|
||||
|
||||
**生产环境地址**: `101.200.208.45`
|
||||
- 用户前端:http://101.200.208.45/emotion-museum/
|
||||
- 管理后台:http://101.200.208.45/emotion-museum-admin/
|
||||
- 后端 API:http://101.200.208.45:19089/api
|
||||
- WebSocket:ws://101.200.208.45:19089/ws
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
.
|
||||
├── backend-single/ # Spring Boot 单体后端服务
|
||||
├── web/ # 用户前端 (Vue3 + TS + Vite)
|
||||
├── web-admin/ # 管理后台 (Vue3 + TS + Element Plus)
|
||||
├── UniApp/ # 跨平台移动应用 (微信小程序/H5)
|
||||
├── mini-program/ # 小程序项目
|
||||
├── course-web/ # 课程 Web 项目
|
||||
├── life-script/ # 生活脚本工具
|
||||
└── tools/ # 工具脚本
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常用命令
|
||||
|
||||
### 后端 (backend-single)
|
||||
|
||||
```bash
|
||||
# 进入后端目录
|
||||
cd backend-single
|
||||
|
||||
# 编译打包
|
||||
mvn clean package -DskipTests
|
||||
|
||||
# 本地运行
|
||||
mvn spring-boot:run
|
||||
|
||||
# 或直接运行 JAR
|
||||
java -jar target/backend-single-1.0.0.jar
|
||||
|
||||
# 运行测试
|
||||
mvn test
|
||||
|
||||
# 运行单个测试类
|
||||
mvn test -Dtest=ClassNameTest
|
||||
```
|
||||
|
||||
### 用户前端 (web)
|
||||
|
||||
```bash
|
||||
# 进入前端目录
|
||||
cd web
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器 (端口 5173)
|
||||
npm run dev
|
||||
|
||||
# 类型检查
|
||||
npm run type-check
|
||||
|
||||
# 代码检查
|
||||
npm run lint
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
|
||||
# 运行测试
|
||||
npm run test
|
||||
|
||||
# E2E 测试
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### 管理后台 (web-admin)
|
||||
|
||||
```bash
|
||||
# 进入目录
|
||||
cd web-admin
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器 (端口 5174)
|
||||
npm run dev
|
||||
|
||||
# 类型检查
|
||||
npm run type-check
|
||||
|
||||
# 代码检查
|
||||
npm run lint
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
```
|
||||
|
||||
### UniApp/小程序
|
||||
|
||||
```bash
|
||||
# 进入目录
|
||||
cd UniApp
|
||||
|
||||
# 开发微信小程序
|
||||
npm run dev:mp-weixin
|
||||
|
||||
# 构建微信小程序
|
||||
npm run build:mp-weixin
|
||||
|
||||
# 开发 H5
|
||||
npm run dev:h5
|
||||
|
||||
# 构建 H5
|
||||
npm run build:h5
|
||||
```
|
||||
|
||||
### 一键部署
|
||||
|
||||
```bash
|
||||
# 部署所有服务(后端 + 前端 + 管理后台)到生产服务器
|
||||
bash deploy-all.sh
|
||||
|
||||
# 仅部署后端
|
||||
bash deploy-all.sh backend
|
||||
|
||||
# 仅部署前端
|
||||
bash deploy-all.sh frontend
|
||||
|
||||
# 仅部署管理后台
|
||||
bash deploy-all.sh admin
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 后端
|
||||
- **框架**: Spring Boot 2.7.18
|
||||
- **ORM**: MyBatis-Plus 3.5.3.1
|
||||
- **数据库**: MySQL 8.0.33
|
||||
- **缓存**: Redis 7.0+
|
||||
- **JWT**: io.jsonwebtoken 0.11.5
|
||||
- **构建**: Maven 3.6+
|
||||
- **JDK**: 17
|
||||
|
||||
### 前端
|
||||
- **框架**: Vue 3.4.x + TypeScript 5.x
|
||||
- **构建**: Vite 5.x
|
||||
- **UI**: Element Plus 2.4.x
|
||||
- **样式**: Tailwind CSS 3.4.x
|
||||
- **状态管理**: Pinia 2.1.x
|
||||
- **路由**: Vue Router 4.2.x
|
||||
- **HTTP**: Axios 1.6.x
|
||||
- **实时通信**: @stomp/stompjs + SockJS
|
||||
- **图表**: ECharts 5.4.x
|
||||
- **包管理**: npm 9+
|
||||
|
||||
### 移动端
|
||||
- **框架**: UniApp (Vue 3)
|
||||
- **平台**: 微信小程序、H5
|
||||
|
||||
---
|
||||
|
||||
## 架构规范
|
||||
|
||||
### 后端分层架构
|
||||
- **Controller 层**: 接口定义,参数校验,禁止业务逻辑
|
||||
- **Service 层**: 业务逻辑实现
|
||||
- **Mapper 层**: 数据访问层
|
||||
- **Entity 层**: 数据实体
|
||||
- **Request/Response 层**: 入参出参封装
|
||||
- **Config 层**: 配置类
|
||||
|
||||
### 接口设计规范
|
||||
- Controller 层路由禁止添加 `/api` 前缀
|
||||
- 使用 `@RequestParam` 传递路径参数,禁止使用 `@PathVariable`
|
||||
- 接口方法参数不超过两个,超出时使用 Request/DTO 对象封装
|
||||
- 使用项目已有的 `Result` 作为统一接口返回
|
||||
- 禁止使用枚举类型作为接口入参
|
||||
|
||||
### 前端架构
|
||||
- 使用 Vue 3 Composition API (`<script setup>`)
|
||||
- 使用 TypeScript 进行类型检查
|
||||
- 组件通信优先使用 Pinia 状态管理
|
||||
- 前端访问后端通过网关统一调用
|
||||
|
||||
---
|
||||
|
||||
## 验证规则(强制)
|
||||
|
||||
**修改任何前后端代码后,必须使用浏览器进行验证**
|
||||
|
||||
### 验证步骤
|
||||
|
||||
1. **使用浏览器工具打开对应页面**
|
||||
- 使用 `agent-browser` skill 或 MCP 工具
|
||||
- 前台页面:通常是 `http://localhost:5173` 或项目指定端口
|
||||
- 管理端页面:通常是 `http://localhost:5174` 或项目指定路径
|
||||
|
||||
2. **验证标准**(全部满足才算通过)
|
||||
- ✅ 前端修改已生效
|
||||
- ✅ 页面没有报错
|
||||
- ✅ 可以正常访问
|
||||
- ✅ 浏览器 Console 中没有任何错误
|
||||
|
||||
3. **只有验证通过才算任务完成**
|
||||
|
||||
---
|
||||
|
||||
## 热加载规则(强制)
|
||||
|
||||
**修改前后端代码后,优先利用热加载,避免不必要的重启**
|
||||
|
||||
### 判断逻辑
|
||||
|
||||
1. **不需要重启的场景**(热加载自动生效)
|
||||
- 前端:修改 `.vue`、`.tsx`、`.ts`、`.js`、`.css`、`.scss` 等源码文件
|
||||
- 后端:修改业务逻辑、API 接口、组件代码等支持热更新的文件
|
||||
- 样式、模板、静态资源等修改
|
||||
|
||||
2. **需要重启的场景**(热加载无法生效)
|
||||
- 前端:修改 `vite.config.ts`、`tsconfig.json`、`.env`、`package.json`、别名配置等
|
||||
- 后端:修改启动配置、数据库连接、中间件注册、环境变量等
|
||||
- 新增或删除依赖包后
|
||||
|
||||
### 操作原则
|
||||
|
||||
- 修改后先观察终端输出,确认热加载是否成功
|
||||
- 只有修改了必须重启才能生效的配置时,才执行重启操作
|
||||
- 重启前后端服务前,需告知用户
|
||||
|
||||
---
|
||||
|
||||
## 语言规则(强制)
|
||||
|
||||
- 所有对话和文档使用**中文**
|
||||
- 代码内容(变量名、函数名、注释)使用英文
|
||||
- 技术术语保持英文(API、HTTP、JSON 等)
|
||||
|
||||
---
|
||||
|
||||
## Git 提交规范
|
||||
|
||||
- 使用中文提交
|
||||
- 格式:`类型:描述`
|
||||
- 类型包括:feat, fix, docs, style, refactor, test, chore
|
||||
|
||||
---
|
||||
|
||||
## 开发规范(来自 Cursor Rules)
|
||||
|
||||
### 基础设置
|
||||
- 保持对话语言为中文
|
||||
- 不允许在未经允许的情况下删除代码和文件
|
||||
- 不允许破坏正常的业务代码
|
||||
- 执行终端命令时要关注执行情况,避免无效等待
|
||||
|
||||
### 代码规范
|
||||
- 生成代码时必须添加类级和函数级注释
|
||||
- 使用 `import` 导包,禁止使用全限定名称引用类
|
||||
- 禁止使用枚举类型作为 Entity、Request、Response、DTO 对象的字段
|
||||
- 禁止以枚举类作为方法的入参
|
||||
- 新增数据的 id 使用已存在的雪花算法生成器生成
|
||||
|
||||
### 架构规范
|
||||
- Controller 层禁止添加业务逻辑
|
||||
- 使用全局异常处理,禁止使用 try-catch
|
||||
- 前端接口访问尽可能走网关调用
|
||||
|
||||
### 接口设计规范
|
||||
- Controller 层接口定义:
|
||||
- 入参使用 request 封装传递到 service 层
|
||||
- service 层方法命名与 controller 层保持一致
|
||||
- 出参使用 response 封装由 service 层传递到 controller 层
|
||||
- 禁止在 controller 层做 entity 与 request/response 的转换
|
||||
- 使用项目已有的 `Result` 做接口返回
|
||||
- 接口和方法参数不允许超过两个,超过时使用 request 或 DTO 对象封装
|
||||
- Controller 层路由禁止添加 `/api` 前缀
|
||||
- Controller 层 `@RequestMapping` 的 value 属性值不允许重复且不允许为空
|
||||
- 必须明确指定 value 属性值且使用驼峰结构命名,避免使用下划线
|
||||
- 禁止使用 `@GetMapping()` 或 `@PostMapping` 等空注解形式
|
||||
- 用户相关接口禁止直接传递用户 id,需要后端根据 token 获取当前登录用户信息
|
||||
- 禁止使用 `/{param}` 格式的路径参数,避免网关路由冲突
|
||||
- 路径参数统一使用 `@RequestParam` 而非 `@PathVariable`
|
||||
- 接口路径命名应具有明确的语义,避免使用通用词汇如 `/get`、`/list` 等
|
||||
- 批量操作接口应使用专门的 Request 对象封装参数,而非直接传递 List
|
||||
- 接口路径应避免层级过深,建议不超过 3 级路径结构
|
||||
|
||||
### 数据库规范
|
||||
- 所有数据表必须包含 `create_time` 和 `update_time` 字段
|
||||
- 删除操作优先使用逻辑删除,添加 `deleted` 字段标识
|
||||
- 数据库字段命名使用下划线分隔,Java 实体类使用驼峰命名
|
||||
- 优先使用 `LambdaQueryWrapper` 构造条件查询
|
||||
- 使用 Lambda 表达式引用实体类属性,提高代码可维护性
|
||||
- 复杂查询条件应使用 `LambdaQueryWrapper` 的链式调用
|
||||
|
||||
### 安全规范
|
||||
- 所有外部输入必须进行参数校验
|
||||
- 敏感信息不得在日志中输出
|
||||
- 数据库操作必须使用参数化查询,防止 SQL 注入
|
||||
|
||||
### 性能规范
|
||||
- 避免 N+1 查询问题,合理使用批量查询
|
||||
- 大数据量查询必须分页处理
|
||||
- 缓存策略要考虑数据一致性问题
|
||||
|
||||
### 日志规范
|
||||
- 关键业务操作必须记录操作日志
|
||||
- 异常信息要包含足够的上下文信息
|
||||
- 生产环境禁止输出 debug 级别日志
|
||||
@@ -1,4 +0,0 @@
|
||||
VITE_APP_ENV=dev
|
||||
VITE_API_BASE_URL=http://localhost:19089
|
||||
VITE_WS_URL=ws://localhost:19089
|
||||
VITE_DEBUG=true
|
||||
@@ -1,4 +0,0 @@
|
||||
VITE_APP_ENV=prod
|
||||
VITE_API_BASE_URL=http://101.200.208.45:19089/api
|
||||
VITE_WS_URL=ws://101.200.208.45:19089
|
||||
VITE_DEBUG=false
|
||||
@@ -1,25 +0,0 @@
|
||||
从 Figma 频道 bdbg59by 拉取画板「登录界面」:
|
||||
1) 导出该画板勾选导出的所有资源到 F:\HBuilderXProjects\EmotionMuseum\static\auth\ 保持原文件名。
|
||||
2) 读取画板布局与样式,按 750 宽→rpx 1:1 还原,生成并覆盖:
|
||||
F:\HBuilderXProjects\EmotionMuseum\pages\login\index.uvue
|
||||
3) 页面中的图片引用使用 /static/auth/<文件名>,字体/字号/间距/圆角/阴影完全按 Figma。
|
||||
|
||||
从 Figma 频道 bdbg59by 拉取画板「初始页」:
|
||||
1) 导出所有资源到 F:\HBuilderXProjects\EmotionMuseum\static\home\ 保持原文件名。
|
||||
2) 读取布局与样式,750px→rpx 等比,生成并覆盖:
|
||||
F:\HBuilderXProjects\EmotionMuseum\pages\home\index.uvue
|
||||
3) 图片使用 /static/home/<文件名>;如果含自定义顶部栏,则页面内实现并与系统导航对齐。
|
||||
|
||||
|
||||
从 Figma 频道 bdbg59by 拉取画板「首页-聊天」:
|
||||
1) 导出所有资源到 F:\HBuilderXProjects\EmotionMuseum\static\splash\ 保持原文件名。
|
||||
2) 读取布局与样式,750px→rpx,生成并覆盖:
|
||||
F:\HBuilderXProjects\EmotionMuseum\pages\splash\index.uvue
|
||||
3) 图片使用 /static/splash/<文件名>;若有沉浸式头部,页面内自定义导航(可在需要时把 navigationStyle 设置为 custom)。
|
||||
|
||||
|
||||
从 Figma 频道 bdbg59by 拉取画板「首页」:
|
||||
1) 导出所有资源到 F:\HBuilderXProjects\EmotionMuseum\static\index\ 保持原文件名。
|
||||
2) 读取布局与样式,750px→rpx,生成并覆盖:
|
||||
F:\HBuilderXProjects\EmotionMuseum\pages\index\index.uvue
|
||||
3) 图片使用 /static/index/<文件名>;布局与组件结构严格与 Figma 一致。
|
||||
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||
CSS.supports('top: constant(a)'))
|
||||
document.write(
|
||||
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||
</script>
|
||||
<title></title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "emotion-museum-uniapp",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:mp-weixin": "NODE_ENV=development VITE_CJS_IGNORE_WARNING=1 VITE_USER_NODE_ENV= UNI_PLATFORM=mp-weixin UNI_INPUT_DIR=src UNI_OUTPUT_DIR=unpackage/dist/dev/mp-weixin UNI_APP_X=false uni -p mp-weixin",
|
||||
"build:mp-weixin": "NODE_ENV=production VITE_CJS_IGNORE_WARNING=1 VITE_USER_NODE_ENV= UNI_PLATFORM=mp-weixin UNI_INPUT_DIR=src UNI_OUTPUT_DIR=unpackage/dist/build/mp-weixin UNI_APP_X=false uni build -p mp-weixin --outDir unpackage/dist/build/mp-weixin",
|
||||
"dev:h5": "NODE_ENV=development VITE_CJS_IGNORE_WARNING=1 VITE_USER_NODE_ENV= UNI_PLATFORM=h5 UNI_INPUT_DIR=src UNI_APP_X=false uni -p h5",
|
||||
"build:h5": "NODE_ENV=production VITE_CJS_IGNORE_WARNING=1 VITE_USER_NODE_ENV= UNI_PLATFORM=h5 UNI_INPUT_DIR=src UNI_APP_X=false uni build -p h5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "3.0.0-alpha-5000120260211001",
|
||||
"@dcloudio/uni-mp-weixin": "3.0.0-alpha-5000120260211001",
|
||||
"vue": "3.4.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-alpha-5000120260211001",
|
||||
"@vitejs/plugin-vue": "^5.2.0",
|
||||
"@vue/compiler-sfc": "3.4.21",
|
||||
"@vue/shared": "3.4.21",
|
||||
"@vue/server-renderer": "3.4.21",
|
||||
"vite": "5.2.8"
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"setting": {
|
||||
"es6": true,
|
||||
"postcss": true,
|
||||
"minified": true,
|
||||
"uglifyFileName": false,
|
||||
"enhance": true,
|
||||
"packNpmRelationList": [],
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
},
|
||||
"useCompilerPlugins": false,
|
||||
"minifyWXML": true,
|
||||
"compileWorklet": false,
|
||||
"uploadWithSourceMap": true,
|
||||
"packNpmManually": false,
|
||||
"minifyWXSS": true,
|
||||
"localPlugins": false,
|
||||
"disableUseStrict": false,
|
||||
"condition": false,
|
||||
"swc": false,
|
||||
"disableSWC": true
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"simulatorPluginLibVersion": {},
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"appid": "wxaf2eaba72d28f0e4",
|
||||
"editorSetting": {},
|
||||
"libVersion": "3.14.2"
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"libVersion": "3.14.2",
|
||||
"projectname": "UniApp",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"coverView": true,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"skylineRenderEnable": false,
|
||||
"preloadBackgroundData": false,
|
||||
"autoAudits": false,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"compileHotReLoad": true,
|
||||
"useApiHook": true,
|
||||
"useStaticServer": false,
|
||||
"useLanDebug": false,
|
||||
"showES6CompileOption": false,
|
||||
"checkInvalidKey": true,
|
||||
"ignoreDevUnusedFiles": true,
|
||||
"bigPackageSizeSupport": false
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
|
||||
import { useAppStore } from './stores/app.js'
|
||||
|
||||
const statusBarHeight = ref(0)
|
||||
const safeAreaTop = ref(0)
|
||||
const safeAreaBottom = ref(0)
|
||||
|
||||
onLaunch(async () => {
|
||||
console.log('App Launch')
|
||||
const store = useAppStore()
|
||||
await store.initialize()
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = systemInfo.statusBarHeight || 20
|
||||
safeAreaTop.value = systemInfo.safeAreaInsets?.top || systemInfo.statusBarHeight || 20
|
||||
safeAreaBottom.value = systemInfo.safeAreaInsets?.bottom || 0
|
||||
uni.setStorageSync('statusBarHeight', statusBarHeight.value)
|
||||
uni.setStorageSync('safeAreaTop', safeAreaTop.value)
|
||||
uni.setStorageSync('safeAreaBottom', safeAreaBottom.value)
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
console.log('App Show')
|
||||
})
|
||||
|
||||
onHide(() => {
|
||||
console.log('App Hide')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
page {
|
||||
background-color: #0F071A;
|
||||
color: #F3E8FF;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.status-bar-space {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.safe-area-bottom {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.safe-area-top {
|
||||
padding-top: constant(safe-area-inset-top);
|
||||
padding-top: env(safe-area-inset-top);
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: rgba(168, 85, 247, 0.05);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(168, 85, 247, 0.15);
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.glass-card-gold {
|
||||
background: linear-gradient(135deg, rgba(168, 85, 247, 0.15), rgba(232, 121, 249, 0.1));
|
||||
border: 1px solid rgba(168, 85, 247, 0.3);
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.glass-input {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
color: #F3E8FF;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.glass-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #9333EA 0%, #7C3AED 100%);
|
||||
border-radius: 32rpx;
|
||||
padding: 28rpx 48rpx;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 30rpx;
|
||||
border: none;
|
||||
box-shadow: 0 8rpx 32rpx rgba(168, 85, 247, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: scale(0.98);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx 40rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.bottom-nav {
|
||||
background: rgba(15, 7, 26, 0.85);
|
||||
backdrop-filter: blur(40rpx);
|
||||
-webkit-backdrop-filter: blur(40rpx);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: #C084FC;
|
||||
transform: translateY(-8rpx);
|
||||
}
|
||||
|
||||
.step-indicator {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.step-dot {
|
||||
width: 24rpx;
|
||||
height: 8rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4rpx;
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
|
||||
.step-dot.active {
|
||||
width: 40rpx;
|
||||
background: #A855F7;
|
||||
box-shadow: 0 0 24rpx rgba(168, 85, 247, 0.6);
|
||||
}
|
||||
|
||||
.hint-chip {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 32rpx;
|
||||
padding: 12rpx 24rpx;
|
||||
font-size: 22rpx;
|
||||
color: rgba(243, 232, 255, 0.8);
|
||||
}
|
||||
|
||||
.hint-chip:active {
|
||||
background: rgba(168, 85, 247, 0.2);
|
||||
border-color: rgba(168, 85, 247, 0.4);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 4rpx;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(168, 85, 247, 0.3);
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.3; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.animate-pulse-slow {
|
||||
animation: pulse 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.text-primary { color: #A855F7; }
|
||||
.text-primary-light { color: #C084FC; }
|
||||
.text-accent { color: #E879F9; }
|
||||
.text-muted { color: rgba(255, 255, 255, 0.4); }
|
||||
.font-serif { font-family: 'Cinzel', serif; }
|
||||
|
||||
.bg-dark { background-color: #0F071A; }
|
||||
.bg-gradient-purple {
|
||||
background: linear-gradient(180deg, #1A0B2E 0%, #0F071A 50%, #050208 100%);
|
||||
}
|
||||
|
||||
.safe-area-bottom {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.safe-area-top {
|
||||
padding-top: constant(safe-area-inset-top);
|
||||
padding-top: env(safe-area-inset-top);
|
||||
}
|
||||
</style>
|
||||
@@ -1,84 +0,0 @@
|
||||
const ENV_TYPE = {
|
||||
DEV: 'dev',
|
||||
TEST: 'test',
|
||||
PROD: 'prod'
|
||||
}
|
||||
|
||||
const normalizeEnv = (value) => {
|
||||
if (!value) return ENV_TYPE.DEV
|
||||
if (value === 'development' || value === 'dev') return ENV_TYPE.DEV
|
||||
if (value === 'production' || value === 'prod') return ENV_TYPE.PROD
|
||||
if (value === 'test') return ENV_TYPE.TEST
|
||||
return ENV_TYPE.DEV
|
||||
}
|
||||
|
||||
const currentEnv = normalizeEnv(import.meta.env.VITE_APP_ENV || import.meta.env.MODE)
|
||||
|
||||
const envConfig = {
|
||||
[ENV_TYPE.DEV]: {
|
||||
API_BASE_URL: 'http://localhost:19089',
|
||||
WS_URL: 'ws://localhost:19089',
|
||||
DEBUG: true
|
||||
},
|
||||
[ENV_TYPE.TEST]: {
|
||||
API_BASE_URL: 'http://101.200.208.45:19089/api',
|
||||
WS_URL: 'ws://101.200.208.45:19089',
|
||||
DEBUG: true
|
||||
},
|
||||
[ENV_TYPE.PROD]: {
|
||||
API_BASE_URL: 'http://101.200.208.45:19089/api',
|
||||
WS_URL: 'ws://101.200.208.45:19089',
|
||||
DEBUG: false
|
||||
}
|
||||
}
|
||||
|
||||
const getConfig = () => {
|
||||
const base = envConfig[currentEnv] || envConfig[ENV_TYPE.DEV]
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || base.API_BASE_URL
|
||||
const wsUrl = import.meta.env.VITE_WS_URL || base.WS_URL
|
||||
const debug = import.meta.env.VITE_DEBUG !== undefined
|
||||
? import.meta.env.VITE_DEBUG === 'true'
|
||||
: base.DEBUG
|
||||
return {
|
||||
API_BASE_URL: apiBaseUrl,
|
||||
WS_URL: wsUrl,
|
||||
DEBUG: debug
|
||||
}
|
||||
}
|
||||
|
||||
const getEnvValue = (key) => {
|
||||
const config = getConfig()
|
||||
return config[key]
|
||||
}
|
||||
|
||||
const isDev = () => {
|
||||
return currentEnv === ENV_TYPE.DEV
|
||||
}
|
||||
|
||||
const isTest = () => {
|
||||
return currentEnv === ENV_TYPE.TEST
|
||||
}
|
||||
|
||||
const isProd = () => {
|
||||
return currentEnv === ENV_TYPE.PROD
|
||||
}
|
||||
|
||||
export {
|
||||
ENV_TYPE,
|
||||
currentEnv,
|
||||
getConfig,
|
||||
getEnvValue,
|
||||
isDev,
|
||||
isTest,
|
||||
isProd
|
||||
}
|
||||
|
||||
export default {
|
||||
ENV_TYPE,
|
||||
currentEnv,
|
||||
getConfig,
|
||||
getEnvValue,
|
||||
isDev,
|
||||
isTest,
|
||||
isProd
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import App from './App.vue'
|
||||
import { createSSRApp } from 'vue'
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
return { app }
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"name" : "EmotionMuseum",
|
||||
"appid" : "",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"uni-app-x" : {},
|
||||
"quickapp" : {},
|
||||
"mp-weixin" : {
|
||||
"appid" : "wxaf2eaba72d28f0e4",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-baidu" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-toutiao" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"uniStatistics" : {
|
||||
"enable" : false
|
||||
},
|
||||
"vueVersion" : "3",
|
||||
"app" : {
|
||||
"distribute" : {
|
||||
"icons" : {
|
||||
"android" : {
|
||||
"hdpi" : "",
|
||||
"xhdpi" : "",
|
||||
"xxhdpi" : "",
|
||||
"xxxhdpi" : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/splash/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/onboarding/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/main/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "个人中心"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarTitleText": "人生OS",
|
||||
"navigationBarBackgroundColor": "#0F071A",
|
||||
"backgroundColor": "#0F071A"
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
}
|
||||
@@ -1,343 +0,0 @@
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
|
||||
<view class="bg-decoration">
|
||||
<view class="aurora-top"></view>
|
||||
<view class="aurora-bottom"></view>
|
||||
</view>
|
||||
|
||||
<view class="content" :style="{ paddingTop: safeAreaTop + 20 + 'px' }">
|
||||
<view class="login-card">
|
||||
<view class="header">
|
||||
<text class="title font-serif">欢迎回来</text>
|
||||
<text class="subtitle">开启你的数字生命档案</text>
|
||||
</view>
|
||||
|
||||
<view class="form">
|
||||
<view class="input-group">
|
||||
<text class="input-label">手机号码</text>
|
||||
<input
|
||||
class="glass-input"
|
||||
type="number"
|
||||
maxlength="11"
|
||||
placeholder="输入手机号"
|
||||
v-model="phone"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="input-group">
|
||||
<text class="input-label">验证码</text>
|
||||
<view class="code-row">
|
||||
<input
|
||||
class="glass-input code-input"
|
||||
type="number"
|
||||
maxlength="6"
|
||||
placeholder="请输入验证码"
|
||||
v-model="code"
|
||||
/>
|
||||
<view
|
||||
class="code-btn"
|
||||
:class="{ disabled: isCountingDown || phone.length !== 11 }"
|
||||
@click="handleGetCode"
|
||||
>
|
||||
<text v-if="isCountingDown" class="countdown">{{ countdown }}s</text>
|
||||
<text v-else>获取验证码</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="btn-primary login-btn"
|
||||
:class="{ disabled: !canSubmit || loading }"
|
||||
@click="handleLogin"
|
||||
>
|
||||
<text v-if="loading">登录中...</text>
|
||||
<text v-else>开启旅程</text>
|
||||
</view>
|
||||
|
||||
<text class="agreement">
|
||||
登录即代表同意《用户协议》与《隐私政策》
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
import { getSmsCode } from '../../services/auth.js'
|
||||
|
||||
const store = useAppStore()
|
||||
|
||||
const statusBarHeight = ref(uni.getStorageSync('statusBarHeight') || 20)
|
||||
const safeAreaTop = ref(uni.getStorageSync('safeAreaTop') || 20)
|
||||
const safeAreaBottom = ref(uni.getStorageSync('safeAreaBottom') || 0)
|
||||
|
||||
onMounted(() => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = systemInfo.statusBarHeight || 20
|
||||
safeAreaTop.value = systemInfo.safeAreaInsets?.top || systemInfo.statusBarHeight || 20
|
||||
safeAreaBottom.value = systemInfo.safeAreaInsets?.bottom || 0
|
||||
})
|
||||
|
||||
const phone = ref('')
|
||||
const code = ref('')
|
||||
const loading = ref(false)
|
||||
const countdown = ref(60)
|
||||
const isCountingDown = ref(false)
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
return phone.value.length === 11 && code.value.length === 6
|
||||
})
|
||||
|
||||
const handleGetCode = async () => {
|
||||
if (isCountingDown.value || loading.value) return
|
||||
|
||||
if (phone.value.length !== 11) {
|
||||
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await getSmsCode(phone.value)
|
||||
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
||||
startCountdown()
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '验证码已发送 (模拟: 888888)', icon: 'none' })
|
||||
startCountdown()
|
||||
}
|
||||
}
|
||||
|
||||
const startCountdown = () => {
|
||||
isCountingDown.value = true
|
||||
countdown.value = 60
|
||||
|
||||
const timer = setInterval(() => {
|
||||
countdown.value--
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer)
|
||||
isCountingDown.value = false
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!canSubmit.value || loading.value) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const result = await store.login(phone.value, code.value)
|
||||
|
||||
if (result.success) {
|
||||
if (result.hasProfile) {
|
||||
uni.redirectTo({ url: '/pages/main/index' })
|
||||
} else {
|
||||
uni.redirectTo({ url: '/pages/onboarding/index' })
|
||||
}
|
||||
} else {
|
||||
uni.showToast({ title: result.error || '登录失败', icon: 'none' })
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '登录失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #0F071A 0%, #1A0B2E 50%, #0F071A 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
height: constant(safe-area-inset-top);
|
||||
height: env(safe-area-inset-top);
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.aurora-top {
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
width: 120%;
|
||||
height: 60%;
|
||||
background: rgba(168, 85, 247, 0.08);
|
||||
filter: blur(120rpx);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.aurora-bottom {
|
||||
position: absolute;
|
||||
bottom: -10%;
|
||||
right: -10%;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
background: rgba(139, 92, 246, 0.05);
|
||||
filter: blur(100rpx);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
padding-top: calc(40rpx + constant(safe-area-inset-top));
|
||||
padding-top: calc(40rpx + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 720rpx;
|
||||
background: rgba(168, 85, 247, 0.05);
|
||||
backdrop-filter: blur(40rpx);
|
||||
border: 1px solid rgba(168, 85, 247, 0.15);
|
||||
border-radius: 48rpx;
|
||||
padding: 64rpx 40rpx;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 64rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 48rpx;
|
||||
font-weight: 300;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
letter-spacing: 6rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
display: block;
|
||||
font-size: 18rpx;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
margin-bottom: 16rpx;
|
||||
letter-spacing: 4rpx;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.glass-input {
|
||||
width: 100%;
|
||||
height: 92rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24rpx;
|
||||
padding: 0 32rpx;
|
||||
color: #F3E8FF;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.glass-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.code-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16rpx;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.code-input {
|
||||
flex: 1;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.code-btn {
|
||||
width: 220rpx;
|
||||
height: 92rpx;
|
||||
background: linear-gradient(135deg, rgba(147, 51, 234, 0.5), rgba(124, 58, 237, 0.4));
|
||||
border: 1px solid rgba(168, 85, 247, 0.4);
|
||||
border-radius: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-size: 22rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
letter-spacing: 2rpx;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.code-btn:active {
|
||||
transform: scale(0.97);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.code-btn.disabled {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.countdown {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
height: 100rpx;
|
||||
background: linear-gradient(135deg, #9333EA 0%, #7C3AED 100%);
|
||||
border-radius: 32rpx;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
box-shadow: 0 8rpx 32rpx rgba(168, 85, 247, 0.3);
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.btn-primary.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.agreement {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -1,258 +0,0 @@
|
||||
<template>
|
||||
<view class="path-view">
|
||||
<text class="page-title">实现路径</text>
|
||||
|
||||
<view v-if="!selectedScript" class="empty-state glass-card">
|
||||
<text class="empty-icon">🗺️</text>
|
||||
<text class="empty-text">先生成剧本,方能洞察路径。</text>
|
||||
<button class="btn-secondary empty-btn" @click="goToScript">去生成剧本</button>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!currentPath" class="empty-state glass-card">
|
||||
<text class="empty-icon">🎯</text>
|
||||
<text class="empty-text">等待开启人生导航...</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="path-content">
|
||||
<view class="target-card glass-card-gold">
|
||||
<text class="target-label">目标:{{ currentPath.title }}</text>
|
||||
<text class="target-summary">{{ (currentPath.description || currentPath.summary || currentPath.content || '').slice(0, 80) }}...</text>
|
||||
</view>
|
||||
|
||||
<view class="timeline">
|
||||
<view class="timeline-line"></view>
|
||||
|
||||
<view
|
||||
v-for="(step, index) in currentPath.steps"
|
||||
:key="index"
|
||||
class="timeline-item"
|
||||
>
|
||||
<view class="timeline-dot" :class="{ done: step.done }">
|
||||
<view class="dot-inner" :class="{ pulse: step.done }"></view>
|
||||
</view>
|
||||
|
||||
<view class="step-card glass-card" :class="{ done: step.done }">
|
||||
<text class="step-phase">节点 {{ index + 1 }}</text>
|
||||
<text class="step-task">{{ step.task }}</text>
|
||||
<text class="step-desc">{{ step.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, onMounted, watch } from 'vue'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
import * as lifePathService from '../../services/lifePath.js'
|
||||
|
||||
const store = useAppStore()
|
||||
|
||||
const pathData = ref(null)
|
||||
|
||||
const selectedScript = computed(() => {
|
||||
return store.scripts.find(s => s.isSelected)
|
||||
})
|
||||
|
||||
const currentPath = computed(() => {
|
||||
if (pathData.value) return pathData.value
|
||||
if (store.currentPath) return store.currentPath
|
||||
return null
|
||||
})
|
||||
|
||||
const loadPath = async (scriptId) => {
|
||||
if (!scriptId) return
|
||||
try {
|
||||
const res = await lifePathService.getPathByScriptId(scriptId)
|
||||
pathData.value = res.data || null
|
||||
store.setCurrentPath(pathData.value)
|
||||
} catch (error) {
|
||||
pathData.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const goToScript = () => {
|
||||
uni.$emit('switchTab', 'script')
|
||||
}
|
||||
|
||||
watch(selectedScript, (val) => {
|
||||
if (val?.id) {
|
||||
loadPath(val.id)
|
||||
} else {
|
||||
pathData.value = null
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (selectedScript.value?.id) {
|
||||
loadPath(selectedScript.value.id)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.path-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 400;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 8rpx;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 120rpx 64rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(192, 132, 252, 0.5);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.empty-btn {
|
||||
font-size: 22rpx;
|
||||
padding: 18rpx 36rpx;
|
||||
}
|
||||
|
||||
.path-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 48rpx;
|
||||
}
|
||||
|
||||
.target-card {
|
||||
padding: 40rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.target-label {
|
||||
display: block;
|
||||
font-size: 18rpx;
|
||||
color: #C084FC;
|
||||
font-weight: 600;
|
||||
letter-spacing: 4rpx;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.target-summary {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
position: relative;
|
||||
padding-left: 60rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 48rpx;
|
||||
}
|
||||
|
||||
.timeline-line {
|
||||
position: absolute;
|
||||
left: 30rpx;
|
||||
top: 30rpx;
|
||||
bottom: 30rpx;
|
||||
width: 2rpx;
|
||||
background: linear-gradient(to bottom, #C084FC, rgba(192, 132, 252, 0.2), transparent);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
position: absolute;
|
||||
left: -46rpx;
|
||||
top: 24rpx;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
background: #0F071A;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.timeline-dot.done {
|
||||
border-color: #C084FC;
|
||||
box-shadow: 0 0 30rpx rgba(192, 132, 252, 0.4);
|
||||
}
|
||||
|
||||
.dot-inner {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.dot-inner.done {
|
||||
background: #C084FC;
|
||||
}
|
||||
|
||||
.dot-inner.pulse {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.6; transform: scale(1.2); }
|
||||
}
|
||||
|
||||
.step-card {
|
||||
padding: 32rpx;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.step-card.done {
|
||||
opacity: 1;
|
||||
border-color: rgba(192, 132, 252, 0.3);
|
||||
}
|
||||
|
||||
.step-phase {
|
||||
display: block;
|
||||
font-size: 18rpx;
|
||||
color: #C084FC;
|
||||
font-weight: 600;
|
||||
letter-spacing: 4rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.step-task {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -1,277 +0,0 @@
|
||||
<template>
|
||||
<view class="record-view">
|
||||
<view class="input-card glass-card">
|
||||
<view class="card-header">
|
||||
<view class="dot"></view>
|
||||
<text class="card-title">记叙当下</text>
|
||||
</view>
|
||||
|
||||
<input
|
||||
class="title-input"
|
||||
placeholder="为这段记忆命名"
|
||||
v-model="newEvent.title"
|
||||
/>
|
||||
|
||||
<picker class="date-picker" mode="date" :value="newEvent.time" @change="onDateChange">
|
||||
<view class="date-value">{{ newEvent.time || '选择日期' }}</view>
|
||||
</picker>
|
||||
|
||||
<textarea
|
||||
class="content-textarea"
|
||||
placeholder="此刻的心境或发生的事..."
|
||||
v-model="newEvent.content"
|
||||
rows="3"
|
||||
/>
|
||||
|
||||
<button
|
||||
class="btn-primary save-btn"
|
||||
:disabled="!canSave"
|
||||
:class="{ disabled: !canSave }"
|
||||
@click="saveEvent"
|
||||
>
|
||||
镌刻至星海
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="events-list">
|
||||
<view
|
||||
v-for="event in events"
|
||||
:key="event.id"
|
||||
class="event-card glass-card"
|
||||
>
|
||||
<view class="event-header">
|
||||
<text class="event-title">{{ event.title }}</text>
|
||||
<text class="event-date">{{ event.time }}</text>
|
||||
</view>
|
||||
|
||||
<text class="event-content">{{ event.content }}</text>
|
||||
|
||||
<view class="ai-reply">
|
||||
<view class="ai-header">
|
||||
<text class="ai-icon">✨</text>
|
||||
<text class="ai-title">Life Harmony AI</text>
|
||||
</view>
|
||||
<text class="ai-content" :class="{ typing: event.isNew }">
|
||||
{{ event.aiFeedback }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
|
||||
const store = useAppStore()
|
||||
|
||||
const newEvent = ref({
|
||||
title: '',
|
||||
time: getTodayDate(),
|
||||
content: ''
|
||||
})
|
||||
|
||||
const events = computed(() => store.events)
|
||||
|
||||
const canSave = computed(() => {
|
||||
return newEvent.value.title && newEvent.value.content
|
||||
})
|
||||
|
||||
function getTodayDate() {
|
||||
const today = new Date()
|
||||
return today.toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
const onDateChange = (e) => {
|
||||
newEvent.value.time = e.detail.value
|
||||
}
|
||||
|
||||
const saveEvent = async () => {
|
||||
if (!canSave.value) return
|
||||
|
||||
const eventData = {
|
||||
...newEvent.value,
|
||||
aiFeedback: '星河守望者正在解读这段紫色波频...',
|
||||
isNew: true
|
||||
}
|
||||
|
||||
await store.createEvent(eventData)
|
||||
|
||||
newEvent.value = {
|
||||
title: '',
|
||||
time: getTodayDate(),
|
||||
content: ''
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
const fullReply = `这段记忆碎片在星海中漾起涟漪。你在${eventData.title}中展现的特质,正在重新定义你人生OS的底层代码。继续保持这份觉知。`
|
||||
eventData.aiFeedback = fullReply
|
||||
eventData.isNew = false
|
||||
await store.fetchEvents()
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
store.fetchEvents()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.record-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.input-card {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
background: #C084FC;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 18rpx;
|
||||
color: rgba(192, 132, 252, 0.8);
|
||||
font-weight: 600;
|
||||
letter-spacing: 4rpx;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.title-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
background: transparent;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.title-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.date-value {
|
||||
font-size: 22rpx;
|
||||
color: rgba(168, 85, 247, 0.6);
|
||||
}
|
||||
|
||||
.content-textarea {
|
||||
width: 100%;
|
||||
height: 160rpx;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 26rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.content-textarea::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(90deg, rgba(147, 51, 234, 0.4), rgba(124, 58, 237, 0.4));
|
||||
border: 1px solid rgba(168, 85, 247, 0.3);
|
||||
}
|
||||
|
||||
.save-btn.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.events-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.event-card {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.event-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.event-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.event-date {
|
||||
font-size: 20rpx;
|
||||
color: rgba(168, 85, 247, 0.5);
|
||||
}
|
||||
|
||||
.event-content {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.ai-reply {
|
||||
background: rgba(168, 85, 247, 0.08);
|
||||
border: 1px solid rgba(168, 85, 247, 0.2);
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.ai-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.ai-icon {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.ai-title {
|
||||
font-size: 18rpx;
|
||||
color: rgba(192, 132, 252, 0.8);
|
||||
font-weight: 600;
|
||||
letter-spacing: 4rpx;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ai-content {
|
||||
font-size: 22rpx;
|
||||
color: rgba(243, 232, 255, 0.8);
|
||||
font-style: italic;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ai-content.typing {
|
||||
animation: typing 2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes typing {
|
||||
from { opacity: 0; transform: translateY(10rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
@@ -1,576 +0,0 @@
|
||||
<template>
|
||||
<view class="script-view">
|
||||
<text class="page-title font-serif">剧本生成器</text>
|
||||
|
||||
<view class="section-card glass-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">我的基础人设</text>
|
||||
<text class="section-hint">可自由修改</text>
|
||||
</view>
|
||||
|
||||
<view class="profile-grid">
|
||||
<input
|
||||
class="glass-input"
|
||||
placeholder="姓名"
|
||||
v-model="nickname"
|
||||
/>
|
||||
<picker class="glass-picker" mode="selector" :range="zodiacOptions" :value="zodiacIndex" @change="onZodiacChange">
|
||||
<view class="picker-value">{{ registrationData.zodiac || '星座' }}</view>
|
||||
</picker>
|
||||
<picker class="glass-picker" mode="selector" :range="mbtiOptions" :value="mbtiIndex" @change="onMbtiChange">
|
||||
<view class="picker-value">{{ registrationData.mbti || 'MBTI' }}</view>
|
||||
</picker>
|
||||
<input
|
||||
class="glass-input"
|
||||
placeholder="职业"
|
||||
v-model="profession"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-card glass-card">
|
||||
<view class="input-group">
|
||||
<text class="label">剧本主题</text>
|
||||
<input
|
||||
class="glass-input"
|
||||
placeholder="如:巅峰重现、治愈之旅、赛博觉醒..."
|
||||
v-model="scriptConfig.theme"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="input-group">
|
||||
<view class="npc-header">
|
||||
<text class="label">关键配角/新的人设</text>
|
||||
<button class="add-btn" @click="addNpc">+ 添加</button>
|
||||
</view>
|
||||
|
||||
<view class="npc-form">
|
||||
<input
|
||||
class="glass-input npc-input"
|
||||
placeholder="姓名"
|
||||
v-model="npcConfig.name"
|
||||
/>
|
||||
<picker class="glass-picker npc-picker" mode="selector" :range="npcRoleOptions" :value="npcRoleIndex" @change="onNpcRoleChange">
|
||||
<view class="picker-value">{{ npcConfig.role || '角色' }}</view>
|
||||
</picker>
|
||||
<picker class="glass-picker npc-picker" mode="selector" :range="npcRelationOptions" :value="npcRelationIndex" @change="onNpcRelationChange">
|
||||
<view class="picker-value">{{ npcConfig.relation || '关系' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<textarea
|
||||
class="glass-textarea"
|
||||
placeholder="自由描述TA的人设特点或关键剧情点..."
|
||||
v-model="npcConfig.desc"
|
||||
rows="2"
|
||||
/>
|
||||
|
||||
<view class="npc-list">
|
||||
<view
|
||||
v-for="(npc, index) in customNpcs"
|
||||
:key="index"
|
||||
class="npc-tag"
|
||||
>
|
||||
<text>{{ npc.name }} ({{ npc.role }})</text>
|
||||
<text class="delete-btn" @click="removeNpc(index)">×</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="params-row">
|
||||
<view class="param-group">
|
||||
<text class="param-label">叙事风格</text>
|
||||
<view class="param-options">
|
||||
<text
|
||||
v-for="style in scriptStyles"
|
||||
:key="style"
|
||||
class="param-option"
|
||||
:class="{ active: scriptConfig.style === style }"
|
||||
@click="scriptConfig.style = style"
|
||||
>
|
||||
{{ style }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="param-group">
|
||||
<text class="param-label">故事篇幅</text>
|
||||
<view class="param-options">
|
||||
<text
|
||||
v-for="length in scriptLengths"
|
||||
:key="length"
|
||||
class="param-option"
|
||||
:class="{ active: scriptConfig.length === length }"
|
||||
@click="scriptConfig.length = length"
|
||||
>
|
||||
{{ length }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button
|
||||
class="btn-primary generate-btn"
|
||||
:loading="isGenerating"
|
||||
:disabled="isGenerating || !scriptConfig.theme"
|
||||
@click="generateScript"
|
||||
>
|
||||
<text v-if="isGenerating">命运编织中...</text>
|
||||
<text v-else>生成平行人生剧本</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view v-if="scripts.length > 0" class="scripts-list">
|
||||
<view
|
||||
v-for="script in scripts"
|
||||
:key="script.id"
|
||||
class="script-card glass-card"
|
||||
:class="{ selected: script.isSelected }"
|
||||
>
|
||||
<view class="script-header">
|
||||
<text class="script-title">{{ script.title }}</text>
|
||||
<text class="script-persona">{{ script.theme || '追光者' }}</text>
|
||||
</view>
|
||||
|
||||
<text class="script-summary" lines="3">{{ getScriptSummary(script) }}</text>
|
||||
|
||||
<view class="script-footer">
|
||||
<text class="script-style">{{ script.style || '风格' }}</text>
|
||||
<button class="select-btn" @click="selectScript(script.id)">
|
||||
路径映射 →
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!isGenerating" class="empty-state glass-card">
|
||||
<text class="empty-icon">🎬</text>
|
||||
<text class="empty-text">尚未生成剧本,定义你的未来篇章</text>
|
||||
</view>
|
||||
|
||||
<view v-if="isGenerating" class="generating-state glass-card">
|
||||
<view class="spinner"></view>
|
||||
<text class="generating-text">正在采集星海中的深紫色碎屑...</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, reactive, onMounted } from 'vue'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
|
||||
const store = useAppStore()
|
||||
|
||||
const zodiacOptions = ['白羊座', '金牛座', '双子座', '巨蟹座', '狮子座', '处女座', '天秤座', '天蝎座', '射手座', '摩羯座', '水瓶座', '双鱼座']
|
||||
const mbtiOptions = ['INTJ', 'INTP', 'ENTJ', 'ENTP', 'INFJ', 'INFP', 'ENFJ', 'ENFP', 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ', 'ISTP', 'ISFP', 'ESTP', 'ESFP']
|
||||
const npcRoleOptions = ['伙伴', '宿敌', '导师', '挚爱', '下属', '路人']
|
||||
const npcRelationOptions = ['信任', '对立', '暧昧', '敬畏', '背叛', '守护']
|
||||
const scriptStyles = ['爽文', '治愈', '热血', '玄幻', '职场', '赛博']
|
||||
const scriptLengths = ['短篇', '中篇', '长篇', '史诗']
|
||||
|
||||
const registrationData = computed(() => store.registrationData || {})
|
||||
const scripts = computed(() => store.scripts || [])
|
||||
|
||||
const nickname = computed({
|
||||
get: () => registrationData.value.nickname || '',
|
||||
set: (val) => store.updateRegistration({ nickname: val })
|
||||
})
|
||||
|
||||
const profession = computed({
|
||||
get: () => registrationData.value.profession || '',
|
||||
set: (val) => store.updateRegistration({ profession: val })
|
||||
})
|
||||
|
||||
const zodiacIndex = computed(() => zodiacOptions.indexOf(registrationData.value.zodiac))
|
||||
const mbtiIndex = computed(() => mbtiOptions.indexOf(registrationData.value.mbti))
|
||||
|
||||
const scriptConfig = reactive({
|
||||
theme: '',
|
||||
style: '爽文',
|
||||
length: '中篇'
|
||||
})
|
||||
|
||||
const npcConfig = reactive({
|
||||
name: '',
|
||||
role: '伙伴',
|
||||
relation: '信任',
|
||||
desc: ''
|
||||
})
|
||||
|
||||
const customNpcs = ref([])
|
||||
const isGenerating = ref(false)
|
||||
|
||||
const npcRoleIndex = computed(() => npcRoleOptions.indexOf(npcConfig.role))
|
||||
const npcRelationIndex = computed(() => npcRelationOptions.indexOf(npcConfig.relation))
|
||||
|
||||
const onZodiacChange = (e) => {
|
||||
store.updateRegistration({ zodiac: zodiacOptions[e.detail.value] })
|
||||
}
|
||||
|
||||
const onMbtiChange = (e) => {
|
||||
store.updateRegistration({ mbti: mbtiOptions[e.detail.value] })
|
||||
}
|
||||
|
||||
const onNpcRoleChange = (e) => {
|
||||
npcConfig.role = npcRoleOptions[e.detail.value]
|
||||
}
|
||||
|
||||
const onNpcRelationChange = (e) => {
|
||||
npcConfig.relation = npcRelationOptions[e.detail.value]
|
||||
}
|
||||
|
||||
const addNpc = () => {
|
||||
if (npcConfig.name) {
|
||||
customNpcs.value.push({ ...npcConfig })
|
||||
npcConfig.name = ''
|
||||
npcConfig.desc = ''
|
||||
}
|
||||
}
|
||||
|
||||
const removeNpc = (index) => {
|
||||
customNpcs.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const generateScript = async () => {
|
||||
if (!scriptConfig.theme || isGenerating.value) return
|
||||
|
||||
isGenerating.value = true
|
||||
|
||||
try {
|
||||
await store.createScript({
|
||||
theme: scriptConfig.theme,
|
||||
style: scriptConfig.style,
|
||||
length: scriptConfig.length,
|
||||
character: registrationData.value,
|
||||
events: store.events
|
||||
})
|
||||
scriptConfig.theme = ''
|
||||
} finally {
|
||||
isGenerating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const selectScript = async (id) => {
|
||||
await store.selectScript(id)
|
||||
uni.$emit('switchTab', 'path')
|
||||
}
|
||||
|
||||
const getScriptSummary = (script) => {
|
||||
const text = script.summary || script.content || ''
|
||||
return text.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await store.fetchUserProfile()
|
||||
await store.fetchEvents()
|
||||
await store.fetchScripts()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.script-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 400;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 8rpx;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 22rpx;
|
||||
color: rgba(192, 132, 252, 0.6);
|
||||
font-weight: 600;
|
||||
letter-spacing: 4rpx;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.section-hint {
|
||||
font-size: 16rpx;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.profile-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.glass-input, .glass-picker {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16rpx;
|
||||
padding: 0 24rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.glass-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
line-height: 80rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 18rpx;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
font-weight: 600;
|
||||
letter-spacing: 4rpx;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.npc-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
font-size: 20rpx;
|
||||
color: #C084FC;
|
||||
border: 1px solid rgba(192, 132, 252, 0.3);
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.npc-form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.npc-input, .npc-picker {
|
||||
height: 72rpx;
|
||||
padding: 0 16rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.npc-picker .picker-value {
|
||||
line-height: 72rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.glass-textarea {
|
||||
width: 100%;
|
||||
height: 120rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.npc-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.npc-tag {
|
||||
background: rgba(168, 85, 247, 0.1);
|
||||
border: 1px solid rgba(168, 85, 247, 0.3);
|
||||
border-radius: 24rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
font-size: 20rpx;
|
||||
color: rgba(243, 232, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 28rpx;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
|
||||
.params-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.param-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.param-label {
|
||||
font-size: 18rpx;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.param-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.param-option {
|
||||
padding: 10rpx 20rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16rpx;
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.param-option.active {
|
||||
background: rgba(168, 85, 247, 0.2);
|
||||
border-color: rgba(168, 85, 247, 0.5);
|
||||
color: #C084FC;
|
||||
}
|
||||
|
||||
.generate-btn {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
box-shadow: 0 8rpx 40rpx rgba(168, 85, 247, 0.3);
|
||||
}
|
||||
|
||||
.scripts-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.script-card {
|
||||
padding: 32rpx;
|
||||
border-left: 4rpx solid transparent;
|
||||
}
|
||||
|
||||
.script-card.selected {
|
||||
border-left-color: #C084FC;
|
||||
}
|
||||
|
||||
.script-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.script-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.script-persona {
|
||||
font-size: 18rpx;
|
||||
color: rgba(168, 85, 247, 0.6);
|
||||
background: rgba(168, 85, 247, 0.1);
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
border: 1px solid rgba(168, 85, 247, 0.2);
|
||||
}
|
||||
|
||||
.script-summary {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.script-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.script-style {
|
||||
font-size: 20rpx;
|
||||
color: #C084FC;
|
||||
}
|
||||
|
||||
.select-btn {
|
||||
font-size: 24rpx;
|
||||
color: #C084FC;
|
||||
font-weight: 600;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 80rpx 48rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.generating-state {
|
||||
padding: 80rpx 48rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border: 4rpx solid #A855F7;
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.generating-text {
|
||||
font-size: 26rpx;
|
||||
color: rgba(192, 132, 252, 0.6);
|
||||
font-style: italic;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,273 +0,0 @@
|
||||
<template>
|
||||
<view class="main-page">
|
||||
<view class="bg-decoration">
|
||||
<view class="aurora-top"></view>
|
||||
<view class="aurora-bottom"></view>
|
||||
</view>
|
||||
|
||||
<view class="header" :style="{ paddingTop: safeAreaTop + 20 + 'px' }">
|
||||
<view class="header-left">
|
||||
<view class="logo-box">
|
||||
<image class="logo" src="/static/logo.svg" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="brand">
|
||||
<text class="brand-title font-serif">人生OS</text>
|
||||
<text class="brand-subtitle">LIFE HARMONY v3.1</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-right" @click="goToProfile">
|
||||
<view class="avatar-box">
|
||||
<image class="avatar" :src="userAvatar" mode="aspectFill" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="page-content" scroll-y>
|
||||
<view v-if="activeTab === 'record'" class="tab-page">
|
||||
<RecordView></RecordView>
|
||||
</view>
|
||||
|
||||
<view v-if="activeTab === 'script'" class="tab-page">
|
||||
<ScriptView></ScriptView>
|
||||
</view>
|
||||
|
||||
<view v-if="activeTab === 'path'" class="tab-page">
|
||||
<PathView></PathView>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="bottom-nav" :style="{ paddingBottom: safeAreaBottom + 'px' }">
|
||||
<view
|
||||
class="nav-item"
|
||||
:class="{ active: activeTab === 'record' }"
|
||||
@click="switchTab('record')"
|
||||
>
|
||||
<text class="nav-icon">📖</text>
|
||||
<text class="nav-label">回溯过去</text>
|
||||
</view>
|
||||
<view
|
||||
class="nav-item"
|
||||
:class="{ active: activeTab === 'script' }"
|
||||
@click="switchTab('script')"
|
||||
>
|
||||
<text class="nav-icon">✨</text>
|
||||
<text class="nav-label">创造未来</text>
|
||||
</view>
|
||||
<view
|
||||
class="nav-item"
|
||||
:class="{ active: activeTab === 'path' }"
|
||||
@click="switchTab('path')"
|
||||
>
|
||||
<text class="nav-icon">🗺️</text>
|
||||
<text class="nav-label">路径实现</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
import RecordView from './RecordView.vue'
|
||||
import ScriptView from './ScriptView.vue'
|
||||
import PathView from './PathView.vue'
|
||||
|
||||
const store = useAppStore()
|
||||
const activeTab = ref('record')
|
||||
|
||||
const safeAreaTop = ref(uni.getStorageSync('safeAreaTop') || 20)
|
||||
const safeAreaBottom = ref(uni.getStorageSync('safeAreaBottom') || 0)
|
||||
|
||||
onMounted(() => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
safeAreaTop.value = systemInfo.safeAreaInsets?.top || systemInfo.statusBarHeight || 20
|
||||
safeAreaBottom.value = systemInfo.safeAreaInsets?.bottom || 0
|
||||
|
||||
store.initialize()
|
||||
uni.$on('switchTab', switchTab)
|
||||
})
|
||||
|
||||
const userAvatar = computed(() => {
|
||||
const nickname = store.userProfile?.nickname || 'user'
|
||||
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${nickname}&backgroundColor=A855F7`
|
||||
})
|
||||
|
||||
const switchTab = (tab) => {
|
||||
activeTab.value = tab
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
uni.$off('switchTab', switchTab)
|
||||
})
|
||||
|
||||
const goToProfile = () => {
|
||||
uni.navigateTo({ url: '/pages/profile/index' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #0F071A 0%, #1A0B2E 50%, #0F071A 100%);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.aurora-top {
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
width: 120%;
|
||||
height: 60%;
|
||||
background: rgba(168, 85, 247, 0.08);
|
||||
filter: blur(120rpx);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.aurora-bottom {
|
||||
position: absolute;
|
||||
bottom: -10%;
|
||||
right: -10%;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
background: rgba(139, 92, 246, 0.05);
|
||||
filter: blur(100rpx);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 32rpx;
|
||||
padding-top: calc(24rpx + constant(safe-area-inset-top));
|
||||
padding-top: calc(24rpx + env(safe-area-inset-top));
|
||||
background: rgba(15, 7, 26, 0.6);
|
||||
backdrop-filter: blur(20rpx);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.logo-box {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 20rpx;
|
||||
background: linear-gradient(135deg, #9333EA 0%, #7C3AED 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
letter-spacing: 6rpx;
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
display: block;
|
||||
font-size: 16rpx;
|
||||
color: rgba(168, 85, 247, 0.6);
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.avatar-box {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tab-page {
|
||||
padding: 32rpx;
|
||||
padding-bottom: calc(180rpx + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.bottom-nav {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 140rpx;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
background: rgba(15, 7, 26, 0.95);
|
||||
backdrop-filter: blur(40rpx);
|
||||
-webkit-backdrop-filter: blur(40rpx);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6rpx;
|
||||
padding: 16rpx 32rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
transition: all 0.3s ease;
|
||||
min-width: 160rpx;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: #C084FC;
|
||||
transform: translateY(-8rpx);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 18rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: 4rpx;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.safe-area-bottom {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
</style>
|
||||
@@ -1,657 +0,0 @@
|
||||
<template>
|
||||
<view class="onboarding-page">
|
||||
<view class="bg-decoration">
|
||||
<view class="aurora-top"></view>
|
||||
<view class="aurora-bottom"></view>
|
||||
</view>
|
||||
|
||||
<view class="content" :style="{ paddingTop: safeAreaTop + 20 + 'px', paddingBottom: safeAreaBottom + 20 + 'px' }">
|
||||
<scroll-view class="step-content" scroll-y>
|
||||
<view class="step-inner">
|
||||
<view v-if="currentStep === 1" class="step animate-fade-in">
|
||||
<view class="step-header">
|
||||
<text class="step-title font-serif">系统初始化</text>
|
||||
<text class="step-subtitle">定义你在人生OS中的唯一代码</text>
|
||||
</view>
|
||||
|
||||
<view class="form-grid">
|
||||
<view class="input-group">
|
||||
<text class="label">你的昵称</text>
|
||||
<input
|
||||
class="glass-input"
|
||||
placeholder="输入昵称"
|
||||
v-model="formData.nickname"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="input-group">
|
||||
<text class="label">性别</text>
|
||||
<picker class="glass-picker" mode="selector" :range="genderOptions" :value="genderIndex" @change="onGenderChange">
|
||||
<view class="picker-value">{{ formData.gender || '请选择' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="input-group">
|
||||
<text class="label">星座</text>
|
||||
<picker class="glass-picker" mode="selector" :range="zodiacOptions" :value="zodiacIndex" @change="onZodiacChange">
|
||||
<view class="picker-value">{{ formData.zodiac || '请选择' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="input-group">
|
||||
<text class="label">MBTI人格</text>
|
||||
<picker class="glass-picker" mode="selector" :range="mbtiOptions" :value="mbtiIndex" @change="onMbtiChange">
|
||||
<view class="picker-value">{{ formData.mbti || '请选择' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="input-group full-width">
|
||||
<text class="label">兴趣爱好</text>
|
||||
<input
|
||||
class="glass-input"
|
||||
placeholder="用逗号分隔,如:阅读,旅行,摄影"
|
||||
v-model="hobbiesText"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="currentStep === 2" class="step animate-fade-in">
|
||||
<view class="step-header">
|
||||
<text class="step-title font-serif">回溯起源</text>
|
||||
<text class="step-subtitle">一段让你感到温暖的早期时光</text>
|
||||
</view>
|
||||
|
||||
<view class="memory-form">
|
||||
<view class="input-group">
|
||||
<text class="label">日期</text>
|
||||
<picker class="glass-picker" mode="date" :value="formData.childhood.date" @change="onChildhoodDateChange">
|
||||
<view class="picker-value">{{ formData.childhood.date || '请选择日期' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="input-group">
|
||||
<text class="label">回忆一段具体且温暖的午后...</text>
|
||||
<textarea
|
||||
class="glass-textarea"
|
||||
rows="4"
|
||||
placeholder="描述那段时光..."
|
||||
v-model="formData.childhood.text"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="hint-section">
|
||||
<view class="hint-title">
|
||||
<text class="icon">✨</text>
|
||||
<text>灵感气泡</text>
|
||||
</view>
|
||||
<view class="hint-tags">
|
||||
<text
|
||||
v-for="(hint, index) in childhoodHints"
|
||||
:key="index"
|
||||
class="hint-tag"
|
||||
@click="addChildhoodHint(hint)"
|
||||
>
|
||||
{{ hint }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="currentStep === 3" class="step animate-fade-in">
|
||||
<view class="step-header">
|
||||
<text class="step-title font-serif">悦然时刻</text>
|
||||
<text class="step-subtitle">一段感受到生命跃动的经历</text>
|
||||
</view>
|
||||
|
||||
<view class="memory-form">
|
||||
<view class="input-group">
|
||||
<text class="label">日期</text>
|
||||
<picker class="glass-picker" mode="date" :value="formData.joy.date" @change="onJoyDateChange">
|
||||
<view class="picker-value">{{ formData.joy.date || '请选择日期' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="input-group">
|
||||
<text class="label">想一个令你会心一笑的瞬间...</text>
|
||||
<textarea
|
||||
class="glass-textarea"
|
||||
rows="4"
|
||||
placeholder="描述那个瞬间..."
|
||||
v-model="formData.joy.text"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="hint-section">
|
||||
<view class="hint-title">
|
||||
<text class="icon">✨</text>
|
||||
<text>灵感气泡</text>
|
||||
</view>
|
||||
<view class="hint-tags">
|
||||
<text
|
||||
v-for="(hint, index) in joyHints"
|
||||
:key="index"
|
||||
class="hint-tag"
|
||||
@click="addJoyHint(hint)"
|
||||
>
|
||||
{{ hint }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="currentStep === 4" class="step animate-fade-in">
|
||||
<view class="step-header">
|
||||
<text class="step-title font-serif">破茧成蝶</text>
|
||||
<text class="step-subtitle">在宁静中默默积蓄力量的日子</text>
|
||||
</view>
|
||||
|
||||
<view class="memory-form">
|
||||
<view class="input-group">
|
||||
<text class="label">日期</text>
|
||||
<picker class="glass-picker" mode="date" :value="formData.low.date" @change="onLowDateChange">
|
||||
<view class="picker-value">{{ formData.low.date || '请选择日期' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="input-group">
|
||||
<text class="label">描述那段有些艰难但让你成长的日子...</text>
|
||||
<textarea
|
||||
class="glass-textarea"
|
||||
rows="4"
|
||||
placeholder="描述那段经历..."
|
||||
v-model="formData.low.text"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="hint-section">
|
||||
<view class="hint-title">
|
||||
<text class="icon">✨</text>
|
||||
<text>灵感气泡</text>
|
||||
</view>
|
||||
<view class="hint-tags">
|
||||
<text
|
||||
v-for="(hint, index) in lowHints"
|
||||
:key="index"
|
||||
class="hint-tag"
|
||||
@click="addLowHint(hint)"
|
||||
>
|
||||
{{ hint }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="currentStep === 5" class="step animate-fade-in">
|
||||
<view class="step-header">
|
||||
<text class="step-title font-serif">未来憧憬</text>
|
||||
<text class="step-subtitle">对未来理想生活状态的预见</text>
|
||||
</view>
|
||||
|
||||
<view class="memory-form">
|
||||
<view class="input-group">
|
||||
<text class="label">你想成为怎样的人?</text>
|
||||
<textarea
|
||||
class="glass-textarea"
|
||||
rows="3"
|
||||
placeholder="描述你对未来的愿景..."
|
||||
v-model="formData.future.vision"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="input-group">
|
||||
<text class="label">你的理想生活状态</text>
|
||||
<textarea
|
||||
class="glass-textarea"
|
||||
rows="3"
|
||||
placeholder="描述理想的一天..."
|
||||
v-model="formData.future.ideal"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="quote-box">
|
||||
<text class="quote-icon">✨</text>
|
||||
<text class="quote-text">"照见未来,便是创造的开始。"</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="bottom-bar">
|
||||
<view class="step-dots">
|
||||
<view
|
||||
v-for="step in 5"
|
||||
:key="step"
|
||||
class="step-dot"
|
||||
:class="{ active: currentStep === step }"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="actions">
|
||||
<button
|
||||
v-if="currentStep > 1"
|
||||
class="btn-secondary"
|
||||
@click="prevStep"
|
||||
>
|
||||
返回
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn-primary next-btn"
|
||||
:loading="isSaving"
|
||||
@click="nextStep"
|
||||
>
|
||||
<text v-if="currentStep === 5">{{ isSaving ? '保存中...' : '开启人生' }}</text>
|
||||
<text v-else>继续</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, reactive, onMounted, watch } from 'vue'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
import * as lifeEventService from '../../services/lifeEvent.js'
|
||||
|
||||
const store = useAppStore()
|
||||
|
||||
const safeAreaTop = ref(uni.getStorageSync('safeAreaTop') || 20)
|
||||
const safeAreaBottom = ref(uni.getStorageSync('safeAreaBottom') || 0)
|
||||
|
||||
onMounted(async () => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
safeAreaTop.value = systemInfo.safeAreaInsets?.top || systemInfo.statusBarHeight || 20
|
||||
safeAreaBottom.value = systemInfo.safeAreaInsets?.bottom || 0
|
||||
await store.fetchUserProfile()
|
||||
Object.assign(formData, store.registrationData)
|
||||
currentStep.value = store.currentStep || 1
|
||||
})
|
||||
|
||||
const currentStep = ref(store.currentStep || 1)
|
||||
const isSaving = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
nickname: '',
|
||||
gender: '',
|
||||
mbti: '',
|
||||
zodiac: '',
|
||||
profession: '',
|
||||
hobbies: [],
|
||||
childhood: { date: '', text: '' },
|
||||
joy: { date: '', text: '' },
|
||||
low: { date: '', text: '' },
|
||||
future: { vision: '', ideal: '' }
|
||||
})
|
||||
|
||||
const genderOptions = ['男', '女']
|
||||
const zodiacOptions = ['白羊座', '金牛座', '双子座', '巨蟹座', '狮子座', '处女座', '天秤座', '天蝎座', '射手座', '摩羯座', '水瓶座', '双鱼座']
|
||||
const mbtiOptions = ['INTJ', 'INTP', 'ENTJ', 'ENTP', 'INFJ', 'INFP', 'ENFJ', 'ENFP', 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ', 'ISTP', 'ISFP', 'ESTP', 'ESFP']
|
||||
|
||||
const childhoodHints = ['秘密花园', '老旧弄堂', '秋千', '夏蝉', '被保护的', '好奇心']
|
||||
const joyHints = ['突破', '共鸣', '清晨阳光', '认可', '旅行终点', '深呼吸']
|
||||
const lowHints = ['迷茫', '无力感', '雨后街道', '重新出发', '裂痕中的光', '蜕变']
|
||||
|
||||
const hobbiesText = computed({
|
||||
get: () => formData.hobbies.join(','),
|
||||
set: (val) => {
|
||||
formData.hobbies = val.split(/[,,]/).map(s => s.trim()).filter(s => s)
|
||||
}
|
||||
})
|
||||
|
||||
const genderIndex = computed(() => genderOptions.indexOf(formData.gender))
|
||||
const zodiacIndex = computed(() => zodiacOptions.indexOf(formData.zodiac))
|
||||
const mbtiIndex = computed(() => mbtiOptions.indexOf(formData.mbti))
|
||||
|
||||
const onGenderChange = (e) => {
|
||||
formData.gender = genderOptions[e.detail.value]
|
||||
}
|
||||
|
||||
const onZodiacChange = (e) => {
|
||||
formData.zodiac = zodiacOptions[e.detail.value]
|
||||
}
|
||||
|
||||
const onMbtiChange = (e) => {
|
||||
formData.mbti = mbtiOptions[e.detail.value]
|
||||
}
|
||||
|
||||
const onChildhoodDateChange = (e) => {
|
||||
formData.childhood.date = e.detail.value
|
||||
}
|
||||
|
||||
const onJoyDateChange = (e) => {
|
||||
formData.joy.date = e.detail.value
|
||||
}
|
||||
|
||||
const onLowDateChange = (e) => {
|
||||
formData.low.date = e.detail.value
|
||||
}
|
||||
|
||||
const addChildhoodHint = (hint) => {
|
||||
formData.childhood.text += hint + ' '
|
||||
}
|
||||
|
||||
const addJoyHint = (hint) => {
|
||||
formData.joy.text += hint + ' '
|
||||
}
|
||||
|
||||
const addLowHint = (hint) => {
|
||||
formData.low.text += hint + ' '
|
||||
}
|
||||
|
||||
const prevStep = () => {
|
||||
if (currentStep.value > 1) {
|
||||
saveStepData()
|
||||
currentStep.value--
|
||||
}
|
||||
}
|
||||
|
||||
const nextStep = async () => {
|
||||
saveStepData()
|
||||
|
||||
if (currentStep.value < 5) {
|
||||
currentStep.value++
|
||||
} else {
|
||||
isSaving.value = true
|
||||
try {
|
||||
const result = await store.saveUserProfile()
|
||||
if (result.success) {
|
||||
const eventsToSave = [
|
||||
{ data: formData.childhood, type: 'childhood', title: '童年记忆' },
|
||||
{ data: formData.joy, type: 'joy', title: '光芒闪耀的时刻' },
|
||||
{ data: formData.low, type: 'low', title: '在暗夜中潜行' }
|
||||
]
|
||||
await Promise.all(
|
||||
eventsToSave.map(({ data, type, title }) => {
|
||||
if (!data?.date || !data?.text) return Promise.resolve()
|
||||
return lifeEventService.createEvent({
|
||||
title,
|
||||
time: data.date,
|
||||
content: data.text,
|
||||
eventType: 'milestone',
|
||||
tags: [type]
|
||||
})
|
||||
})
|
||||
)
|
||||
await store.fetchEvents()
|
||||
uni.showToast({ title: '欢迎开启人生OS', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({ url: '/pages/main/index' })
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({ title: result.error || '保存失败', icon: 'none' })
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const saveStepData = () => {
|
||||
store.updateRegistration({ ...formData })
|
||||
}
|
||||
|
||||
watch(currentStep, (val) => {
|
||||
store.setCurrentStep(val)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.onboarding-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #0F071A 0%, #1A0B2E 50%, #0F071A 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.aurora-top {
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
width: 120%;
|
||||
height: 60%;
|
||||
background: rgba(168, 85, 247, 0.08);
|
||||
filter: blur(120rpx);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.aurora-bottom {
|
||||
position: absolute;
|
||||
bottom: -10%;
|
||||
right: -10%;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
background: rgba(139, 92, 246, 0.05);
|
||||
filter: blur(100rpx);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 40rpx;
|
||||
padding-top: calc(40rpx + constant(safe-area-inset-top));
|
||||
padding-top: calc(40rpx + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.step-inner {
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.step {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.step-header {
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
display: block;
|
||||
font-size: 46rpx;
|
||||
font-weight: 400;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
margin-bottom: 12rpx;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.step-subtitle {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: rgba(192, 132, 252, 0.6);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 18rpx;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
margin-bottom: 12rpx;
|
||||
letter-spacing: 4rpx;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.glass-input, .glass-picker {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 20rpx;
|
||||
padding: 0 24rpx;
|
||||
color: #F3E8FF;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.glass-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
line-height: 88rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.glass-textarea {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
color: #F3E8FF;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.glass-textarea::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.memory-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.hint-section {
|
||||
background: rgba(168, 85, 247, 0.08);
|
||||
border: 1px solid rgba(168, 85, 247, 0.15);
|
||||
border-radius: 32rpx;
|
||||
padding: 32rpx;
|
||||
margin-top: 16rpx;
|
||||
box-shadow: inset 0 0 30rpx rgba(168, 85, 247, 0.08);
|
||||
}
|
||||
|
||||
.hint-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 18rpx;
|
||||
color: rgba(192, 132, 252, 0.6);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 4rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.hint-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.hint-tag {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 32rpx;
|
||||
padding: 10rpx 22rpx;
|
||||
font-size: 22rpx;
|
||||
color: rgba(243, 232, 255, 0.8);
|
||||
}
|
||||
|
||||
.hint-tag:active {
|
||||
background: rgba(168, 85, 247, 0.2);
|
||||
border-color: rgba(168, 85, 247, 0.4);
|
||||
}
|
||||
|
||||
.quote-box {
|
||||
background: linear-gradient(135deg, rgba(168, 85, 247, 0.15), rgba(232, 121, 249, 0.1));
|
||||
border: 1px solid rgba(168, 85, 247, 0.3);
|
||||
border-radius: 20rpx;
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.quote-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.quote-text {
|
||||
font-size: 26rpx;
|
||||
color: rgba(243, 232, 255, 0.7);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
padding-top: 32rpx;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.step-dots {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.step-dot {
|
||||
flex: 1;
|
||||
height: 8rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4rpx;
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
|
||||
.step-dot.active {
|
||||
flex: 1;
|
||||
height: 8rpx;
|
||||
background: #A855F7;
|
||||
box-shadow: 0 0 24rpx rgba(168, 85, 247, 0.6);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 24rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
min-width: 200rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,296 +0,0 @@
|
||||
<template>
|
||||
<view class="profile-page">
|
||||
<view class="bg-decoration">
|
||||
<view class="aurora-top"></view>
|
||||
<view class="aurora-bottom"></view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="content" scroll-y :style="{ paddingTop: safeAreaTop + 20 + 'px', paddingBottom: safeAreaBottom + 20 + 'px' }">
|
||||
<view class="user-card">
|
||||
<view class="avatar-box">
|
||||
<image class="avatar" :src="userAvatar" mode="aspectFill" />
|
||||
<view class="verified-badge">✓</view>
|
||||
</view>
|
||||
|
||||
<view class="user-info">
|
||||
<text class="nickname font-serif">{{ userProfile.nickname || '未同步系统' }}</text>
|
||||
<text class="user-tags">{{ userTags }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-row">
|
||||
<view class="stat-card glass-card">
|
||||
<text class="stat-label">觉醒深度</text>
|
||||
<text class="stat-value">Lv.4</text>
|
||||
</view>
|
||||
<view class="stat-card glass-card">
|
||||
<text class="stat-label">星历契合</text>
|
||||
<text class="stat-value">98%</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="menu-list">
|
||||
<view class="menu-item glass-card" @click="editProfile">
|
||||
<view class="menu-left">
|
||||
<text class="menu-icon">👤</text>
|
||||
<text class="menu-title">个人档案设置</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
|
||||
<view class="menu-item glass-card">
|
||||
<view class="menu-left">
|
||||
<text class="menu-icon">🔄</text>
|
||||
<text class="menu-title">多账号切换</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
|
||||
<view class="menu-item glass-card">
|
||||
<view class="menu-left">
|
||||
<text class="menu-icon">📧</text>
|
||||
<text class="menu-title">与开发者对话</text>
|
||||
</view>
|
||||
<text class="menu-arrow">↗</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="logout-btn" @click="handleLogout">
|
||||
TERMINATE LIFE HARMONY
|
||||
</button>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
|
||||
const store = useAppStore()
|
||||
|
||||
const safeAreaTop = ref(uni.getStorageSync('safeAreaTop') || 20)
|
||||
const safeAreaBottom = ref(uni.getStorageSync('safeAreaBottom') || 0)
|
||||
|
||||
onMounted(() => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
safeAreaTop.value = systemInfo.safeAreaInsets?.top || systemInfo.statusBarHeight || 20
|
||||
safeAreaBottom.value = systemInfo.safeAreaInsets?.bottom || 0
|
||||
})
|
||||
|
||||
const userProfile = computed(() => store.userProfile || {})
|
||||
|
||||
const userAvatar = computed(() => {
|
||||
const nickname = userProfile.value.nickname || 'user'
|
||||
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${nickname}&backgroundColor=A855F7`
|
||||
})
|
||||
|
||||
const userTags = computed(() => {
|
||||
const { mbti, zodiac, profession } = userProfile.value
|
||||
const tags = [mbti || 'QUESTER', zodiac || 'STAR', profession || '星民']
|
||||
return tags.join(' · ')
|
||||
})
|
||||
|
||||
const editProfile = () => {
|
||||
uni.navigateTo({ url: '/pages/onboarding/index?edit=1' })
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
uni.showModal({
|
||||
title: '确认退出',
|
||||
content: '确定要退出登录吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
await store.logout()
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.profile-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #0F071A 0%, #1A0B2E 50%, #0F071A 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.aurora-top {
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
width: 120%;
|
||||
height: 60%;
|
||||
background: rgba(168, 85, 247, 0.08);
|
||||
filter: blur(120rpx);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.aurora-bottom {
|
||||
position: absolute;
|
||||
bottom: -10%;
|
||||
right: -10%;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
background: rgba(139, 92, 246, 0.05);
|
||||
filter: blur(100rpx);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 40rpx;
|
||||
padding-top: calc(60rpx + constant(safe-area-inset-top));
|
||||
padding-top: calc(60rpx + env(safe-area-inset-top));
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.user-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48rpx 0;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.avatar-box {
|
||||
position: relative;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid rgba(168, 85, 247, 0.3);
|
||||
padding: 8rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: rgba(168, 85, 247, 0.1);
|
||||
}
|
||||
|
||||
.verified-badge {
|
||||
position: absolute;
|
||||
bottom: 8rpx;
|
||||
right: 8rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: #9333EA;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20rpx;
|
||||
color: white;
|
||||
border: 4rpx solid #0F071A;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: 300;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
margin-bottom: 16rpx;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.user-tags {
|
||||
display: block;
|
||||
font-size: 18rpx;
|
||||
color: rgba(168, 85, 247, 0.6);
|
||||
letter-spacing: 4rpx;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 18rpx;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
letter-spacing: 4rpx;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: 300;
|
||||
color: rgba(243, 232, 255, 0.9);
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 64rpx;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
font-size: 32rpx;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
padding: 32rpx;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-size: 20rpx;
|
||||
letter-spacing: 6rpx;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.logout-btn:active {
|
||||
color: rgba(168, 85, 247, 0.5);
|
||||
border-color: rgba(168, 85, 247, 0.2);
|
||||
}
|
||||
</style>
|
||||
@@ -1,131 +0,0 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="bg-decoration">
|
||||
<view class="aurora-top"></view>
|
||||
<view class="aurora-bottom"></view>
|
||||
</view>
|
||||
<view class="overlay">
|
||||
<view class="status-bar-space" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
<view class="content-area" :style="{ paddingBottom: safeAreaBottom + 'px' }">
|
||||
<image class="logo" src="/static/logo.svg" mode="widthFix"></image>
|
||||
<text class="app-name font-serif">人生OS</text>
|
||||
<text class="app-version">LIFE HARMONY v3.1</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
|
||||
const statusBarHeight = ref(20)
|
||||
const safeAreaBottom = ref(0)
|
||||
|
||||
onLoad(() => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = systemInfo.statusBarHeight || 20
|
||||
safeAreaBottom.value = systemInfo.safeAreaInsets?.bottom || 0
|
||||
setTimeout(async () => {
|
||||
const store = useAppStore()
|
||||
const token = uni.getStorageSync('access_token')
|
||||
if (token) {
|
||||
await store.fetchUserProfile()
|
||||
if (store.hasProfile) {
|
||||
uni.reLaunch({ url: '/pages/main/index' })
|
||||
} else {
|
||||
uni.reLaunch({ url: '/pages/onboarding/index' })
|
||||
}
|
||||
} else {
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
}
|
||||
}, 2000)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
position: relative;
|
||||
width: 750rpx;
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
background: linear-gradient(180deg, #0F071A 0%, #1A0B2E 50%, #0F071A 100%);
|
||||
}
|
||||
|
||||
.status-bar-space {
|
||||
height: constant(safe-area-inset-top);
|
||||
height: env(safe-area-inset-top);
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.aurora-top {
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
width: 120%;
|
||||
height: 60%;
|
||||
background: rgba(168, 85, 247, 0.08);
|
||||
filter: blur(120rpx);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.aurora-bottom {
|
||||
position: absolute;
|
||||
bottom: -10%;
|
||||
right: -10%;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
background: rgba(139, 92, 246, 0.05);
|
||||
filter: blur(100rpx);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 180rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 52rpx;
|
||||
font-weight: 300;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
letter-spacing: 8rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
font-size: 24rpx;
|
||||
color: rgba(168, 85, 247, 0.7);
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,123 +0,0 @@
|
||||
/**
|
||||
* 认证服务
|
||||
* 处理登录、注册、验证码等认证相关接口
|
||||
* 与Web端(life-script/src/services/auth.js)保持一致
|
||||
*/
|
||||
|
||||
import { get, post } from './request.js'
|
||||
|
||||
/**
|
||||
* 获取短信验证码
|
||||
* @param {string} phone - 手机号
|
||||
* @returns {Promise<Object>} 验证码响应
|
||||
*/
|
||||
export const getSmsCode = async (phone) => {
|
||||
const response = await get('/auth/sms-code', { phone })
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录(手机号 + 验证码)
|
||||
* @param {Object} params - 登录参数
|
||||
* @param {string} params.phone - 手机号
|
||||
* @param {string} params.smsCode - 短信验证码
|
||||
* @returns {Promise<Object>} 登录响应(包含 token)
|
||||
*/
|
||||
export const login = async ({ phone, smsCode }) => {
|
||||
const response = await post('/auth/login', { phone, smsCode })
|
||||
|
||||
// 保存token
|
||||
if (response.data) {
|
||||
const { accessToken, refreshToken } = response.data
|
||||
if (accessToken) {
|
||||
uni.setStorageSync('access_token', accessToken)
|
||||
}
|
||||
if (refreshToken) {
|
||||
uni.setStorageSync('refresh_token', refreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const logout = async () => {
|
||||
try {
|
||||
await post('/auth/logout')
|
||||
} finally {
|
||||
// 无论成功失败都清除本地token
|
||||
uni.removeStorageSync('access_token')
|
||||
uni.removeStorageSync('refresh_token')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token
|
||||
* @returns {Promise<Object>} 新的token
|
||||
*/
|
||||
export const refreshToken = async () => {
|
||||
const refreshToken = uni.getStorageSync('refresh_token')
|
||||
if (!refreshToken) {
|
||||
throw new Error('No refresh token')
|
||||
}
|
||||
|
||||
const response = await post('/auth/refreshToken', { refreshToken })
|
||||
|
||||
// 更新token
|
||||
if (response.data) {
|
||||
const { accessToken, refreshToken: newRefreshToken } = response.data
|
||||
if (accessToken) {
|
||||
uni.setStorageSync('access_token', accessToken)
|
||||
}
|
||||
if (newRefreshToken) {
|
||||
uni.setStorageSync('refresh_token', newRefreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证token是否有效
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export const validateToken = async () => {
|
||||
try {
|
||||
const response = await get('/auth/validateToken')
|
||||
return response.data === true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
* @returns {Promise<Object>} 用户信息
|
||||
*/
|
||||
export const getCurrentUserInfo = async () => {
|
||||
const response = await get('/auth/userInfo')
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查手机号是否已注册
|
||||
* @param {string} phone - 手机号
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export const checkPhone = async (phone) => {
|
||||
const response = await get('/auth/checkPhone', { phone })
|
||||
return response.data
|
||||
}
|
||||
|
||||
export default {
|
||||
getSmsCode,
|
||||
login,
|
||||
logout,
|
||||
refreshToken,
|
||||
validateToken,
|
||||
getCurrentUserInfo,
|
||||
checkPhone
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
/**
|
||||
* 爽文剧本服务
|
||||
* 处理剧本的增删改查
|
||||
*/
|
||||
|
||||
import { get, post, put, del } from './request.js'
|
||||
|
||||
export const getScriptList = async () => {
|
||||
const response = await get('/epicScript/listAll')
|
||||
return response
|
||||
}
|
||||
|
||||
export const getScriptPage = async ({ pageNum = 1, pageSize = 10 }) => {
|
||||
const response = await get('/epicScript/page', { pageNum, pageSize })
|
||||
return response
|
||||
}
|
||||
|
||||
export const getScriptById = async (id) => {
|
||||
const response = await get('/epicScript/detail', { id })
|
||||
return response
|
||||
}
|
||||
|
||||
export const createScript = async (scriptData) => {
|
||||
const requestData = transformToBackendFormat(scriptData)
|
||||
const response = await post('/epicScript/create', requestData)
|
||||
return response
|
||||
}
|
||||
|
||||
export const updateScript = async (scriptData) => {
|
||||
const requestData = transformToBackendFormat(scriptData)
|
||||
const response = await put('/epicScript/update', requestData)
|
||||
return response
|
||||
}
|
||||
|
||||
export const selectScript = async (id) => {
|
||||
const response = await put('/epicScript/select', null, { params: { id } })
|
||||
return response
|
||||
}
|
||||
|
||||
export const deleteScript = async (id) => {
|
||||
const response = await del('/epicScript/delete', { id })
|
||||
return response
|
||||
}
|
||||
|
||||
const transformToBackendFormat = (frontendData) => {
|
||||
const {
|
||||
id,
|
||||
theme,
|
||||
style,
|
||||
length,
|
||||
content,
|
||||
isSelected,
|
||||
character,
|
||||
events
|
||||
} = frontendData
|
||||
|
||||
let title = theme || '我的剧本'
|
||||
let plotIntro = ''
|
||||
let plotTurning = ''
|
||||
let plotClimax = ''
|
||||
let plotEnding = ''
|
||||
|
||||
if (content) {
|
||||
const sections = content.split(/【[^】]+】/)
|
||||
const titles = content.match(/【[^】]+】/g) || []
|
||||
|
||||
titles.forEach((t, index) => {
|
||||
const sectionContent = sections[index + 1]?.trim() || ''
|
||||
if (t.includes('序幕') || t.includes('低谷')) {
|
||||
plotIntro = sectionContent
|
||||
} else if (t.includes('转折') || t.includes('契机')) {
|
||||
plotTurning = sectionContent
|
||||
} else if (t.includes('高潮') || t.includes('抉择')) {
|
||||
plotClimax = sectionContent
|
||||
} else if (t.includes('结局') || t.includes('开始')) {
|
||||
plotEnding = sectionContent
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let characterInfo = ''
|
||||
if (character) {
|
||||
const parts = [
|
||||
`姓名:${character.nickname || '未设置'}`,
|
||||
`性别:${character.gender || '未设置'}`,
|
||||
`MBTI:${character.mbti || '未设置'}`,
|
||||
`星座:${character.zodiac || '未设置'}`,
|
||||
`职业:${character.profession || '未设置'}`,
|
||||
`兴趣爱好:${character.hobbies?.join(',') || '无'}`
|
||||
]
|
||||
|
||||
if (character.future) {
|
||||
if (character.future.vision) parts.push(`未来愿景:${character.future.vision}`)
|
||||
if (character.future.ideal) parts.push(`理想生活:${character.future.ideal}`)
|
||||
}
|
||||
|
||||
characterInfo = parts.join('\n')
|
||||
}
|
||||
|
||||
let lifeEventsSummary = ''
|
||||
const eventParts = []
|
||||
|
||||
if (character) {
|
||||
if (character.childhood?.text) {
|
||||
eventParts.push(`【童年记忆】(${character.childhood.date || '未知时间'}): ${character.childhood.text}`)
|
||||
}
|
||||
if (character.joy?.text) {
|
||||
eventParts.push(`【高光时刻】(${character.joy.date || '未知时间'}): ${character.joy.text}`)
|
||||
}
|
||||
if (character.low?.text) {
|
||||
eventParts.push(`【至暗时刻】(${character.low.date || '未知时间'}): ${character.low.text}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (events && Array.isArray(events)) {
|
||||
events.forEach(e => {
|
||||
const dateStr = e.time || e.eventDate || '未知时间'
|
||||
const titleStr = e.title || '无标题'
|
||||
const contentStr = e.content || ''
|
||||
eventParts.push(`【人生事件】(${dateStr}) ${titleStr}${contentStr ? ': ' + contentStr : ''}`)
|
||||
})
|
||||
}
|
||||
|
||||
lifeEventsSummary = eventParts.join('\n')
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
theme,
|
||||
style,
|
||||
length,
|
||||
plotIntro,
|
||||
plotTurning,
|
||||
plotClimax,
|
||||
plotEnding,
|
||||
plotJson: content ? { fullContent: content } : null,
|
||||
isSelected,
|
||||
characterInfo,
|
||||
lifeEventsSummary
|
||||
}
|
||||
}
|
||||
|
||||
export const transformToFrontendFormat = (backendData) => {
|
||||
if (!backendData) return null
|
||||
|
||||
const {
|
||||
id,
|
||||
userId,
|
||||
title,
|
||||
theme,
|
||||
style,
|
||||
length,
|
||||
plotIntro,
|
||||
plotTurning,
|
||||
plotClimax,
|
||||
plotEnding,
|
||||
plotJson,
|
||||
isSelected,
|
||||
createTime
|
||||
} = backendData
|
||||
|
||||
let content = ''
|
||||
if (plotJson?.fullContent) {
|
||||
content = plotJson.fullContent
|
||||
} else {
|
||||
const parts = []
|
||||
if (plotIntro) parts.push(`【序幕:低谷回响】\n${plotIntro}`)
|
||||
if (plotTurning) parts.push(`【转折:契机出现】\n${plotTurning}`)
|
||||
if (plotClimax) parts.push(`【高潮:命运抉择】\n${plotClimax}`)
|
||||
if (plotEnding) parts.push(`【结局:新的开始】\n${plotEnding}`)
|
||||
content = parts.join('\n\n')
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
userId,
|
||||
title: title || theme || '未命名剧本',
|
||||
theme: theme || '',
|
||||
style: style || '',
|
||||
length: length || 'medium',
|
||||
content,
|
||||
isSelected: isSelected || false,
|
||||
date: createTime ? new Date(createTime).toLocaleDateString() : new Date().toLocaleDateString()
|
||||
}
|
||||
}
|
||||
|
||||
export const transformListToFrontend = (backendList) => {
|
||||
if (!Array.isArray(backendList)) return []
|
||||
return backendList.map(transformToFrontendFormat)
|
||||
}
|
||||
|
||||
export default {
|
||||
getScriptList,
|
||||
getScriptPage,
|
||||
getScriptById,
|
||||
createScript,
|
||||
updateScript,
|
||||
selectScript,
|
||||
deleteScript,
|
||||
transformToFrontendFormat,
|
||||
transformListToFrontend
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
/**
|
||||
* 生命事件服务
|
||||
* 处理生命事件的增删改查
|
||||
*/
|
||||
|
||||
import { get, post, put, del } from './request.js'
|
||||
|
||||
export const getEventList = async () => {
|
||||
const response = await get('/lifeEvent/list')
|
||||
return response
|
||||
}
|
||||
|
||||
export const getEventPage = async ({ pageNum = 1, pageSize = 10 }) => {
|
||||
const response = await get('/lifeEvent/page', { pageNum, pageSize })
|
||||
return response
|
||||
}
|
||||
|
||||
export const getEventById = async (id) => {
|
||||
const response = await get('/lifeEvent/detail', { id })
|
||||
return response
|
||||
}
|
||||
|
||||
export const createEvent = async (eventData) => {
|
||||
const requestData = transformToBackendFormat(eventData)
|
||||
const response = await post('/lifeEvent/create', requestData)
|
||||
return response
|
||||
}
|
||||
|
||||
export const updateEvent = async (eventData) => {
|
||||
const requestData = transformToBackendFormat(eventData)
|
||||
const response = await put('/lifeEvent/update', requestData)
|
||||
return response
|
||||
}
|
||||
|
||||
export const deleteEvent = async (id) => {
|
||||
const response = await del('/lifeEvent/delete', { id })
|
||||
return response
|
||||
}
|
||||
|
||||
const transformToBackendFormat = (frontendData) => {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
time,
|
||||
content,
|
||||
aiFeedback,
|
||||
eventType = 'daily_log',
|
||||
emotionType,
|
||||
emotionScore,
|
||||
tags
|
||||
} = frontendData
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
eventDate: time,
|
||||
content,
|
||||
aiReply: aiFeedback,
|
||||
eventType,
|
||||
emotionType,
|
||||
emotionScore,
|
||||
tags
|
||||
}
|
||||
}
|
||||
|
||||
export const transformToFrontendFormat = (backendData) => {
|
||||
if (!backendData) return null
|
||||
|
||||
const {
|
||||
id,
|
||||
userId,
|
||||
title,
|
||||
eventDate,
|
||||
content,
|
||||
aiReply,
|
||||
eventType,
|
||||
emotionType,
|
||||
emotionScore,
|
||||
tags,
|
||||
createTime
|
||||
} = backendData
|
||||
|
||||
return {
|
||||
id,
|
||||
userId,
|
||||
title: title || '',
|
||||
time: eventDate ? eventDate.split('T')[0] : '',
|
||||
content: content || '',
|
||||
aiFeedback: aiReply || '',
|
||||
eventType: eventType || 'daily_log',
|
||||
emotionType,
|
||||
emotionScore,
|
||||
tags: tags || [],
|
||||
createTime
|
||||
}
|
||||
}
|
||||
|
||||
export const transformListToFrontend = (backendList) => {
|
||||
if (!Array.isArray(backendList)) return []
|
||||
return backendList.map(transformToFrontendFormat)
|
||||
}
|
||||
|
||||
export default {
|
||||
getEventList,
|
||||
getEventPage,
|
||||
getEventById,
|
||||
createEvent,
|
||||
updateEvent,
|
||||
deleteEvent,
|
||||
transformToFrontendFormat,
|
||||
transformListToFrontend
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
/**
|
||||
* 实现路径服务
|
||||
* 处理路径的增删改查
|
||||
*/
|
||||
|
||||
import { get, post, put, del } from './request.js'
|
||||
|
||||
export const getPathList = async () => {
|
||||
const response = await get('/lifePath/listAll')
|
||||
return response
|
||||
}
|
||||
|
||||
export const getPathPage = async ({ pageNum = 1, pageSize = 10 }) => {
|
||||
const response = await get('/lifePath/page', { pageNum, pageSize })
|
||||
return response
|
||||
}
|
||||
|
||||
export const getPathByScriptId = async (scriptId) => {
|
||||
const response = await get('/lifePath/byScript', { scriptId })
|
||||
return response
|
||||
}
|
||||
|
||||
export const getPathById = async (id) => {
|
||||
const response = await get('/lifePath/detail', { id })
|
||||
return response
|
||||
}
|
||||
|
||||
export const createPath = async (pathData) => {
|
||||
const requestData = transformToBackendFormat(pathData)
|
||||
const response = await post('/lifePath/create', requestData)
|
||||
return response
|
||||
}
|
||||
|
||||
export const updatePath = async (pathData) => {
|
||||
const requestData = transformToBackendFormat(pathData)
|
||||
const response = await put('/lifePath/update', requestData)
|
||||
return response
|
||||
}
|
||||
|
||||
export const deletePath = async (id) => {
|
||||
const response = await del('/lifePath/delete', { id })
|
||||
return response
|
||||
}
|
||||
|
||||
const transformToBackendFormat = (frontendData) => {
|
||||
const {
|
||||
id,
|
||||
scriptId,
|
||||
title,
|
||||
description,
|
||||
content,
|
||||
status = 'active',
|
||||
progress = 0
|
||||
} = frontendData
|
||||
|
||||
let steps = []
|
||||
if (content) {
|
||||
const stepMatches = content.match(/(\d+)\.\s*([^::]+)[::]\s*([^\n]+)/g)
|
||||
if (stepMatches) {
|
||||
steps = stepMatches.map((match, index) => {
|
||||
const parts = match.match(/(\d+)\.\s*([^::]+)[::]\s*(.+)/)
|
||||
return {
|
||||
phase: `阶段${index + 1}`,
|
||||
time: parts?.[2]?.trim() || '',
|
||||
content: parts?.[3]?.trim() || match,
|
||||
action: '',
|
||||
resources: '',
|
||||
habit: ''
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const lines = content.split('\n').filter(line => line.trim())
|
||||
steps = lines.map((line, index) => ({
|
||||
phase: `阶段${index + 1}`,
|
||||
time: '',
|
||||
content: line.trim(),
|
||||
action: '',
|
||||
resources: '',
|
||||
habit: ''
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
scriptId,
|
||||
title: title || '实现路径',
|
||||
description,
|
||||
steps,
|
||||
status,
|
||||
progress
|
||||
}
|
||||
}
|
||||
|
||||
export const transformToFrontendFormat = (backendData) => {
|
||||
if (!backendData) return null
|
||||
|
||||
const {
|
||||
id,
|
||||
userId,
|
||||
scriptId,
|
||||
title,
|
||||
description,
|
||||
steps,
|
||||
status,
|
||||
progress,
|
||||
createTime
|
||||
} = backendData
|
||||
|
||||
let content = ''
|
||||
if (Array.isArray(steps) && steps.length > 0) {
|
||||
content = steps.map((step, index) => {
|
||||
const phase = step.phase || `阶段${index + 1}`
|
||||
const time = step.time ? `(${step.time})` : ''
|
||||
return `${index + 1}. ${phase}${time}:${step.content || ''}`
|
||||
}).join('\n')
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
userId,
|
||||
scriptId,
|
||||
title: title || '实现路径',
|
||||
description: description || '',
|
||||
content,
|
||||
steps: steps || [],
|
||||
status: status || 'active',
|
||||
progress: progress || 0,
|
||||
createTime
|
||||
}
|
||||
}
|
||||
|
||||
export const transformListToFrontend = (backendList) => {
|
||||
if (!Array.isArray(backendList)) return []
|
||||
return backendList.map(transformToFrontendFormat)
|
||||
}
|
||||
|
||||
export default {
|
||||
getPathList,
|
||||
getPathPage,
|
||||
getPathByScriptId,
|
||||
getPathById,
|
||||
createPath,
|
||||
updatePath,
|
||||
deletePath,
|
||||
transformToFrontendFormat,
|
||||
transformListToFrontend
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import { getEnvValue, isDev } from '../config/env.js'
|
||||
|
||||
const API_BASE_URL = getEnvValue('API_BASE_URL')
|
||||
|
||||
/**
|
||||
* 请求拦截处理
|
||||
* 自动添加token到请求头
|
||||
*/
|
||||
const getHeaders = () => {
|
||||
const token = uni.getStorageSync('access_token')
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一请求方法
|
||||
* @param {Object} options - 请求配置
|
||||
* @returns {Promise} 请求Promise
|
||||
*/
|
||||
const request = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${API_BASE_URL}${options.url}`,
|
||||
method: options.method || 'GET',
|
||||
data: options.data,
|
||||
header: getHeaders(),
|
||||
timeout: 30000,
|
||||
success: (res) => {
|
||||
const { data, statusCode } = res
|
||||
|
||||
// 处理401未授权
|
||||
if (statusCode === 401) {
|
||||
uni.removeStorageSync('access_token')
|
||||
uni.removeStorageSync('refresh_token')
|
||||
uni.redirectTo({ url: '/pages/login/index' })
|
||||
reject(new Error('登录已过期,请重新登录'))
|
||||
return
|
||||
}
|
||||
|
||||
// 后端返回格式: { code, message, data }
|
||||
if (data.code === 200 || data.code === 0) {
|
||||
resolve(data)
|
||||
} else {
|
||||
reject(new Error(data.message || '请求失败'))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error(err.errMsg || '网络错误'))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* GET请求
|
||||
*/
|
||||
export const get = (url, params = {}) => {
|
||||
// 构建查询字符串
|
||||
const queryString = Object.keys(params)
|
||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
|
||||
.join('&')
|
||||
const fullUrl = queryString ? `${url}?${queryString}` : url
|
||||
|
||||
return request({ url: fullUrl, method: 'GET' })
|
||||
}
|
||||
|
||||
/**
|
||||
* POST请求
|
||||
*/
|
||||
export const post = (url, data = {}) => {
|
||||
return request({ url, method: 'POST', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT请求
|
||||
*/
|
||||
export const put = (url, data = {}) => {
|
||||
return request({ url, method: 'PUT', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE请求
|
||||
*/
|
||||
export const del = (url, params = {}) => {
|
||||
const queryString = Object.keys(params)
|
||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
|
||||
.join('&')
|
||||
const fullUrl = queryString ? `${url}?${queryString}` : url
|
||||
|
||||
return request({ url: fullUrl, method: 'DELETE' })
|
||||
}
|
||||
|
||||
export default {
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
del
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
/**
|
||||
* 用户档案服务
|
||||
* 处理用户档案的增删改查
|
||||
* 与Web端(life-script/src/services/userProfile.js)保持一致
|
||||
*/
|
||||
|
||||
import { get, post, put } from './request.js'
|
||||
|
||||
/**
|
||||
* 获取当前用户档案
|
||||
* @returns {Promise<Object|null>} 用户档案
|
||||
*/
|
||||
export const getCurrentProfile = async () => {
|
||||
const response = await get('/user-profile/me')
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户档案
|
||||
* @param {Object} profileData - 档案数据
|
||||
* @returns {Promise<Object>} 创建的档案
|
||||
*/
|
||||
export const createProfile = async (profileData) => {
|
||||
// 转换前端数据格式为后端格式
|
||||
const requestData = transformToBackendFormat(profileData)
|
||||
const response = await post('/user-profile/create', requestData)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户档案
|
||||
* @param {Object} profileData - 档案数据(必须包含id)
|
||||
* @returns {Promise<Object>} 更新后的档案
|
||||
*/
|
||||
export const updateProfile = async (profileData) => {
|
||||
const requestData = transformToBackendFormat(profileData)
|
||||
const response = await put('/user-profile/update', requestData)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 将前端数据格式转换为后端格式
|
||||
* @param {Object} frontendData - 前端数据
|
||||
* @returns {Object} 后端格式数据
|
||||
*/
|
||||
const transformToBackendFormat = (frontendData) => {
|
||||
const {
|
||||
id,
|
||||
nickname,
|
||||
gender,
|
||||
zodiac,
|
||||
profession,
|
||||
mbti,
|
||||
hobbies,
|
||||
childhood,
|
||||
joy,
|
||||
low,
|
||||
future
|
||||
} = frontendData
|
||||
|
||||
return {
|
||||
id,
|
||||
nickname,
|
||||
gender,
|
||||
zodiac,
|
||||
profession,
|
||||
mbti,
|
||||
// 兴趣爱好转为JSON字符串
|
||||
hobbies: Array.isArray(hobbies) ? JSON.stringify(hobbies) : hobbies,
|
||||
// 童年经历
|
||||
childhoodDate: childhood?.date || null,
|
||||
childhoodContent: childhood?.text || null,
|
||||
// 高光时刻(对应前端的joy)
|
||||
peakDate: joy?.date || null,
|
||||
peakContent: joy?.text || null,
|
||||
// 低谷时期(对应前端的low)
|
||||
valleyDate: low?.date || null,
|
||||
valleyContent: low?.text || null,
|
||||
// 未来期许
|
||||
futureVision: future?.vision || null,
|
||||
// 理想生活状态
|
||||
idealLife: future?.ideal || null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将后端数据格式转换为前端格式
|
||||
* @param {Object} backendData - 后端数据
|
||||
* @returns {Object} 前端格式数据
|
||||
*/
|
||||
export const transformToFrontendFormat = (backendData) => {
|
||||
if (!backendData) return null
|
||||
|
||||
const {
|
||||
id,
|
||||
userId,
|
||||
nickname,
|
||||
gender,
|
||||
zodiac,
|
||||
profession,
|
||||
mbti,
|
||||
hobbies,
|
||||
childhoodDate,
|
||||
childhoodContent,
|
||||
peakDate,
|
||||
peakContent,
|
||||
valleyDate,
|
||||
valleyContent,
|
||||
futureVision,
|
||||
idealLife
|
||||
} = backendData
|
||||
|
||||
return {
|
||||
id,
|
||||
userId,
|
||||
nickname: nickname || '',
|
||||
gender: gender || '',
|
||||
zodiac: zodiac || '',
|
||||
profession: profession || '',
|
||||
mbti: mbti || '',
|
||||
// 兴趣爱好从JSON字符串解析
|
||||
hobbies: hobbies ? (typeof hobbies === 'string' ? JSON.parse(hobbies) : hobbies) : [],
|
||||
// 童年经历
|
||||
childhood: {
|
||||
date: childhoodDate || '',
|
||||
text: childhoodContent || ''
|
||||
},
|
||||
// 高光时刻
|
||||
joy: {
|
||||
date: peakDate || '',
|
||||
text: peakContent || ''
|
||||
},
|
||||
// 低谷时期
|
||||
low: {
|
||||
date: valleyDate || '',
|
||||
text: valleyContent || ''
|
||||
},
|
||||
// 未来期许
|
||||
future: {
|
||||
vision: futureVision || '',
|
||||
ideal: idealLife || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getCurrentProfile,
|
||||
createProfile,
|
||||
updateProfile,
|
||||
transformToFrontendFormat
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M25.9966 29.8365C24.4459 31.3668 22.7527 31.1252 21.1229 30.4003C19.3981 29.6593 17.8157 29.6271 15.996 30.4003C13.7174 31.3991 12.5148 31.1091 11.1539 29.8365C3.43193 21.7336 4.57124 9.39407 13.3376 8.94301C15.4738 9.05578 16.9612 10.1351 18.2113 10.2317C20.0785 9.84512 21.8666 8.73359 23.8604 8.87858C26.2498 9.07188 28.0537 10.0384 29.2405 11.7782C24.3035 14.7906 25.4744 21.4114 30 23.264C29.098 25.6803 27.9271 28.0806 25.9808 29.8526L25.9966 29.8365ZM18.0531 8.84636C17.8157 5.25403 20.6798 2.28996 23.9712 2C24.43 6.15614 20.2684 9.24908 18.0531 8.84636Z" fill="#090909"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 693 B |
@@ -1,10 +0,0 @@
|
||||
<svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_435_546)">
|
||||
<path d="M15.5 3.75C13.2907 3.74977 11.1652 4.59569 9.56028 6.11395C7.95534 7.6322 6.99282 9.70748 6.87049 11.9134L6.66974 15.5296C6.37016 16.168 6.08818 16.8145 5.82412 17.4684C4.11912 21.6938 3.44812 25.4063 4.32812 25.7624C4.78874 25.948 5.58349 25.1766 6.48137 23.7934C6.84279 25.6833 7.78945 27.4116 9.18737 28.7337C7.77524 29.215 6.56112 29.8722 6.56112 30.5625C6.56112 31.2624 9.97112 31.2541 12.3897 31.25H12.3911C13.146 31.2472 13.7799 31.239 14.2886 31.1716C15.0918 31.278 15.9055 31.278 16.7086 31.1716C17.2174 31.2404 17.854 31.2473 18.6089 31.2486C21.0289 31.2541 24.4375 31.2624 24.4375 30.5625C24.4375 29.8722 23.2234 29.2164 21.8126 28.7337C23.2083 27.414 24.1543 25.6893 24.5172 23.803C25.411 25.1807 26.2044 25.948 26.6622 25.7624C27.5422 25.4063 26.874 21.6924 25.1662 17.4684C24.9055 16.8216 24.6271 16.1819 24.3316 15.5503L24.1295 11.9134C24.0072 9.70748 23.0446 7.6322 21.4397 6.11395C19.8348 4.59569 17.7093 3.74977 15.5 3.75Z" fill="#007AFF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_435_546">
|
||||
<rect width="33" height="33" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 941 B |
@@ -1,7 +0,0 @@
|
||||
Reserved auth/login assets (copy your images here):
|
||||
- login_bg.png
|
||||
- login_illustration.png
|
||||
- icon_eye_on.png
|
||||
- icon_eye_off.png
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M25.0958 12.8883C25.7133 12.8883 26.315 12.9358 26.9008 13.015C25.8242 8.3125 20.8842 4.75 14.9467 4.75C8.2175 4.75 2.755 9.31 2.755 14.9308C2.755 18.1767 4.57584 21.0425 7.41 22.9108L5.81084 26.125L10.1808 24.2408C11.115 24.5733 12.0967 24.8425 13.1417 24.985C12.9992 24.3675 12.92 23.7342 12.92 23.0692C12.9042 17.4642 18.3667 12.8883 25.0958 12.8883ZM19 9.32583C19.1996 9.32583 19.3973 9.36515 19.5817 9.44154C19.7661 9.51792 19.9337 9.62989 20.0748 9.77103C20.216 9.91218 20.3279 10.0797 20.4043 10.2642C20.4807 10.4486 20.52 10.6462 20.52 10.8458C20.52 11.0454 20.4807 11.2431 20.4043 11.4275C20.3279 11.6119 20.216 11.7795 20.0748 11.9206C19.9337 12.0618 19.7661 12.1737 19.5817 12.2501C19.3973 12.3265 19.1996 12.3658 19 12.3658C18.5969 12.3658 18.2103 12.2057 17.9252 11.9206C17.6401 11.6356 17.48 11.249 17.48 10.8458C17.48 10.4427 17.6401 10.0561 17.9252 9.77103C18.2103 9.48598 18.5969 9.32583 19 9.32583ZM10.8775 12.3817C10.4744 12.3817 10.0878 12.2215 9.8027 11.9365C9.51765 11.6514 9.35751 11.2648 9.35751 10.8617C9.35751 10.4585 9.51765 10.0719 9.8027 9.78686C10.0878 9.50181 10.4744 9.34167 10.8775 9.34167C11.2806 9.34167 11.6673 9.50181 11.9523 9.78686C12.2374 10.0719 12.3975 10.4585 12.3975 10.8617C12.3975 11.2648 12.2374 11.6514 11.9523 11.9365C11.6673 12.2215 11.2806 12.3817 10.8775 12.3817Z" fill="#34C759"/>
|
||||
<path d="M35.245 23.0691C35.245 18.5725 30.7008 14.9308 25.0958 14.9308C19.4908 14.9308 14.9467 18.5725 14.9467 23.0691C14.9467 27.5658 19.4908 31.2075 25.0958 31.2075C26.0142 31.2075 26.9008 31.0808 27.74 30.8908L33.2183 33.25L31.3183 29.45C33.6933 27.9616 35.245 25.6816 35.245 23.0691ZM22.04 22.5625C21.7394 22.5625 21.4455 22.4733 21.1955 22.3063C20.9456 22.1393 20.7507 21.9019 20.6357 21.6241C20.5206 21.3464 20.4905 21.0408 20.5492 20.7459C20.6078 20.4511 20.7526 20.1802 20.9652 19.9676C21.1778 19.7551 21.4486 19.6103 21.7435 19.5517C22.0383 19.493 22.3439 19.5231 22.6217 19.6382C22.8994 19.7532 23.1368 19.948 23.3038 20.198C23.4708 20.4479 23.56 20.7418 23.56 21.0425C23.5758 21.8816 22.8792 22.5625 22.04 22.5625ZM28.1358 22.5625C27.7327 22.5625 27.3461 22.4023 27.061 22.1173C26.776 21.8322 26.6158 21.4456 26.6158 21.0425C26.6158 20.6393 26.776 20.2527 27.061 19.9676C27.3461 19.6826 27.7327 19.5225 28.1358 19.5225C28.539 19.5225 28.9256 19.6826 29.2106 19.9676C29.4957 20.2527 29.6558 20.6393 29.6558 21.0425C29.6558 21.4456 29.4957 21.8322 29.2106 22.1173C28.9256 22.4023 28.539 22.5625 28.1358 22.5625Z" fill="#34C759"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 18.75L6.14001 16.6919C5.0395 16.1066 4.11919 15.2325 3.4779 14.1637C2.83661 13.0948 2.49854 11.8715 2.5 10.625V2.5C2.5 2.16848 2.6317 1.85054 2.86612 1.61612C3.10054 1.3817 3.41848 1.25 3.75 1.25H16.25C16.5815 1.25 16.8995 1.3817 17.1339 1.61612C17.3683 1.85054 17.5 2.16848 17.5 2.5V10.625C17.5015 11.8715 17.1634 13.0948 16.5221 14.1637C15.8808 15.2325 14.9605 16.1066 13.86 16.6919L10 18.75ZM3.75 2.5V10.625C3.74931 11.6448 4.02618 12.6456 4.55093 13.52C5.07568 14.3945 5.82853 15.1096 6.72875 15.5887L10 17.3331L13.2713 15.5894C14.1716 15.1102 14.9245 14.3949 15.4492 13.5204C15.974 12.6458 16.2508 11.6449 16.25 10.625V2.5H3.75Z" fill="#FE904B" fill-opacity="0.68"/>
|
||||
<path d="M10 15.7981V3.75H15V10.5031C15 11.2953 14.7848 12.0726 14.3776 12.7521C13.9703 13.4316 13.3861 13.9878 12.6875 14.3612L10 15.7981Z" fill="#FE904B" fill-opacity="0.68"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 966 B |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 195 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="13" height="21" viewBox="0 0 13 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.6667 0.083313H2.33333C1.7808 0.083313 1.25089 0.302806 0.860194 0.693507C0.469493 1.08421 0.25 1.61411 0.25 2.16665V18.8333C0.25 19.3858 0.469493 19.9158 0.860194 20.3065C1.25089 20.6972 1.7808 20.9166 2.33333 20.9166H10.6667C11.2192 20.9166 11.7491 20.6972 12.1398 20.3065C12.5305 19.9158 12.75 19.3858 12.75 18.8333V2.16665C12.75 1.61411 12.5305 1.08421 12.1398 0.693507C11.7491 0.302806 11.2192 0.083313 10.6667 0.083313ZM7.54167 19.875H5.45833V18.8333H7.54167V19.875ZM10.6667 17.7916H2.33333V3.20831H10.6667V17.7916Z" fill="#FE904B" fill-opacity="0.68"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 675 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="14" height="19" viewBox="0 0 14 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 5C2 2.238 4.238 0 7 0C9.762 0 12 2.238 12 5V8H12.4C13.28 8 14 8.72 14 9.6V16.6C14 17.92 12.92 19 11.6 19H2.4C1.08 19 0 17.92 0 16.6V9.6C0 8.72 0.72 8 1.6 8H2V5ZM10 5V8H4V5C4 3.342 5.342 2 7 2C8.658 2 10 3.342 10 5ZM7 10.25C6.60234 10.2496 6.21639 10.3846 5.90573 10.6329C5.59507 10.8811 5.37822 11.2278 5.2909 11.6157C5.20357 12.0037 5.25098 12.4098 5.42531 12.7672C5.59965 13.1246 5.89051 13.412 6.25 13.582V16C6.25 16.1989 6.32902 16.3897 6.46967 16.5303C6.61032 16.671 6.80109 16.75 7 16.75C7.19891 16.75 7.38968 16.671 7.53033 16.5303C7.67098 16.3897 7.75 16.1989 7.75 16V13.582C8.10949 13.412 8.40035 13.1246 8.57469 12.7672C8.74902 12.4098 8.79643 12.0037 8.7091 11.6157C8.62178 11.2278 8.40493 10.8811 8.09427 10.6329C7.78361 10.3846 7.39766 10.2496 7 10.25Z" fill="#FE904B" fill-opacity="0.68"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 959 B |
|
Before Width: | Height: | Size: 1.1 MiB |
@@ -1,7 +0,0 @@
|
||||
Reserved common assets (copy your images here with the exact names):
|
||||
- logo.png (already present, replace if needed)
|
||||
- icon_back.png
|
||||
- icon_close.png
|
||||
- icon_bluetooth.png
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 670 KiB |
|
Before Width: | Height: | Size: 79 KiB |
@@ -1,8 +0,0 @@
|
||||
Reserved device assets (copy your images here):
|
||||
- device_placeholder.png
|
||||
- bluetooth_on.png
|
||||
- bluetooth_off.png
|
||||
- search_illustration.png
|
||||
- device_connected.png
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
Reserved home assets (copy your images here):
|
||||
- banner.png
|
||||
- tab_home.png
|
||||
- tab_home_active.png
|
||||
- tab_profile.png
|
||||
- tab_profile_active.png
|
||||
- icon_settings.png
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<svg width="23" height="24" viewBox="0 0 23 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.5135 0H9.32432C8.66487 0 8.03242 0.252856 7.56611 0.702944C7.09981 1.15303 6.83784 1.76348 6.83784 2.4V4.8H9.32432V2.4H20.5135V21.6H9.32432V19.2H6.83784V21.6C6.83784 22.2365 7.09981 22.847 7.56611 23.2971C8.03242 23.7471 8.66487 24 9.32432 24H20.5135C21.173 24 21.8054 23.7471 22.2717 23.2971C22.738 22.847 23 22.2365 23 21.6V2.4C23 1.76348 22.738 1.15303 22.2717 0.702944C21.8054 0.252856 21.173 0 20.5135 0Z" fill="#FF8D28" fill-opacity="0.41"/>
|
||||
<path d="M7.96922 16.308L6.21624 18L2.86102e-05 12L6.21624 6L7.96922 7.692L4.76165 10.8H16.7838V13.2H4.76165L7.96922 16.308Z" fill="#FF8D28" fill-opacity="0.41"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 727 B |
@@ -1,6 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
<path d="M3.05273 8C3.05273 5.172 3.05273 3.757 3.978 2.879C4.90221 2 6.39168 2 9.36852 2H15.6843C18.6612 2 20.1506 2 21.0748 2.879C22.0001 3.757 22.0001 5.172 22.0001 8V16C22.0001 18.828 22.0001 20.243 21.0748 21.121C20.1506 22 18.6612 22 15.6843 22H9.36852C6.39168 22 4.90221 22 3.978 21.121C3.05273 20.243 3.05273 18.828 3.05273 16V8Z" stroke="black" stroke-width="1.5"/>
|
||||
<path opacity="0.5" d="M8.31579 2.5V22M2 12H4.10526M2 16H4.10526M2 8H4.10526" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M12 6.5H17.2632M12 10H17.2632" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 758 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.9999 3.33337C29.2049 3.33337 36.6666 10.795 36.6666 20C36.6666 29.205 29.2049 36.6667 19.9999 36.6667C17.3399 36.6704 14.718 36.0348 12.3549 34.8134L5.97827 36.5917C5.6225 36.691 5.24674 36.6939 4.88947 36.6002C4.53221 36.5064 4.20629 36.3194 3.94512 36.0582C3.68394 35.797 3.49689 35.4711 3.40314 35.1138C3.3094 34.7566 3.31231 34.3808 3.4116 34.025L5.1916 27.6534C3.96719 25.2883 3.3298 22.6633 3.33327 20C3.33327 10.795 10.7949 3.33337 19.9999 3.33337ZM19.9999 5.83337C16.2427 5.83337 12.6394 7.32593 9.98259 9.98269C7.32582 12.6395 5.83327 16.2428 5.83327 20C5.83327 22.45 6.45493 24.805 7.6216 26.895L7.8716 27.345L6.01827 33.985L12.6633 32.1317L13.1133 32.3817C15.0039 33.4329 17.1102 34.0371 19.2705 34.1481C21.4309 34.259 23.5879 33.8738 25.5763 33.0219C27.5647 32.17 29.3316 30.874 30.7415 29.2334C32.1514 27.5928 33.1669 25.6511 33.71 23.5572C34.2532 21.4633 34.3096 19.2728 33.875 17.1537C33.4404 15.0346 32.5262 13.0432 31.2027 11.3321C29.8791 9.62111 28.1813 8.23591 26.2394 7.28275C24.2975 6.32959 22.1631 5.83379 19.9999 5.83337Z" fill="#FFA629"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="39" height="39" viewBox="0 0 39 39" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.5 33.2C20.5765 33.2 23.527 32.02 25.7024 29.9196C27.8779 27.8192 29.1 24.9704 29.1 22C29.1 19.0296 27.8779 16.1808 25.7024 14.0804C23.527 11.98 20.5765 10.8 17.5 10.8C14.4235 10.8 11.473 11.98 9.29756 14.0804C7.12214 16.1808 5.9 19.0296 5.9 22C5.9 24.9704 7.12214 27.8192 9.29756 29.9196C11.473 32.02 14.4235 33.2 17.5 33.2ZM17.5 8C19.4042 8 21.2897 8.36212 23.0489 9.06569C24.8081 9.76925 26.4066 10.8005 27.753 12.1005C29.0995 13.4005 30.1676 14.9439 30.8963 16.6424C31.6249 18.341 32 20.1615 32 22C32 25.713 30.4723 29.274 27.753 31.8995C25.0338 34.525 21.3456 36 17.5 36C9.4815 36 3 29.7 3 22C3 18.287 4.52767 14.726 7.24695 12.1005C9.96623 9.475 13.6544 8 17.5 8ZM18.225 15V22.35L24.75 26.088L23.6625 27.81L16.05 23.4V15H18.225Z" fill="#FE904B" fill-opacity="0.5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 887 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.2732 7.39199L7.80658 9.36533C10.0385 11.0562 12.2143 12.8198 14.3306 14.6533C15.1839 15.3933 15.2532 16.692 14.4839 17.5187C12.6326 19.5102 10.7169 21.441 8.73991 23.308L8.82391 23.448C11.0826 21.6893 13.3096 19.8904 15.5039 18.052C15.8789 17.7351 16.3573 17.5671 16.848 17.5802C17.3388 17.5932 17.8076 17.7863 18.1652 18.1227C19.916 19.7728 21.619 21.4731 23.2719 23.2213L23.9892 22.7413C22.5472 20.8286 21.0506 18.9576 19.5012 17.1307C19.1682 16.7376 18.9991 16.2316 19.0289 15.7173C19.0586 15.2031 19.2851 14.7199 19.6612 14.368C21.2248 12.9126 22.7275 11.3931 24.1652 9.81333L22.6199 7.82666C21.3479 9.50548 20.0346 11.1526 18.6812 12.7667C18.4989 12.9836 18.2726 13.1593 18.0172 13.2821C17.7619 13.4049 17.4833 13.4721 17.2001 13.4792C16.9168 13.4862 16.6353 13.433 16.3741 13.3231C16.113 13.2131 15.8781 13.0489 15.6852 12.8413C13.9386 10.9688 12.1337 9.1515 10.2732 7.39199ZM9.05991 4.94666C9.43538 4.64684 9.90624 4.49221 10.3864 4.51106C10.8665 4.52991 11.3238 4.72099 11.6746 5.04933C13.5447 6.80123 15.3607 8.61008 17.1199 10.4733C18.4561 8.86038 19.752 7.21454 21.0066 5.53733C21.1913 5.2905 21.4304 5.08959 21.7054 4.95019C21.9803 4.81079 22.2837 4.73666 22.592 4.73357C22.9003 4.73047 23.2051 4.79849 23.4828 4.93234C23.7605 5.06619 24.0036 5.26226 24.1932 5.50533L26.6132 8.61866C27.1999 9.37333 27.1746 10.448 26.5306 11.172C25.0668 12.8064 23.5352 14.3789 21.9399 15.8853C23.5327 17.783 25.0707 19.726 26.5519 21.712C27.2386 22.6333 27.0052 23.936 26.0572 24.568L24.2826 25.752C23.8881 26.0153 23.4128 26.1298 22.9417 26.075C22.4706 26.0203 22.0343 25.8 21.7106 25.4533C20.1113 23.7461 18.4635 22.085 16.7692 20.472C14.5106 22.3653 12.1959 24.1947 9.87324 26.0133C9.64999 26.1878 9.39221 26.3129 9.11697 26.3803C8.84173 26.4476 8.55532 26.4557 8.27671 26.4041C7.9981 26.3524 7.73366 26.2421 7.5009 26.0805C7.26814 25.9189 7.07237 25.7096 6.92658 25.4667L6.17724 24.2213C5.94132 23.8287 5.8473 23.3671 5.91088 22.9135C5.97447 22.4599 6.19181 22.0419 6.52658 21.7293C8.42732 19.9498 10.2709 18.1102 12.0546 16.2133C9.93732 14.399 7.76011 12.6558 5.52658 10.9867C5.28145 10.8035 5.08153 10.5666 4.94214 10.2942C4.80275 10.0218 4.7276 9.72104 4.72245 9.41508C4.7173 9.10911 4.78229 8.80603 4.91243 8.52907C5.04258 8.25212 5.23442 8.00865 5.47324 7.81733L9.05991 4.94666Z" fill="#FFA629" fill-opacity="0.59"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="114" height="114" viewBox="0 0 114 114" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.9643 11C17.4651 11 13.1501 12.6012 9.96872 15.4512C6.7873 18.3013 5 22.1668 5 26.1975V62.6713C5 66.702 6.7873 70.5675 9.96872 73.4176C13.1501 76.2676 17.4651 77.8688 21.9643 77.8688H25.3571V83.9174C25.3564 85.0402 25.7028 86.1413 26.3579 87.0985C27.0131 88.0557 27.9513 88.8317 29.0687 89.3404C30.1861 89.8491 31.4388 90.0706 32.6882 89.9803C33.9375 89.8901 35.1345 89.4917 36.1464 88.8292L52.8868 77.8688H83.0357C87.5349 77.8688 91.8499 76.2676 95.0313 73.4176C98.2127 70.5675 100 66.702 100 62.6713V26.1975C100 22.1668 98.2127 18.3013 95.0313 15.4512C91.8499 12.6012 87.5349 11 83.0357 11H21.9643ZM11.7857 26.1975C11.7857 23.7791 12.8581 21.4598 14.7669 19.7497C16.6758 18.0397 19.2648 17.079 21.9643 17.079H83.0357C85.7352 17.079 88.3242 18.0397 90.2331 19.7497C92.1419 21.4598 93.2143 23.7791 93.2143 26.1975V62.6713C93.2143 65.0897 92.1419 67.409 90.2331 69.1191C88.3242 70.8291 85.7352 71.7898 83.0357 71.7898H50.6679L32.1429 83.9174V71.7898H21.9643C19.2648 71.7898 16.6758 70.8291 14.7669 69.1191C12.8581 67.409 11.7857 65.0897 11.7857 62.6713V26.1975Z" fill="#FFCD29"/>
|
||||
<circle cx="92" cy="20" r="15" fill="#FF9500"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,46 +0,0 @@
|
||||
<svg width="53" height="53" viewBox="0 0 53 53" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.8896 45.7746C12.8896 43.9941 15.3782 37.7045 15.3782 37.7045C17.7756 36.9634 35.0875 37.5927 37.7251 37.5927C37.7251 37.5927 40.1101 42.6981 40.1101 45.7746C40.1101 48.8511 32.7895 51.3438 26.5247 51.3438C20.26 51.3438 12.8896 48.8511 12.8896 45.7746Z" fill="#8B5738"/>
|
||||
<path d="M26.5249 43.5801C32.7107 43.5801 37.7252 41.4018 37.7252 38.7148C37.7252 36.0278 32.7107 33.8496 26.5249 33.8496C20.339 33.8496 15.3245 36.0278 15.3245 38.7148C15.3245 41.4018 20.339 43.5801 26.5249 43.5801Z" fill="#FFB17A"/>
|
||||
<path d="M16.0119 43.9527C17.2623 44.6649 19.2416 44.6111 20.7777 44.8885C22.7321 45.2446 23.8377 46.7352 27.6056 46.669C28.9513 46.6441 32.0899 46.4909 31.916 46.4951C31.8084 46.2756 31.1955 46.0645 30.003 45.8781C28.9099 45.7042 28.1232 45.7208 26.8562 45.7208C25.8873 45.7208 24.5581 45.4309 23.2166 44.6939C22.6037 44.3585 21.6514 43.9859 20.7198 43.7954C19.2871 43.5014 18.2892 43.812 16.2893 42.8638C15.3328 42.4124 14.5585 41.4766 14.3763 40.3877L13.9043 41.7458C14.5254 42.8845 15.0927 43.431 16.0119 43.9527ZM36.6198 42.8389C33.4149 44.8761 30.707 44.6484 30.707 44.6484C30.4792 44.7974 32.3466 45.1991 33.7337 44.9672C36.4583 44.5117 38.1145 43.1453 39.1911 41.3152C39.042 40.8514 38.7149 39.9736 38.7149 39.9736C38.4044 40.7231 38.0441 41.9321 36.6198 42.8389Z" fill="#CC8552"/>
|
||||
<path d="M30.8519 48.6192C29.0797 48.9422 27.382 48.3915 25.6388 47.9816C23.2497 47.4226 18.7612 47.5013 18.7612 47.5013C18.4921 47.6338 20.8771 48.4992 22.0365 48.5985C23.0923 48.6896 24.2103 48.5985 25.2123 49.0084C26.6243 49.584 27.9907 49.8738 29.3571 49.8738C31.9533 49.8738 35.3444 48.3087 36.1519 46.2756C36.1519 46.2756 34.6695 47.1452 33.5184 47.729C32.682 48.1555 31.7794 48.4536 30.8519 48.6192Z" fill="#A86D44"/>
|
||||
<path d="M14.8317 46.1597C15.1836 46.7228 15.5438 47.2445 16.0159 47.7124C16.815 48.5074 17.8253 48.967 18.4754 48.8511C18.4754 48.8511 16.1732 46.9919 16.4175 46.5075C16.5417 46.259 21.32 46.6524 21.32 46.6524C21.32 46.6524 19.8708 45.8905 18.4588 45.6172C17.5438 45.4392 15.8709 45.7953 14.3472 44.7229C13.5067 44.1308 13.3783 43.4186 13.3783 43.4186C13.3783 43.4186 13.076 44.4496 13.0139 44.843C13.8959 45.7663 14.7157 45.9733 14.8317 46.1597ZM39.046 44.1142C38.2096 44.8637 36.8888 46.3791 36.8888 47.1617C36.8888 47.1617 38.3711 45.7249 39.369 45.0665C39.6174 44.9051 39.7955 44.698 39.9901 44.4786C39.9901 44.4786 39.8907 43.7374 39.7334 43.2115C39.6713 43.5635 39.4187 43.783 39.046 44.1142Z" fill="#A86D44"/>
|
||||
<path d="M26.5 42.0356C37.6504 42.0356 46.6897 32.9964 46.6897 21.8459C46.6897 10.6955 37.6504 1.65625 26.5 1.65625C15.3495 1.65625 6.3103 10.6955 6.3103 21.8459C6.3103 32.9964 15.3495 42.0356 26.5 42.0356Z" fill="url(#paint0_radial_739_1078)"/>
|
||||
<path d="M26.5 42.0356C37.6504 42.0356 46.6897 32.9964 46.6897 21.8459C46.6897 10.6955 37.6504 1.65625 26.5 1.65625C15.3495 1.65625 6.3103 10.6955 6.3103 21.8459C6.3103 32.9964 15.3495 42.0356 26.5 42.0356Z" fill="url(#paint1_radial_739_1078)"/>
|
||||
<path opacity="0.7" d="M19.0096 9.26257C20.8895 9.42406 24.7403 10.8319 21.9371 14.4425C20.0945 16.8151 17.7923 19.022 16.9766 20.9309C14.6206 26.4503 20.5251 28.7028 22.3594 29.2784C23.9494 29.7794 27.0135 30.8973 27.8747 32.3259C31.1996 37.8329 21.0882 39.3359 21.0882 39.3359C21.0882 39.3359 28.5289 42.048 34.6571 37.9033C36.9758 36.334 39.0171 32.6819 37.0214 29.3943C36.0525 27.8001 33.3818 26.3634 32.7275 25.8499C29.9285 23.6388 28.2018 21.3532 29.5931 18.7901C30.1562 17.755 31.3321 17.2167 32.4708 16.902C35.6591 16.0201 39.6382 16.1774 40.6775 12.2397C41.576 8.84437 35.0173 1.25874 24.8438 2.49265C20.6203 3.00195 16.8193 4.76999 13.7096 7.67671C7.97074 13.0471 8.46761 17.49 8.46761 17.49C8.46761 17.49 12.2066 8.67046 19.0096 9.26257Z" fill="url(#paint2_linear_739_1078)"/>
|
||||
<path opacity="0.39" d="M41.311 10.0286C39.87 7.93344 37.8784 6.26891 35.7791 4.83211C32.4293 2.54235 28.0982 2.35602 28.0982 2.35602C33.4148 4.89836 32.8186 8.26883 31.0836 10.6787C28.9719 13.6144 24.7361 15.5977 23.6305 19.1711C22.9846 21.258 23.3821 23.4525 25.4151 24.7858C27.4482 26.1191 34.7646 29.6593 35.0172 33.8496C35.2077 37.0048 32.6985 38.9923 32.6985 38.9923C35.6342 38.3587 41.812 34.0359 39.9818 29.5889C39.0957 27.4358 35.0669 26.0073 37.0958 23.5519C37.8494 22.6368 43.1121 22.1482 43.6049 16.8234C43.8326 14.3348 42.4911 11.7428 41.311 10.0286Z" fill="url(#paint3_linear_739_1078)"/>
|
||||
<path opacity="0.85" d="M10.2315 25.5311C10.2315 25.5311 7.20883 17.5149 13.4156 10.0907C18.5293 3.97087 23.1461 6.44696 22.4339 9.05555C21.7176 11.6641 19.4941 12.4674 16.1443 15.0884C11.718 18.55 10.2315 25.5311 10.2315 25.5311Z" fill="url(#paint4_linear_739_1078)"/>
|
||||
<path d="M38.4912 14.397C37.6838 13.9953 37.0379 13.4902 36.7563 12.633L35.5597 8.67047L34.363 12.633C34.0815 13.486 33.4314 13.9953 32.6281 14.397L30.6282 15.2623L32.6861 16.1153C33.4935 16.517 34.0815 17.0677 34.3672 17.9206L35.5638 21.8501L36.7604 17.9206C37.042 17.0677 37.6341 16.517 38.4415 16.1153L40.4994 15.2623L38.4912 14.397Z" fill="url(#paint5_linear_739_1078)"/>
|
||||
<path d="M34.4956 28.7235C33.6882 28.3219 33.0423 27.8167 32.7607 26.9596L31.5641 22.997L30.3674 26.9596C30.0858 27.8126 29.4358 28.3219 28.6325 28.7235L26.6326 29.5889L28.6905 30.4419C29.4979 30.8435 30.0858 31.3942 30.3716 32.2472L31.5682 36.1767L32.7648 32.2472C33.0464 31.3942 33.6385 30.8435 34.4459 30.4419L36.5038 29.5889L34.4956 28.7235Z" fill="url(#paint6_linear_739_1078)"/>
|
||||
<path d="M44.0314 22.347C43.4227 22.0447 42.9341 21.6596 42.7188 21.0178L41.8162 18.0241L40.9135 21.0178C40.6982 21.6637 40.2096 22.0447 39.6009 22.347L38.0938 23.0012L39.6465 23.6471C40.2552 23.9494 40.7023 24.3634 40.9135 25.0094L41.8162 27.9741L42.7188 25.0094C42.9341 24.3634 43.3772 23.9494 43.9859 23.6471L45.5386 23.0012L44.0314 22.347Z" fill="white"/>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_739_1078" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(33.0529 9.1753) rotate(-3.714) scale(31.5449)">
|
||||
<stop offset="0.104" stop-color="#CE93D8"/>
|
||||
<stop offset="1" stop-color="#AB47BC"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial_739_1078" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(22.0898 19.1028) scale(28.7314)">
|
||||
<stop offset="0.28" stop-color="#81D4FA" stop-opacity="0"/>
|
||||
<stop offset="0.964" stop-color="#81D4FA" stop-opacity="0.9"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint2_linear_739_1078" x1="30.9264" y1="4.41017" x2="14.8823" y2="45.9638" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#673AB7"/>
|
||||
<stop offset="0.937" stop-color="#673AB7" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_739_1078" x1="33.4885" y1="10.3242" x2="33.4885" y2="44.7506" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.235" stop-color="#1D44B3"/>
|
||||
<stop offset="0.884" stop-color="#2044B3" stop-opacity="0.074"/>
|
||||
<stop offset="0.936" stop-color="#2144B3" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_739_1078" x1="13.6256" y1="6.82476" x2="17.2398" y2="20.2969" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.227" stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_739_1078" x1="35.5559" y1="11.9913" x2="35.5559" y2="21.6915" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.261" stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_739_1078" x1="31.5603" y1="24.0823" x2="31.5603" y2="35.291" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.5 KiB |
@@ -1,11 +0,0 @@
|
||||
<svg width="45" height="45" viewBox="0 0 45 45" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_838_293)">
|
||||
<path d="M35 37.5H7.5V10H24.025L26.525 7.5H7.5C6.83696 7.5 6.20107 7.76339 5.73223 8.23223C5.26339 8.70107 5 9.33696 5 10V37.5C5 38.163 5.26339 38.7989 5.73223 39.2678C6.20107 39.7366 6.83696 40 7.5 40H35C35.663 40 36.2989 39.7366 36.7678 39.2678C37.2366 38.7989 37.5 38.163 37.5 37.5V18.75L35 21.25V37.5Z" fill="#FFA629"/>
|
||||
<path d="M41.9126 7.29999L37.7001 3.08749C37.5131 2.90003 37.291 2.7513 37.0465 2.64981C36.802 2.54833 36.5398 2.49609 36.2751 2.49609C36.0103 2.49609 35.7482 2.54833 35.5036 2.64981C35.2591 2.7513 35.037 2.90003 34.8501 3.08749L17.7126 20.325L16.3251 26.3375C16.266 26.6289 16.2722 26.9299 16.3433 27.2186C16.4144 27.5074 16.5486 27.7768 16.7362 28.0075C16.9239 28.2382 17.1603 28.4244 17.4286 28.5528C17.6968 28.6812 17.9902 28.7485 18.2876 28.75C18.4413 28.7668 18.5964 28.7668 18.7501 28.75L24.8126 27.4125L41.9126 10.15C42.1 9.96304 42.2488 9.74094 42.3502 9.49642C42.4517 9.25189 42.504 8.98974 42.504 8.72499C42.504 8.46024 42.4517 8.19809 42.3502 7.95357C42.2488 7.70904 42.1 7.48694 41.9126 7.29999ZM23.5126 25.1L18.9376 26.1125L20.0001 21.575L32.9001 8.58749L36.4251 12.1125L23.5126 25.1ZM37.8376 10.7L34.3126 7.17499L36.2501 5.19999L39.8001 8.74999L37.8376 10.7Z" fill="#FFA629"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_838_293">
|
||||
<rect width="45" height="45" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.99948 1C6.19836 1 6.38909 1.07524 6.52972 1.20918C6.67034 1.34311 6.74935 1.52476 6.74935 1.71417V2.42834H9.74883V1.71417C9.74883 1.52476 9.82783 1.34311 9.96846 1.20918C10.1091 1.07524 10.2998 1 10.4987 1C10.6976 1 10.8883 1.07524 11.0289 1.20918C11.1696 1.34311 11.2486 1.52476 11.2486 1.71417V2.42834H14.248V1.71417C14.248 1.52476 14.327 1.34311 14.4677 1.20918C14.6083 1.07524 14.799 1 14.9979 1C15.1968 1 15.3875 1.07524 15.5282 1.20918C15.6688 1.34311 15.7478 1.52476 15.7478 1.71417V2.42834C16.3444 2.42834 16.9166 2.65406 17.3385 3.05586C17.7604 3.45766 17.9974 4.00261 17.9974 4.57084V8.1931C17.4598 8.28228 16.9484 8.48001 16.4977 8.77301V4.57084C16.4977 4.38143 16.4187 4.19978 16.278 4.06585C16.1374 3.93192 15.9467 3.85667 15.7478 3.85667H5.24961C5.05073 3.85667 4.86 3.93192 4.71937 4.06585C4.57874 4.19978 4.49974 4.38143 4.49974 4.57084V18.8542C4.49974 19.0436 4.57874 19.2253 4.71937 19.3592C4.86 19.4931 5.05073 19.5684 5.24961 19.5684H7.56971V19.5712C7.44286 20.0446 7.4786 20.5441 7.67169 20.9967H5.24961C4.65298 20.9967 4.08078 20.771 3.6589 20.3692C3.23701 19.9674 3 19.4224 3 18.8542V4.57084C3 4.00261 3.23701 3.45766 3.6589 3.05586C4.08078 2.65406 4.65298 2.42834 5.24961 2.42834V1.71417C5.24961 1.52476 5.32861 1.34311 5.46924 1.20918C5.60987 1.07524 5.8006 1 5.99948 1ZM13.4982 10.9984C13.6586 10.9984 13.8071 11.0469 13.9301 11.1283L12.5653 12.4267H7.49922C7.30034 12.4267 7.10961 12.3514 6.96898 12.2175C6.82835 12.0836 6.74935 11.9019 6.74935 11.7125C6.74935 11.5231 6.82835 11.3415 6.96898 11.2075C7.10961 11.0736 7.30034 10.9984 7.49922 10.9984H13.4982ZM8.99296 15.9047C8.96929 15.7328 8.88076 15.5749 8.74389 15.4606C8.60701 15.3463 8.43114 15.2833 8.24909 15.2834H7.49922C7.30034 15.2834 7.10961 15.3586 6.96898 15.4925C6.82835 15.6265 6.74935 15.8081 6.74935 15.9975C6.74935 16.1869 6.82835 16.3686 6.96898 16.5025C7.10961 16.6365 7.30034 16.7117 7.49922 16.7117H8.24909C8.32907 16.7118 8.40856 16.6998 8.48455 16.676C8.63052 16.4056 8.79999 16.1485 8.99296 15.9047ZM7.49922 6.71335C7.30034 6.71335 7.10961 6.78859 6.96898 6.92252C6.82835 7.05645 6.74935 7.2381 6.74935 7.42751C6.74935 7.61692 6.82835 7.79857 6.96898 7.93251C7.10961 8.06644 7.30034 8.14168 7.49922 8.14168H13.4982C13.6971 8.14168 13.8878 8.06644 14.0284 7.93251C14.169 7.79857 14.248 7.61692 14.248 7.42751C14.248 7.2381 14.169 7.05645 14.0284 6.92252C13.8878 6.78859 13.6971 6.71335 13.4982 6.71335H7.49922ZM17.0046 10.2199C17.4435 9.80189 18.0388 9.56704 18.6595 9.56704C19.2803 9.56704 19.8756 9.80189 20.3145 10.2199C20.7534 10.6379 21 11.2049 21 11.7961C21 12.3873 20.7534 12.9542 20.3145 13.3723L13.8821 19.497C13.3475 20.0042 12.6791 20.3649 11.9474 20.5411L10.1313 20.9739C9.97793 21.0105 9.81726 21.0086 9.66491 20.9685C9.51256 20.9283 9.37373 20.8513 9.26196 20.7448C9.15018 20.6384 9.06928 20.5061 9.02713 20.361C8.98498 20.2159 8.98302 20.0629 9.02145 19.9169L9.47737 18.1886C9.66034 17.4901 10.0383 16.8531 10.5722 16.346L17.0046 10.2199Z" fill="#FFA629"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.0 KiB |
@@ -1,14 +0,0 @@
|
||||
<svg width="32" height="31" viewBox="0 0 32 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_35_520)">
|
||||
<path d="M15.7574 0C7.28215 0 0.411621 6.87053 0.411621 15.3458C0.411621 23.821 7.28215 30.6915 15.7574 30.6915C24.2326 30.6915 31.1032 23.821 31.1032 15.3458C31.1032 6.87053 24.2326 0 15.7574 0ZM19.1536 8.78553C20.9771 8.75704 22.6262 9.74837 23.4079 11.4384C24.131 13.6726 23.5259 15.9456 22.2506 17.6585C21.4088 18.8201 20.4028 19.8219 19.3742 20.6972C18.428 21.578 16.31 23.3211 15.7486 23.3695C15.2525 23.2746 14.6956 22.7127 14.3017 22.424C12.0885 20.7415 9.7065 18.6964 8.49848 16.4594C7.48564 14.3117 7.48379 11.6545 9.06014 10.008C11.1041 8.16528 14.1854 8.52595 15.7486 10.4511C16.1685 9.90646 16.6849 9.47752 17.2976 9.16546C17.9186 8.91763 18.5458 8.79504 19.1536 8.78553Z" fill="url(#paint0_linear_35_520)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_35_520" x1="15.7574" y1="0" x2="15.7574" y2="30.6915" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFA629"/>
|
||||
<stop offset="1" stop-color="#FB784C"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_35_520">
|
||||
<rect width="30.6915" height="30.6915" fill="white" transform="translate(0.411621)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 986 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
@@ -1,5 +0,0 @@
|
||||
<svg width="41" height="41" viewBox="0 0 41 41" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.55212 9.95834C5.55212 9.61853 5.68711 9.29264 5.92739 9.05236C6.16767 8.81208 6.49357 8.67709 6.83337 8.67709H34.1667C34.5065 8.67709 34.8324 8.81208 35.0727 9.05236C35.313 9.29264 35.448 9.61853 35.448 9.95834C35.448 10.2982 35.313 10.624 35.0727 10.8643C34.8324 11.1046 34.5065 11.2396 34.1667 11.2396H6.83337C6.49357 11.2396 6.16767 11.1046 5.92739 10.8643C5.68711 10.624 5.55212 10.2982 5.55212 9.95834Z" fill="#FFA629"/>
|
||||
<path opacity="0.7" d="M5.55212 20.5C5.55212 20.1602 5.68711 19.8343 5.92739 19.594C6.16767 19.3537 6.49357 19.2188 6.83337 19.2188H25.625C25.9648 19.2188 26.2907 19.3537 26.531 19.594C26.7713 19.8343 26.9063 20.1602 26.9063 20.5C26.9063 20.8398 26.7713 21.1657 26.531 21.406C26.2907 21.6463 25.9648 21.7812 25.625 21.7812H6.83337C6.49357 21.7812 6.16767 21.6463 5.92739 21.406C5.68711 21.1657 5.55212 20.8398 5.55212 20.5Z" fill="#FFA629"/>
|
||||
<path opacity="0.4" d="M5.55212 31.0417C5.55212 30.7018 5.68711 30.376 5.92739 30.1357C6.16767 29.8954 6.49357 29.7604 6.83337 29.7604H15.375C15.7148 29.7604 16.0407 29.8954 16.281 30.1357C16.5213 30.376 16.6563 30.7018 16.6563 31.0417C16.6563 31.3815 16.5213 31.7074 16.281 31.9476C16.0407 32.1879 15.7148 32.3229 15.375 32.3229H6.83337C6.49357 32.3229 6.16767 32.1879 5.92739 31.9476C5.68711 31.7074 5.55212 31.3815 5.55212 31.0417Z" fill="#FFA629"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.0002 14.1667C20.1298 14.1667 22.6668 11.6296 22.6668 8.50001C22.6668 5.3704 20.1298 2.83334 17.0002 2.83334C13.8705 2.83334 11.3335 5.3704 11.3335 8.50001C11.3335 11.6296 13.8705 14.1667 17.0002 14.1667Z" fill="#FFA629"/>
|
||||
<path opacity="0.5" d="M28.3334 24.7917C28.3334 28.3121 28.3334 31.1667 17.0001 31.1667C5.66675 31.1667 5.66675 28.3121 5.66675 24.7917C5.66675 21.2713 10.7412 18.4167 17.0001 18.4167C23.2589 18.4167 28.3334 21.2713 28.3334 24.7917Z" fill="#FFA629"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 589 B |
@@ -1,14 +0,0 @@
|
||||
<svg width="80" height="81" viewBox="0 0 80 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_f_153_663)">
|
||||
<ellipse cx="40" cy="40.5" rx="36" ry="36.5" fill="#FECE4B"/>
|
||||
<path d="M40 7C58.1865 7 73 21.9593 73 40.5C73 59.0407 58.1865 74 40 74C21.8135 74 7 59.0407 7 40.5C7 21.9593 21.8135 7 40 7Z" stroke="white" stroke-opacity="0.27" stroke-width="6"/>
|
||||
</g>
|
||||
<path d="M38.1034 42.3967H26.7229V38.6032H38.1034V27.2227H41.897V38.6032H53.2775V42.3967H41.897V53.7773H38.1034V42.3967Z" fill="white" fill-opacity="0.91"/>
|
||||
<defs>
|
||||
<filter id="filter0_f_153_663" x="0" y="0" width="80" height="81" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="2" result="effect1_foregroundBlur_153_663"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 919 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="39" height="39" viewBox="0 0 39 39" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.3438 16.25C22.3438 16.9227 22.8898 17.4687 23.5625 17.4687H24.6838L21.4126 20.7399C21.3749 20.7777 21.3301 20.8077 21.2807 20.8282C21.2314 20.8487 21.1784 20.8592 21.125 20.8592C21.0716 20.8592 21.0187 20.8487 20.9693 20.8282C20.9199 20.8077 20.8751 20.7777 20.8374 20.7399L18.2601 18.1626C17.7269 17.6298 17.0039 17.3304 16.25 17.3304C15.4961 17.3304 14.7731 17.6298 14.2399 18.1626L10.5138 21.8888C10.394 22.0003 10.298 22.1349 10.2314 22.2844C10.1647 22.4339 10.1289 22.5953 10.126 22.7589C10.1232 22.9225 10.1533 23.0851 10.2146 23.2368C10.2759 23.3886 10.3671 23.5265 10.4828 23.6422C10.5985 23.7579 10.7364 23.8492 10.8882 23.9104C11.0399 23.9717 11.2025 24.0018 11.3661 23.999C11.5297 23.9961 11.6911 23.9603 11.8406 23.8936C11.9901 23.827 12.1247 23.731 12.2363 23.6112L15.9624 19.8851C16.0001 19.8473 16.0449 19.8173 16.0943 19.7968C16.1437 19.7763 16.1966 19.7658 16.25 19.7658C16.3034 19.7658 16.3564 19.7763 16.4057 19.7968C16.4551 19.8173 16.4999 19.8473 16.5376 19.8851L19.1149 22.4624C19.6481 22.9952 20.3711 23.2946 21.125 23.2946C21.8789 23.2946 22.6019 22.9952 23.1351 22.4624L26.4063 19.1929V20.3125C26.4063 20.6357 26.5347 20.9457 26.7632 21.1743C26.9918 21.4028 27.3018 21.5312 27.625 21.5312C27.9482 21.5312 28.2582 21.4028 28.4868 21.1743C28.7154 20.9457 28.8438 20.6357 28.8438 20.3125V16.25C28.8438 15.9268 28.7154 15.6168 28.4868 15.3882C28.2582 15.1597 27.9482 15.0312 27.625 15.0312H23.5625C23.2393 15.0312 22.9293 15.1597 22.7007 15.3882C22.4722 15.6168 22.3438 15.9268 22.3438 16.25Z" fill="#FFA629"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.9168 12.0833L13.2918 15.7083M24.5148 3.66245C24.6294 3.62289 24.7529 3.61637 24.8711 3.64366C24.9893 3.67095 25.0974 3.73094 25.1831 3.81677C25.2688 3.9026 25.3287 4.01081 25.3558 4.12904C25.3829 4.24726 25.3762 4.37073 25.3365 4.48533L18.1783 24.9424C18.1355 25.0647 18.0569 25.1713 17.9527 25.2484C17.8485 25.3254 17.7236 25.3694 17.5942 25.3746C17.4647 25.3799 17.3367 25.346 17.2266 25.2776C17.1166 25.2091 17.0297 25.1092 16.9772 24.9907L13.0876 16.24C13.0221 16.0945 12.9056 15.978 12.7601 15.9125L4.00937 12.0217C3.89131 11.969 3.79177 11.8821 3.7236 11.7722C3.65544 11.6624 3.62178 11.5346 3.62699 11.4054C3.63219 11.2762 3.67602 11.1515 3.7528 11.0475C3.82958 10.9435 3.93578 10.8649 4.0577 10.8218L24.5148 3.66245Z" stroke="#FFA629" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 927 B |
@@ -1,26 +0,0 @@
|
||||
<svg width="81" height="80" viewBox="0 0 81 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_f_612_521)">
|
||||
<ellipse cx="5.76768" cy="3.91141" rx="5.76768" ry="3.91141" transform="matrix(0.994377 0.105902 -0.109566 0.99398 13.9968 41.0223)" fill="#FE904B"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_f_612_521)">
|
||||
<ellipse cx="5.76768" cy="3.91141" rx="5.76768" ry="3.91141" transform="matrix(-0.994377 0.105902 0.109566 0.99398 63.5996 41.0223)" fill="#FE904B"/>
|
||||
</g>
|
||||
<path d="M39 3.5C52.9073 3.5 62.5245 7.55998 68.6895 14.1172C74.8857 20.7078 78 30.2512 78 42C78 53.6196 74.465 62.0849 68.1357 67.6924C61.7467 73.3528 52.0631 76.5 39 76.5C16.7941 76.5 3.5 59.8138 3.5 42C3.5 32.9304 6.44414 23.2398 12.3418 15.877C18.1765 8.59281 26.9561 3.5 39 3.5Z" fill="white" fill-opacity="0.21" stroke="white" stroke-width="6"/>
|
||||
<ellipse cx="26.4678" cy="37.3073" rx="5.76882" ry="8.01676" fill="#115CB0"/>
|
||||
<ellipse cx="51.9302" cy="37.3073" rx="5.76882" ry="8.01676" fill="#115CB0"/>
|
||||
<ellipse cx="26.4678" cy="35.7431" rx="1.79032" ry="1.75978" fill="white"/>
|
||||
<ellipse cx="51.9302" cy="35.7431" rx="1.79032" ry="1.75978" fill="white"/>
|
||||
<path d="M30.6453 56.6648C30.6453 56.6648 34.027 60.3799 38.8012 60.3799C43.5754 60.3799 46.9571 56.6648 46.9571 56.6648" stroke="#115CB0" stroke-width="3"/>
|
||||
<defs>
|
||||
<filter id="filter0_f_612_521" x="7.552" y="35.5848" width="23.5029" height="19.8723" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="3" result="effect1_foregroundBlur_612_521"/>
|
||||
</filter>
|
||||
<filter id="filter1_f_612_521" x="46.5415" y="35.5848" width="23.5029" height="19.8723" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="3" result="effect1_foregroundBlur_612_521"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="39" height="38" viewBox="0 0 39 38" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M31.1678 24.6647L31.8169 26.2661C32.4534 27.8386 33.7348 29.0814 35.3513 29.7017L36.9997 30.3324L35.3513 30.9651C33.7348 31.5834 32.4555 32.8282 31.8169 34.3987L31.1678 36.0001L30.5165 34.3987C30.2007 33.6218 29.7241 32.9162 29.1161 32.3255C28.5082 31.7349 27.7818 31.2719 26.9821 30.9651L25.3337 30.3324L26.9821 29.7017C27.782 29.3947 28.5085 28.9313 29.1165 28.3403C29.7244 27.7494 30.201 27.0433 30.5165 26.2661L31.1678 24.6647Z" stroke="#FFA629" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M14.4233 2.45754C14.593 1.84749 15.4841 1.84749 15.6538 2.45754L15.9805 3.63642C16.6532 6.06015 17.9689 8.26993 19.796 10.045C21.6231 11.82 23.8978 13.0982 26.3927 13.7517L27.6061 14.0691C28.2341 14.234 28.2341 15.0996 27.6061 15.2645L26.3927 15.5819C23.8978 16.2354 21.6231 17.5136 19.796 19.2886C17.9689 21.0636 16.6532 23.2734 15.9805 25.6972L15.6538 26.876C15.4841 27.4861 14.593 27.4861 14.4233 26.876L14.0966 25.6972C13.4239 23.2734 12.1082 21.0636 10.2811 19.2886C8.45395 17.5136 6.17931 16.2354 3.68445 15.5819L2.47097 15.2645C1.84301 15.0996 1.84301 14.234 2.47097 14.0691L3.68445 13.7517C6.17931 13.0982 8.45395 11.82 10.2811 10.045C12.1082 8.26993 13.4239 6.06015 14.0966 3.63642L14.4233 2.45754Z" stroke="#FFA629" stroke-width="3" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.8749 14.2545L17.304 14.833C17.456 14.9832 17.6611 15.0674 17.8749 15.0674C18.0886 15.0674 18.2937 14.9832 18.4458 14.833L17.8749 14.2545ZM12.729 16.315C12.729 15.2642 13.353 14.378 14.2132 14.0043C15.0333 13.6479 16.174 13.7183 17.304 14.833L18.4458 13.6771C16.8934 12.1442 15.055 11.8669 13.5665 12.5136C12.8311 12.8393 12.2066 13.3722 11.7693 14.0471C11.332 14.722 11.1008 15.5097 11.104 16.3139L12.729 16.315ZM15.5782 22.5009C15.8794 22.7262 16.226 22.9862 16.5835 23.1834C16.941 23.3827 17.3809 23.5625 17.8749 23.5625V21.9375C17.773 21.9375 17.617 21.8985 17.3733 21.7642C17.0875 21.5943 16.8132 21.4057 16.5521 21.1998L15.5782 22.5009ZM20.1715 22.5009C21.0003 21.8801 22.128 21.1218 23.0099 20.1771C23.9199 19.2032 24.6457 17.9595 24.6457 16.315H23.0207C23.0207 17.4244 22.5484 18.291 21.8225 19.0678C21.0696 19.8749 20.1239 20.5064 19.1976 21.1998L20.1715 22.5009ZM19.1976 21.1998C18.9365 21.4053 18.6622 21.5935 18.3765 21.7631C18.1327 21.8985 17.9767 21.9375 17.8749 21.9375V23.5625C18.3689 23.5625 18.8076 23.3827 19.1662 23.1834C19.5237 22.9862 19.8693 22.7262 20.1715 22.5009L19.1976 21.1998ZM24.6457 16.3139C24.6488 15.2413 24.2374 14.2099 23.4974 13.4334L22.322 14.5557C22.752 15.0053 23.0207 15.6249 23.0207 16.315L24.6457 16.3139ZM23.4974 13.4334C23.1284 13.0439 22.6847 12.7318 22.1927 12.518C21.7007 12.3041 21.1706 12.1917 20.6341 12.1875C19.5031 12.1821 18.3364 12.6577 17.304 13.6771L18.4458 14.833C19.215 14.0747 19.982 13.8093 20.6265 13.8125C21.2798 13.8158 21.8821 14.0953 22.322 14.5557L23.4974 13.4334ZM16.5521 21.1998L16.082 20.8531L15.1221 22.1639L15.5771 22.4998L16.5521 21.1998ZM16.082 20.8531C14.3205 19.5629 12.729 18.3539 12.729 16.3139H11.104C11.104 19.3115 13.5166 20.9885 15.1221 22.1639L16.082 20.8531Z" fill="black"/>
|
||||
<path opacity="0.5" d="M12.9998 5.76003L12.4018 6.31036C12.4779 6.39309 12.5704 6.45914 12.6733 6.50431C12.7762 6.54949 12.8874 6.57281 12.9998 6.57281C13.1122 6.57281 13.2234 6.54949 13.3264 6.50431C13.4293 6.45914 13.5217 6.39309 13.5978 6.31036L12.9998 5.76003ZM10.2254 20.3233C6.91692 17.5954 2.979 14.9207 2.979 9.87886H1.354C1.354 15.8545 6.125 19.0493 9.19192 21.5767L10.2254 20.3233ZM2.979 9.87886C2.979 7.40886 4.31367 5.35703 6.1055 4.50119C7.8345 3.67461 10.168 3.88369 12.4018 6.31036L13.5978 5.20969C10.9567 2.33994 7.8735 1.85461 5.40459 3.03328C2.9985 4.18486 1.354 6.83903 1.354 9.87886H2.979ZM9.19192 21.5767C9.74659 22.0339 10.3424 22.5214 10.9469 22.8908C11.5493 23.2591 12.2426 23.5624 12.9998 23.5624V21.9374C12.6738 21.9374 12.2848 21.8042 11.793 21.5041C11.3033 21.2051 10.7953 20.7924 10.2254 20.3233L9.19192 21.5767ZM24.6457 9.87994C24.6457 6.84119 23.0012 4.18594 20.594 3.03544C18.1262 1.85569 15.043 2.34211 12.4018 5.21078L13.5978 6.31144C15.8317 3.88478 18.1652 3.67569 19.8942 4.50228C21.686 5.35811 23.0207 7.40994 23.0207 9.87994H24.6457ZM23.6425 14.3454C24.3114 12.9523 24.6544 11.4253 24.6457 9.87994H23.0207C23.0292 11.1828 22.7402 12.4703 22.1757 13.6445L23.6425 14.3454ZM15.1004 20.8704C14.2045 21.5724 13.548 21.9374 12.9998 21.9374V23.5624C14.1731 23.5624 15.2369 22.8279 16.1036 22.1487L15.1004 20.8704Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="19" height="20" viewBox="0 0 19 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.1673 12.9988L15.4826 13.8466C15.7918 14.6791 16.4141 15.3371 17.1993 15.6655L18 15.9994L17.1993 16.3343C16.4141 16.6617 15.7928 17.3207 15.4826 18.1521L15.1673 18.9999L14.851 18.1521C14.6976 17.7408 14.4661 17.3673 14.1708 17.0546C13.8755 16.7419 13.5227 16.4968 13.1343 16.3343L12.3336 15.9994L13.1343 15.6655C13.5228 15.503 13.8756 15.2577 14.171 14.9448C14.4663 14.6319 14.6977 14.2581 14.851 13.8466L15.1673 12.9988Z" stroke="#FFA629" stroke-width="2" stroke-linejoin="round"/>
|
||||
<path d="M7.03424 1.24223C7.11667 0.919258 7.54945 0.919258 7.63189 1.24223L7.79058 1.86634C8.11733 3.14949 8.75637 4.31937 9.64385 5.2591C10.5313 6.19883 11.6362 6.87549 12.848 7.22148L13.4374 7.38951C13.7424 7.4768 13.7424 7.93506 13.4374 8.02235L12.848 8.19038C11.6362 8.53637 10.5313 9.21304 9.64385 10.1528C8.75637 11.0925 8.11733 12.2624 7.79058 13.5455L7.63189 14.1696C7.54945 14.4926 7.11667 14.4926 7.03424 14.1696L6.87555 13.5455C6.5488 12.2624 5.90976 11.0925 5.02228 10.1528C4.1348 9.21304 3.02997 8.53637 1.81817 8.19038L1.22876 8.02235C0.923748 7.93506 0.923748 7.4768 1.22876 7.38951L1.81817 7.22148C3.02997 6.87549 4.1348 6.19883 5.02228 5.2591C5.90976 4.31937 6.5488 3.14949 6.87555 1.86634L7.03424 1.24223Z" stroke="#FFA629" stroke-width="2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,9 +0,0 @@
|
||||
<svg width="46" height="46" viewBox="0 0 46 46" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.9999 7.66666C31.4524 7.66666 38.3333 14.5475 38.3333 23C38.3333 31.4525 31.4524 38.3333 22.9999 38.3333C14.5474 38.3333 7.66659 31.4525 7.66659 23C7.66659 14.5475 14.5474 7.66666 22.9999 7.66666ZM22.9999 3.83333C12.4583 3.83333 3.83325 12.4583 3.83325 23C3.83325 33.5417 12.4583 42.1667 22.9999 42.1667C33.5416 42.1667 42.1666 33.5417 42.1666 23C42.1666 12.4583 33.5416 3.83333 22.9999 3.83333ZM16.1574 27.6767L13.5508 26.5075C14.0874 25.4342 14.3749 24.2842 14.3749 23.0958C14.3749 21.8308 14.0874 20.6233 13.5508 19.4925L16.1574 18.3233C16.8858 19.8758 17.2499 21.4667 17.2499 23.0958C17.2499 24.5333 16.8858 26.0667 16.1574 27.6767ZM22.0991 30.5517L19.5883 29.3058C20.6041 27.14 21.0833 24.9167 21.0833 22.7317C21.0833 20.5658 20.6041 18.5725 19.5883 16.6942L22.0991 15.2567C23.3641 17.48 23.9583 19.9717 23.9583 22.7317C23.9583 25.5492 23.3641 28.1558 22.0991 30.5517ZM28.1366 33.2542L25.5108 31.9125C27.0249 28.9608 27.7916 26.0092 27.7916 23C27.7916 19.9908 27.0249 17.0008 25.5108 14.0108L28.1366 12.7458C29.8041 16.1575 30.6666 19.5883 30.6666 23C30.6666 26.4883 29.8041 29.9 28.1366 33.2542Z" fill="url(#paint0_linear_73_538)" fill-opacity="0.58"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_73_538" x1="22.9999" y1="3.83333" x2="22.9999" y2="42.1667" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FE904B"/>
|
||||
<stop offset="0.990385" stop-color="#98562D" stop-opacity="0.4"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 673 KiB |
@@ -1,6 +0,0 @@
|
||||
Reserved splash assets (copy your images here):
|
||||
- background.png
|
||||
- logo_lockup.png
|
||||
- loading_spinner.png
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 MiB |
@@ -1,192 +0,0 @@
|
||||
import { reactive, computed, readonly } from 'vue'
|
||||
import * as authService from '../services/auth.js'
|
||||
import * as userProfileService from '../services/userProfile.js'
|
||||
import * as lifeEventService from '../services/lifeEvent.js'
|
||||
import * as epicScriptService from '../services/epicScript.js'
|
||||
import * as lifePathService from '../services/lifePath.js'
|
||||
|
||||
const state = reactive({
|
||||
isLoggedIn: false,
|
||||
isLoading: false,
|
||||
currentStep: 1,
|
||||
userInfo: null,
|
||||
userProfile: null,
|
||||
events: [],
|
||||
scripts: [],
|
||||
paths: [],
|
||||
currentPath: null,
|
||||
registrationData: {
|
||||
nickname: '',
|
||||
gender: '',
|
||||
mbti: '',
|
||||
zodiac: '',
|
||||
profession: '',
|
||||
hobbies: [],
|
||||
childhood: { date: '', text: '' },
|
||||
joy: { date: '', text: '' },
|
||||
low: { date: '', text: '' },
|
||||
future: { vision: '', ideal: '' }
|
||||
}
|
||||
})
|
||||
|
||||
const hasProfile = computed(() => {
|
||||
return state.userProfile && state.userProfile.nickname
|
||||
})
|
||||
|
||||
const login = async (phone, smsCode) => {
|
||||
state.isLoading = true
|
||||
try {
|
||||
const res = await authService.login({ phone, smsCode })
|
||||
state.isLoggedIn = true
|
||||
await fetchUserProfile()
|
||||
return { success: true, hasProfile: hasProfile.value }
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message }
|
||||
} finally {
|
||||
state.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
await authService.logout()
|
||||
state.isLoggedIn = false
|
||||
state.userInfo = null
|
||||
state.userProfile = null
|
||||
}
|
||||
|
||||
const fetchUserProfile = async () => {
|
||||
try {
|
||||
const res = await userProfileService.getCurrentProfile()
|
||||
if (res.data) {
|
||||
state.userProfile = userProfileService.transformToFrontendFormat(res.data)
|
||||
Object.assign(state.registrationData, state.userProfile)
|
||||
}
|
||||
return res.data
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const saveUserProfile = async () => {
|
||||
state.isLoading = true
|
||||
try {
|
||||
const dataToSave = { ...state.registrationData }
|
||||
if (state.userProfile?.id) {
|
||||
await userProfileService.updateProfile({
|
||||
id: state.userProfile.id,
|
||||
...dataToSave
|
||||
})
|
||||
} else {
|
||||
await userProfileService.createProfile(dataToSave)
|
||||
}
|
||||
await fetchUserProfile()
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message }
|
||||
} finally {
|
||||
state.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateRegistration = (data) => {
|
||||
Object.assign(state.registrationData, data)
|
||||
}
|
||||
|
||||
const setCurrentStep = (step) => {
|
||||
state.currentStep = step
|
||||
}
|
||||
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const res = await lifeEventService.getEventList()
|
||||
state.events = lifeEventService.transformListToFrontend(res.data || [])
|
||||
return state.events
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const createEvent = async (eventData) => {
|
||||
try {
|
||||
await lifeEventService.createEvent(eventData)
|
||||
await fetchEvents()
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
const fetchScripts = async () => {
|
||||
try {
|
||||
const res = await epicScriptService.getScriptList()
|
||||
state.scripts = epicScriptService.transformListToFrontend(res.data || [])
|
||||
return state.scripts
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const createScript = async (scriptData) => {
|
||||
try {
|
||||
await epicScriptService.createScript(scriptData)
|
||||
await fetchScripts()
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
const selectScript = async (id) => {
|
||||
try {
|
||||
await epicScriptService.selectScript(id)
|
||||
await fetchScripts()
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
const fetchPaths = async () => {
|
||||
try {
|
||||
const res = await lifePathService.getPathList()
|
||||
state.paths = lifePathService.transformListToFrontend(res.data || [])
|
||||
return state.paths
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const setCurrentPath = (path) => {
|
||||
state.currentPath = path
|
||||
}
|
||||
|
||||
const initialize = async () => {
|
||||
const token = uni.getStorageSync('access_token')
|
||||
if (token) {
|
||||
state.isLoggedIn = true
|
||||
await fetchUserProfile()
|
||||
}
|
||||
}
|
||||
|
||||
export const useAppStore = () => {
|
||||
return readonly({
|
||||
...state,
|
||||
hasProfile,
|
||||
login,
|
||||
logout,
|
||||
fetchUserProfile,
|
||||
saveUserProfile,
|
||||
updateRegistration,
|
||||
setCurrentStep,
|
||||
fetchEvents,
|
||||
createEvent,
|
||||
fetchScripts,
|
||||
createScript,
|
||||
selectScript,
|
||||
fetchPaths,
|
||||
setCurrentPath,
|
||||
initialize
|
||||
})
|
||||
}
|
||||
|
||||
export default useAppStore
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* 这里是uni-app内置的常用样式变量
|
||||
*
|
||||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||
*
|
||||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||
*/
|
||||
|
||||
/* 颜色变量 */
|
||||
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #007aff;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
|
||||
/* 文字基本颜色 */
|
||||
$uni-text-color:#333;//基本色
|
||||
$uni-text-color-inverse:#fff;//反色
|
||||
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
|
||||
$uni-text-color-placeholder: #808080;
|
||||
$uni-text-color-disable:#c0c0c0;
|
||||
|
||||
/* 背景颜色 */
|
||||
$uni-bg-color:#ffffff;
|
||||
$uni-bg-color-grey:#f8f8f8;
|
||||
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
|
||||
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
|
||||
|
||||
/* 边框颜色 */
|
||||
$uni-border-color:#c8c7cc;
|
||||
|
||||
/* 尺寸变量 */
|
||||
|
||||
/* 文字尺寸 */
|
||||
$uni-font-size-sm:12px;
|
||||
$uni-font-size-base:14px;
|
||||
$uni-font-size-lg:16px;
|
||||
|
||||
/* 图片尺寸 */
|
||||
$uni-img-size-sm:20px;
|
||||
$uni-img-size-base:26px;
|
||||
$uni-img-size-lg:40px;
|
||||
|
||||
/* Border Radius */
|
||||
$uni-border-radius-sm: 2px;
|
||||
$uni-border-radius-base: 3px;
|
||||
$uni-border-radius-lg: 6px;
|
||||
$uni-border-radius-circle: 50%;
|
||||
|
||||
/* 水平间距 */
|
||||
$uni-spacing-row-sm: 5px;
|
||||
$uni-spacing-row-base: 10px;
|
||||
$uni-spacing-row-lg: 15px;
|
||||
|
||||
/* 垂直间距 */
|
||||
$uni-spacing-col-sm: 4px;
|
||||
$uni-spacing-col-base: 8px;
|
||||
$uni-spacing-col-lg: 12px;
|
||||
|
||||
/* 透明度 */
|
||||
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||
|
||||
/* 文章场景相关 */
|
||||
$uni-color-title: #2C405A; // 文章标题颜色
|
||||
$uni-font-size-title:20px;
|
||||
$uni-color-subtitle: #555555; // 二级标题颜色
|
||||
$uni-font-size-subtitle:26px;
|
||||
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
||||
$uni-font-size-paragraph:15px;
|
||||
@@ -1,104 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Life OS - 登录</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
:root {
|
||||
--bg-dark: #0F071A;
|
||||
--primary: #A855F7;
|
||||
--primary-light: #C084FC;
|
||||
--accent: #E879F9;
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(180deg, #0F071A 0%, #1A0B2E 50%, #0F071A 100%);
|
||||
min-height: 100vh;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
color: #F3E8FF;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: rgba(168, 85, 247, 0.05);
|
||||
backdrop-filter: blur(40px);
|
||||
border: 1px solid rgba(168, 85, 247, 0.15);
|
||||
border-radius: 48rpx;
|
||||
}
|
||||
|
||||
.glass-input {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24rpx;
|
||||
color: #F3E8FF;
|
||||
}
|
||||
|
||||
.glass-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #9333EA 0%, #7C3AED 100%);
|
||||
border-radius: 32rpx;
|
||||
box-shadow: 0 8px 32px rgba(168, 85, 247, 0.3);
|
||||
}
|
||||
|
||||
.aurora-top {
|
||||
position: fixed;
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
width: 120%;
|
||||
height: 60%;
|
||||
background: rgba(168, 85, 247, 0.08);
|
||||
filter: blur(120px);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.aurora-bottom {
|
||||
position: fixed;
|
||||
bottom: -10%;
|
||||
right: -10%;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
background: rgba(139, 92, 246, 0.05);
|
||||
filter: blur(100px);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="flex items-center justify-center p-6">
|
||||
<div class="aurora-top"></div>
|
||||
<div class="aurora-bottom"></div>
|
||||
|
||||
<div class="glass-card w-full max-w-md p-12 relative z-10">
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-4xl font-light text-white/90 mb-4 tracking-wider">欢迎回来</h1>
|
||||
<p class="text-white/40 italic">开启你的数字生命档案</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6 mb-8">
|
||||
<div>
|
||||
<label class="block text-sm text-white/60 mb-3 tracking-widest">手机号码</label>
|
||||
<input type="tel" placeholder="输入手机号" class="glass-input w-full h-14 px-6 text-center text-lg tracking-widest">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-white/60 mb-3 tracking-widest">验证码</label>
|
||||
<div class="flex gap-3">
|
||||
<input type="text" placeholder="六位验证码" class="glass-input flex-1 h-14 px-6 text-center">
|
||||
<button class="glass-input h-14 px-6 text-sm text-white/80 whitespace-nowrap">获取</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-primary w-full h-14 text-white font-semibold mb-6">开启旅程</button>
|
||||
|
||||
<p class="text-xs text-center text-white/25 leading-relaxed">
|
||||
登录即代表同意《用户协议》与《隐私政策》
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,60 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { resolve } from 'path'
|
||||
import uni from '@dcloudio/vite-plugin-uni'
|
||||
|
||||
const uniPlugin = uni.default || uni
|
||||
|
||||
const vueCompatPlugin = () => {
|
||||
const virtualId = '\0virtual:vue-compat'
|
||||
const publicId = 'virtual:vue-compat'
|
||||
return {
|
||||
name: 'vue-compat-uni',
|
||||
resolveId(id) {
|
||||
if (id === publicId) return virtualId
|
||||
if (id === 'vue') return virtualId
|
||||
return null
|
||||
},
|
||||
load(id) {
|
||||
if (id !== virtualId) return null
|
||||
return [
|
||||
"import * as VueRuntime from 'vue/dist/vue.runtime.esm-bundler.js'",
|
||||
"export * from 'vue/dist/vue.runtime.esm-bundler.js'",
|
||||
'export default VueRuntime',
|
||||
'export const injectHook = () => {}',
|
||||
'export const isInSSRComponentSetup = () => false'
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig(({ command }) => {
|
||||
const isMpWeixin = process.env.UNI_PLATFORM === 'mp-weixin'
|
||||
const inputDir = process.env.UNI_INPUT_DIR || '.'
|
||||
const mpEntry = resolve(inputDir, 'main.js')
|
||||
const outDir = isMpWeixin
|
||||
? (command === 'serve' ? 'unpackage/dist/dev/mp-weixin' : 'unpackage/dist/build/mp-weixin')
|
||||
: 'dist'
|
||||
return {
|
||||
envDir: __dirname,
|
||||
server: {
|
||||
watch: {
|
||||
usePolling: true,
|
||||
interval: 100
|
||||
},
|
||||
hmr: true
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
{ find: /^vue$/, replacement: 'virtual:vue-compat' }
|
||||
]
|
||||
},
|
||||
build: {
|
||||
outDir,
|
||||
emptyOutDir: false,
|
||||
rollupOptions: isMpWeixin
|
||||
? { input: mpEntry }
|
||||
: undefined
|
||||
},
|
||||
plugins: [vueCompatPlugin(), uniPlugin()]
|
||||
}
|
||||
})
|
||||
@@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -1,59 +0,0 @@
|
||||
# PncyssD 浏览器兼容性测试报告 (Browser Compatibility Report)
|
||||
|
||||
## 1. 测试环境与技术栈
|
||||
|
||||
### 1.1 技术基础
|
||||
本项目基于以下现代 Web 技术构建,天然具备良好的跨浏览器兼容性:
|
||||
- **框架**: React 19 (利用最新的 Fiber 架构和并发渲染特性)
|
||||
- **构建工具**: Vite (生成高度优化的 ES Modules 和兼容性 Polyfills)
|
||||
- **样式引擎**: Tailwind CSS v4 (自动处理厂商前缀 Vendor Prefixes)
|
||||
- **图标库**: Lucide React (SVG 矢量图标,无分辨率限制,全平台兼容)
|
||||
|
||||
### 1.2 目标浏览器
|
||||
根据项目需求,我们确保以下主流浏览器的最新两个主版本完全兼容:
|
||||
- Google Chrome (Desktop & Mobile)
|
||||
- Mozilla Firefox
|
||||
- Apple Safari (macOS & iOS)
|
||||
- Microsoft Edge
|
||||
|
||||
## 2. 兼容性验证点 (Verification Points)
|
||||
|
||||
### 2.1 CSS 特性支持
|
||||
- **Glassmorphism (backdrop-filter)**:
|
||||
- Chrome/Edge (90+): 原生支持,效果完美。
|
||||
- Safari (iOS/macOS): 原生支持 (`-webkit-backdrop-filter` 由 Tailwind 自动添加)。
|
||||
- Firefox: 最新版本已默认开启支持。
|
||||
- *回退方案*: 对于不支持的浏览器,Tailwind 配置了透明度回退,虽然没有模糊效果,但背景颜色依然可见,保证内容可读性。
|
||||
- **Grid & Flexbox Layout**:
|
||||
- 全面支持所有目标浏览器,用于复杂的仪表盘布局和卡片排列。
|
||||
- **CSS Variables**:
|
||||
- 用于定义主题色,现代浏览器均支持。
|
||||
|
||||
### 2.2 JavaScript / React 特性
|
||||
- **ES6+ 语法**: 通过 Vite/Babel 转译为 ES2015+,确保在旧版浏览器 (如 Chrome 60+) 也能运行核心逻辑。
|
||||
- **Hooks (useState, useEffect)**: React 核心特性,兼容所有支持 React 的环境。
|
||||
- **LocalStorage**: 用于数据持久化,所有现代浏览器均支持。
|
||||
|
||||
### 2.3 响应式设计 (Responsive Design)
|
||||
- **Breakpoints**:
|
||||
- Mobile (< 768px): 侧边栏自动折叠为汉堡菜单,布局转为单列。
|
||||
- Tablet (768px - 1024px): 网格布局自动调整列数。
|
||||
- Desktop (> 1024px): 完整的三栏/两栏布局。
|
||||
- **Touch Events**:
|
||||
- 针对 iOS/Android 优化了点击区域 (Tap Targets),确保按钮高度至少 44px。
|
||||
|
||||
## 3. 已知问题与解决方案
|
||||
| 问题 | 影响范围 | 解决方案 |
|
||||
| :--- | :--- | :--- |
|
||||
| `backdrop-filter` 性能 | 低端移动设备 | 减少大面积模糊区域,使用 `bg-black/80` 代替高模糊度以提升帧率。 |
|
||||
| 滚动条样式 | Windows (非 Webkit) | 使用标准 CSS scrollbar 属性配合 Webkit 伪类,确保 Firefox 和 Chrome 均有较好体验。 |
|
||||
| 字体渲染差异 | Windows vs macOS | 定义了系统字体栈 (System Font Stack),优先使用各平台最佳无衬线字体。 |
|
||||
|
||||
## 4. 测试结论
|
||||
代码库已通过静态分析和模拟环境测试。基于 Tailwind CSS 和 React 的标准化实现,PncyssD 设计系统在主流浏览器上表现一致,未发现阻塞性的兼容性问题。
|
||||
|
||||
建议在发布前进行真机测试,特别是针对 iOS Safari 的刘海屏适配 (SafeArea) 和低端安卓机的性能测试。
|
||||
|
||||
---
|
||||
*生成日期: 2025-12-21*
|
||||
*执行人: AI Assistant*
|
||||
@@ -1,16 +0,0 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
@@ -1,61 +0,0 @@
|
||||
# PncyssD 视觉一致性报告 (Visual Consistency Report)
|
||||
|
||||
## 1. 概述
|
||||
本报告详细说明了 Emotion Museum (course-web) 项目根据 PncyssD 原型进行的视觉重构工作。重构旨在确保全站风格统一、交互流畅,并符合现代 Glassmorphism(毛玻璃)设计美学。
|
||||
|
||||
## 2. 设计系统规范 (Design System)
|
||||
|
||||
### 2.1 色彩体系 (Color Palette)
|
||||
- **主色调 (Primary)**: `emerald-500` (#10b981) - 用于核心操作、高亮状态。
|
||||
- **背景色 (Background)**: Deep Sea / Dark Theme - 使用深色渐变背景,配合 `bg-black/20` 或 `bg-white/5` 实现层次感。
|
||||
- **文本颜色 (Typography)**:
|
||||
- 主要文本: `text-gray-100` (白色略带灰,减少视觉疲劳)
|
||||
- 次要文本: `text-gray-400` / `text-gray-500`
|
||||
- 强调文本: `text-primary` (绿色), `text-accent` (橙色/金色)
|
||||
|
||||
### 2.2 组件风格 (Component Styling)
|
||||
- **GlassCard (毛玻璃卡片)**:
|
||||
- 统一使用 `backdrop-blur-xl` 配合 `bg-white/5` 或 `bg-black/20`。
|
||||
- 边框使用 `border-white/10`,实现细腻的边缘光感。
|
||||
- 悬停效果: `hover:bg-white/10`,部分组件带有 `group-hover` 触发的光影流动效果。
|
||||
- **Input / Select / Textarea**:
|
||||
- 统一背景 `bg-white/5`,去除了默认边框,使用 `focus:ring` 进行聚焦反馈。
|
||||
- 占位符颜色统一为 `placeholder-gray-500`。
|
||||
- **Button**:
|
||||
- `primary`: 渐变背景或高亮背景,带阴影。
|
||||
- `outline`: 透明背景,带边框,悬停变色。
|
||||
- `ghost`: 纯文本交互,无背景。
|
||||
|
||||
## 3. 页面重构详情 (Page Refactoring Details)
|
||||
|
||||
### 3.1 登陆与引导 (Landing & Onboarding)
|
||||
- **LandingPage**: 重构为沉浸式全屏背景,CTA按钮使用 PncyssD 标准组件,添加了平滑滚动和淡入动画。
|
||||
- **LoginPage**: 采用了居中 GlassCard 布局,输入框样式统一,背景添加了动态光效。
|
||||
- **OnboardingPage**: 多步骤表单采用了统一的卡片容器,进度指示器风格与主色调保持一致。
|
||||
|
||||
### 3.2 核心功能区 (Dashboard)
|
||||
- **DashboardPage (Layout)**:
|
||||
- 将背景从浅色 (`#f8fafc`) 调整为深海主题 (`bg-deep-sea` + 径向渐变)。
|
||||
- 侧边栏 (Sidebar) 统一为毛玻璃效果,选中状态添加了发光边框和指示条。
|
||||
- **TimelineView (时空日记)**:
|
||||
- 日志卡片标准化为 GlassCard,日期和内容排版优化。
|
||||
- 输入区域使用 PncyssD Textarea,支持自动高度适应。
|
||||
- **ScriptView (剧本生成器)**:
|
||||
- 移除了硬编码的亮色背景和渐变。
|
||||
- 重新设计了 "主角设定" 卡片,使用图标和半透明背景增强视觉吸引力。
|
||||
- 剧本展示区采用了大字号标题和衬线字体 (Serif) 增强阅读体验,章节卡片添加了悬停高亮效果。
|
||||
- **PathView (实现路径)**:
|
||||
- 路径节点使用连接线和图标进行可视化。
|
||||
- 下拉选择框替换为 PncyssD Select 组件,确保交互一致性。
|
||||
|
||||
## 4. 动画与交互 (Animations & Interactions)
|
||||
- **Hover Effects**: 按钮、卡片在悬停时有轻微的上浮 (`-translate-y`) 和光影变化。
|
||||
- **Transitions**: 全局使用 `transition-all duration-300` 确保状态切换流畅。
|
||||
- **Loading States**: 按钮加载状态统一使用 `Loader` 图标旋转动画。
|
||||
|
||||
## 5. 结论
|
||||
经过全面重构,course-web 前端代码已完全符合 PncyssD 原型设计要求。所有页面均采用了统一的组件库和设计令牌 (Design Tokens),实现了像素级的视觉一致性。
|
||||
|
||||
---
|
||||
*生成日期: 2025-12-21*
|
||||
*执行人: AI Assistant*
|
||||
@@ -1,133 +0,0 @@
|
||||
#!/bin/bash
|
||||
# course-web 部署脚本
|
||||
# 功能:项目构建、文件传输、原子切换、历史版本管理、回滚支持
|
||||
|
||||
SERVER_IP="101.200.208.45"
|
||||
USERNAME="root"
|
||||
APP_NAME="course-web"
|
||||
DEPLOY_BASE="/data/www/course-web-deploy"
|
||||
RELEASES_DIR="${DEPLOY_BASE}/releases"
|
||||
LINK_PATH="/data/www/course-of-life"
|
||||
MAX_RELEASES=5
|
||||
|
||||
# 打印带颜色的信息
|
||||
function log_info() {
|
||||
echo -e "\033[32m[INFO] $1\033[0m"
|
||||
}
|
||||
|
||||
function log_error() {
|
||||
echo -e "\033[31m[ERROR] $1\033[0m"
|
||||
}
|
||||
|
||||
# 检查环境
|
||||
function check_env() {
|
||||
log_info "检查本地环境..."
|
||||
if ! command -v npm &> /dev/null; then
|
||||
log_error "未找到npm命令,请先安装Node.js"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v scp &> /dev/null; then
|
||||
log_error "未找到scp命令"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 构建项目
|
||||
function build_project() {
|
||||
log_info "开始构建项目..."
|
||||
# 清理旧构建
|
||||
rm -rf dist
|
||||
|
||||
# 安装依赖并构建
|
||||
# npm install # 视情况开启,为了速度暂时注释,假设依赖已安装
|
||||
if npm run build; then
|
||||
log_info "项目构建成功"
|
||||
else
|
||||
log_error "项目构建失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "dist" ]; then
|
||||
log_error "dist目录不存在"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 部署到服务器
|
||||
function deploy() {
|
||||
TIMESTAMP=$(date +%Y%m%d%H%M%S)
|
||||
RELEASE_PATH="${RELEASES_DIR}/${TIMESTAMP}"
|
||||
|
||||
log_info "准备部署版本: ${TIMESTAMP}"
|
||||
|
||||
# 1. 创建远程目录结构
|
||||
ssh "${USERNAME}@${SERVER_IP}" "mkdir -p ${RELEASE_PATH}"
|
||||
|
||||
# 2. 上传文件
|
||||
log_info "上传文件到服务器..."
|
||||
if scp -r dist/* "${USERNAME}@${SERVER_IP}:${RELEASE_PATH}/"; then
|
||||
log_info "文件上传成功"
|
||||
else
|
||||
log_error "文件上传失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. 设置权限
|
||||
ssh "${USERNAME}@${SERVER_IP}" "chmod -R 755 ${RELEASE_PATH}"
|
||||
|
||||
# 4. 原子切换软链接
|
||||
log_info "切换服务版本..."
|
||||
# 检查目标路径是否为普通目录(非软链接),如果是则备份并移除,防止ln失败
|
||||
ssh "${USERNAME}@${SERVER_IP}" "
|
||||
if [ -d '${LINK_PATH}' ] && [ ! -L '${LINK_PATH}' ]; then
|
||||
echo '检测到目标路径为普通目录,进行备份...'
|
||||
mv '${LINK_PATH}' '${LINK_PATH}_backup_$(date +%s)'
|
||||
fi
|
||||
ln -snf '${RELEASE_PATH}' '${LINK_PATH}'
|
||||
"
|
||||
|
||||
log_info "部署完成!当前版本指向: ${RELEASE_PATH}"
|
||||
|
||||
# 5. 清理旧版本
|
||||
clean_old_releases
|
||||
}
|
||||
|
||||
# 清理旧版本,只保留最近的N个
|
||||
function clean_old_releases() {
|
||||
log_info "清理旧版本(保留最近${MAX_RELEASES}个)..."
|
||||
ssh "${USERNAME}@${SERVER_IP}" "
|
||||
cd ${RELEASES_DIR} && ls -t | tail -n +$((${MAX_RELEASES} + 1)) | xargs -I {} rm -rf {}
|
||||
"
|
||||
}
|
||||
|
||||
# 回滚到上一个版本
|
||||
function rollback() {
|
||||
log_info "开始回滚操作..."
|
||||
# 获取当前指向的版本
|
||||
CURRENT_LINK=$(ssh "${USERNAME}@${SERVER_IP}" "readlink ${LINK_PATH}")
|
||||
log_info "当前版本: ${CURRENT_LINK}"
|
||||
|
||||
# 获取上一个版本目录
|
||||
PREV_VERSION=$(ssh "${USERNAME}@${SERVER_IP}" "ls -dt ${RELEASES_DIR}/* | grep -v '${CURRENT_LINK}' | head -n 1")
|
||||
|
||||
if [ -z "$PREV_VERSION" ]; then
|
||||
log_error "没有找到可回滚的历史版本"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "回滚目标版本: ${PREV_VERSION}"
|
||||
ssh "${USERNAME}@${SERVER_IP}" "ln -snf ${PREV_VERSION} ${LINK_PATH}"
|
||||
log_info "回滚成功!"
|
||||
}
|
||||
|
||||
# 主流程
|
||||
case "$1" in
|
||||
"rollback")
|
||||
rollback
|
||||
;;
|
||||
*)
|
||||
check_env
|
||||
build_project
|
||||
deploy
|
||||
;;
|
||||
esac
|
||||
@@ -1,59 +0,0 @@
|
||||
import os
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import ssl
|
||||
|
||||
# Configuration
|
||||
RESOURCES = [
|
||||
{
|
||||
"url": "https://grainy-gradients.vercel.app/noise.svg",
|
||||
"filename": "noise.svg",
|
||||
"dir": "src/assets"
|
||||
}
|
||||
]
|
||||
|
||||
def download_file(url, directory, filename):
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
|
||||
filepath = os.path.join(directory, filename)
|
||||
|
||||
print(f"Downloading {url} to {filepath}...")
|
||||
|
||||
try:
|
||||
# Create a request with a User-Agent
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=None,
|
||||
headers={
|
||||
'User-Agent': 'Mozilla/5.0'
|
||||
}
|
||||
)
|
||||
|
||||
# Bypass SSL verification
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
with urllib.request.urlopen(req, context=ctx, timeout=10) as response:
|
||||
with open(filepath, 'wb') as f:
|
||||
f.write(response.read())
|
||||
|
||||
print(f"Success: {filepath}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error downloading {url}: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("Starting static resource download...")
|
||||
|
||||
success_count = 0
|
||||
for res in RESOURCES:
|
||||
if download_file(res["url"], res["dir"], res["filename"]):
|
||||
success_count += 1
|
||||
|
||||
print(f"\nDownload complete. {success_count}/{len(RESOURCES)} files downloaded.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,131 +0,0 @@
|
||||
import os
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import hashlib
|
||||
import sys
|
||||
import ssl
|
||||
import time
|
||||
|
||||
# Configuration - PncyssD Resource Manifest
|
||||
RESOURCES = [
|
||||
{
|
||||
"url": "https://grainy-gradients.vercel.app/noise.svg",
|
||||
"filename": "noise.svg",
|
||||
"dir": "src/assets",
|
||||
"md5": None # Skip check for dynamic/external resources if hash unknown, or fill if known
|
||||
}
|
||||
]
|
||||
|
||||
def get_remote_file_size(url):
|
||||
try:
|
||||
req = urllib.request.Request(url, method='HEAD', headers={'User-Agent': 'Mozilla/5.0'})
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
with urllib.request.urlopen(req, context=ctx, timeout=10) as response:
|
||||
return int(response.headers.get('Content-Length', 0))
|
||||
except:
|
||||
return 0
|
||||
|
||||
def calculate_md5(filepath):
|
||||
hash_md5 = hashlib.md5()
|
||||
with open(filepath, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
hash_md5.update(chunk)
|
||||
return hash_md5.hexdigest()
|
||||
|
||||
def download_file_with_resume(url, directory, filename, expected_md5=None):
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
|
||||
filepath = os.path.join(directory, filename)
|
||||
temp_filepath = filepath + ".part"
|
||||
|
||||
total_size = get_remote_file_size(url)
|
||||
downloaded_size = 0
|
||||
|
||||
if os.path.exists(temp_filepath):
|
||||
downloaded_size = os.path.getsize(temp_filepath)
|
||||
|
||||
# Check if file already exists and is complete
|
||||
if os.path.exists(filepath):
|
||||
if expected_md5:
|
||||
current_md5 = calculate_md5(filepath)
|
||||
if current_md5 == expected_md5:
|
||||
print(f"[SKIP] {filename} already exists and matches MD5.")
|
||||
return True
|
||||
else:
|
||||
print(f"[WARN] {filename} exists but MD5 mismatch. Re-downloading.")
|
||||
else:
|
||||
# If no MD5 provided, check size if possible, or just skip if it exists
|
||||
if total_size > 0 and os.path.getsize(filepath) == total_size:
|
||||
print(f"[SKIP] {filename} already exists (size match).")
|
||||
return True
|
||||
|
||||
headers = {'User-Agent': 'Mozilla/5.0'}
|
||||
if downloaded_size > 0:
|
||||
headers['Range'] = f'bytes={downloaded_size}-'
|
||||
print(f"[RESUME] Resuming {filename} from {downloaded_size} bytes...")
|
||||
else:
|
||||
print(f"[START] Downloading {filename}...")
|
||||
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, context=ctx, timeout=20) as response:
|
||||
mode = 'ab' if downloaded_size > 0 else 'wb'
|
||||
with open(temp_filepath, mode) as f:
|
||||
while True:
|
||||
chunk = response.read(8192)
|
||||
if not chunk:
|
||||
break
|
||||
f.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
|
||||
# Simple progress bar
|
||||
if total_size > 0:
|
||||
percent = (downloaded_size / total_size) * 100
|
||||
sys.stdout.write(f"\rProgress: [{('=' * int(percent // 2)).ljust(50)}] {percent:.1f}%")
|
||||
sys.stdout.flush()
|
||||
|
||||
print() # Newline after progress
|
||||
|
||||
# Verify MD5 if provided
|
||||
if expected_md5:
|
||||
if calculate_md5(temp_filepath) != expected_md5:
|
||||
print(f"\n[ERROR] MD5 verification failed for {filename}")
|
||||
return False
|
||||
|
||||
os.rename(temp_filepath, filepath)
|
||||
print(f"[SUCCESS] Saved to {filepath}")
|
||||
return True
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 416: # Range Not Satisfiable (likely already complete)
|
||||
print(f"\n[INFO] File likely already complete.")
|
||||
if os.path.exists(temp_filepath):
|
||||
os.rename(temp_filepath, filepath)
|
||||
return True
|
||||
print(f"\n[ERROR] HTTP Error: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"\n[ERROR] Failed to download {url}: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("=== PncyssD Static Resource Downloader ===")
|
||||
print("Features: MD5 Check, Resume Capability, Progress Bar")
|
||||
print("==========================================")
|
||||
|
||||
success_count = 0
|
||||
for res in RESOURCES:
|
||||
if download_file_with_resume(res["url"], res["dir"], res["filename"], res.get("md5")):
|
||||
success_count += 1
|
||||
|
||||
print(f"\nTotal: {len(RESOURCES)}, Success: {success_count}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,29 +0,0 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>course-web</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"name": "course-web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.9",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"axios": "^1.13.2",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"tailwind-merge": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"fast-check": "^4.5.2",
|
||||
"globals": "^16.5.0",
|
||||
"jsdom": "^27.3.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vite": "npm:rolldown-vite@7.2.5",
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
"overrides": {
|
||||
"vite": "npm:rolldown-vite@7.2.5"
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useStoreData } from './hooks/useStoreData';
|
||||
import { Store } from './utils/store';
|
||||
import { LandingPage } from './pages/LandingPage';
|
||||
import { OnboardingPage } from './pages/OnboardingPage';
|
||||
import { DashboardPage } from './pages/DashboardPage';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
|
||||
function App() {
|
||||
// Logic is now in CurrentPage, App just provides layout
|
||||
return (
|
||||
<div className="relative min-h-screen font-sans text-gray-100 overflow-hidden">
|
||||
{/* Global Background with Gradient and Glow Effects - Similar to image */}
|
||||
<div className="fixed inset-0 -z-30">
|
||||
{/* Base Gradient Background - From blue (left) to dark brown (right) with blue gradient */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-[#1a1c2c] via-[#1a1c2c] via-[#1f1f2e] to-[#2d1b1b]"></div>
|
||||
{/* Blue gradient layer for left side */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-[#1e3a5f]/40 via-[#1a1c2c]/30 to-transparent"></div>
|
||||
{/* Additional gradient layer for smoother transition */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-[#1a1c2c]/75 via-[#1f1f2e]/55 to-[#2d1b1b]/75"></div>
|
||||
{/* Vertical gradient for depth */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-[#1a1c2c]/65 via-transparent to-[#2d1b1b]/55"></div>
|
||||
|
||||
{/* Glow Effects - Enhanced blue on left */}
|
||||
{/* Left side - Enhanced Blue glow */}
|
||||
<div className="absolute top-[-8%] left-[-8%] w-[60%] h-[60%] bg-blue-700/25 blur-[140px] rounded-full animate-float"></div>
|
||||
<div className="absolute bottom-[5%] left-[5%] w-[45%] h-[45%] bg-blue-800/20 blur-[110px] rounded-full animate-pulse-slow"></div>
|
||||
<div className="absolute top-[20%] left-[15%] w-[35%] h-[35%] bg-cyan-800/15 blur-[100px] rounded-full animate-float"></div>
|
||||
|
||||
{/* Right side - Reddish-orange glow (bottom right prominent like in image) */}
|
||||
<div className="absolute bottom-[-8%] right-[-8%] w-[55%] h-[55%] bg-orange-800/22 blur-[140px] rounded-full animate-float-delayed"></div>
|
||||
<div className="absolute top-[15%] right-[10%] w-[40%] h-[40%] bg-amber-800/16 blur-[120px] rounded-full animate-pulse-slow"></div>
|
||||
|
||||
{/* Central subtle glow */}
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[50%] h-[50%] bg-orange-900/12 blur-[160px] rounded-full"></div>
|
||||
</div>
|
||||
<div className="fixed inset-0 -z-10 bg-noise opacity-10 brightness-120 contrast-130 pointer-events-none"></div>
|
||||
|
||||
{/* Page Content */}
|
||||
<CurrentPage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CurrentPage() {
|
||||
const data = useStoreData();
|
||||
const [view, setView] = useState('landing'); // 'landing', 'onboarding', 'login'
|
||||
|
||||
// If onboarding is complete, go directly to Dashboard
|
||||
if (data.onboardingComplete) {
|
||||
return <DashboardPage />;
|
||||
}
|
||||
|
||||
// Login Page
|
||||
if (view === 'login') {
|
||||
return (
|
||||
<LoginPage
|
||||
onLoginSuccess={() => {
|
||||
// 登录成功后,检查用户是否已完成引导
|
||||
// 如果已完成,直接进入仪表盘;否则进入引导流程
|
||||
setView('onboarding');
|
||||
}}
|
||||
onBack={() => setView('landing')}
|
||||
onSignUp={() => setView('onboarding')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Onboarding Page
|
||||
if (view === 'onboarding') {
|
||||
return (
|
||||
<OnboardingPage
|
||||
onFinish={() => {
|
||||
// The store update in OnboardingPage triggers a re-render here
|
||||
// causing data.onboardingComplete to be true
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Default: Landing Page
|
||||
return (
|
||||
<LandingPage
|
||||
onStart={() => setView('onboarding')}
|
||||
onLogin={() => setView('login')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,86 +0,0 @@
|
||||
import request from '../utils/request';
|
||||
|
||||
/**
|
||||
* 认证相关 API
|
||||
*/
|
||||
export const authApi = {
|
||||
/**
|
||||
* 发送短信验证码
|
||||
* @param {string} phone - 手机号
|
||||
*/
|
||||
sendSmsCode(phone) {
|
||||
return request({
|
||||
url: '/auth/sms-code',
|
||||
method: 'get',
|
||||
params: { phone }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 手机号验证码登录
|
||||
* @param {Object} data - 登录参数
|
||||
* @param {string} data.phone - 手机号
|
||||
* @param {string} data.smsCode - 短信验证码
|
||||
*/
|
||||
login(data) {
|
||||
return request({
|
||||
url: '/auth/login',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
getUserInfo() {
|
||||
return request({
|
||||
url: '/auth/userInfo',
|
||||
method: 'get'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
logout() {
|
||||
return request({
|
||||
url: '/auth/logout',
|
||||
method: 'post'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
* @param {string} refreshToken - 刷新令牌
|
||||
*/
|
||||
refreshToken(refreshToken) {
|
||||
return request({
|
||||
url: '/auth/refreshToken',
|
||||
method: 'post',
|
||||
data: { refreshToken }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证令牌
|
||||
*/
|
||||
validateToken() {
|
||||
return request({
|
||||
url: '/auth/validateToken',
|
||||
method: 'get'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查手机号是否存在
|
||||
* @param {string} phone - 手机号
|
||||
*/
|
||||
checkPhone(phone) {
|
||||
return request({
|
||||
url: '/auth/checkPhone',
|
||||
method: 'get',
|
||||
params: { phone }
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,233 +0,0 @@
|
||||
import request from '../utils/request';
|
||||
|
||||
/**
|
||||
* 生命事件 API
|
||||
*/
|
||||
export const lifeEventApi = {
|
||||
/**
|
||||
* 分页查询生命事件
|
||||
*/
|
||||
getPage(params) {
|
||||
return request({
|
||||
url: '/lifeEvent/page',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取所有生命事件列表
|
||||
*/
|
||||
getList() {
|
||||
return request({
|
||||
url: '/lifeEvent/list',
|
||||
method: 'get'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取生命事件详情
|
||||
*/
|
||||
getById(id) {
|
||||
return request({
|
||||
url: '/lifeEvent/detail',
|
||||
method: 'get',
|
||||
params: { id }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建生命事件
|
||||
*/
|
||||
create(data) {
|
||||
return request({
|
||||
url: '/lifeEvent/create',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新生命事件
|
||||
*/
|
||||
update(data) {
|
||||
return request({
|
||||
url: '/lifeEvent/update',
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除生命事件
|
||||
*/
|
||||
delete(id) {
|
||||
return request({
|
||||
url: '/lifeEvent/delete',
|
||||
method: 'delete',
|
||||
params: { id }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 爽文剧本 API
|
||||
*/
|
||||
export const epicScriptApi = {
|
||||
/**
|
||||
* 分页查询爽文剧本
|
||||
*/
|
||||
getPage(params) {
|
||||
return request({
|
||||
url: '/epicScript/page',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取所有爽文剧本列表
|
||||
*/
|
||||
getList() {
|
||||
return request({
|
||||
url: '/epicScript/listAll',
|
||||
method: 'get'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取爽文剧本详情
|
||||
*/
|
||||
getById(id) {
|
||||
return request({
|
||||
url: '/epicScript/detail',
|
||||
method: 'get',
|
||||
params: { id }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建爽文剧本
|
||||
*/
|
||||
create(data) {
|
||||
return request({
|
||||
url: '/epicScript/create',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新爽文剧本
|
||||
*/
|
||||
update(data) {
|
||||
return request({
|
||||
url: '/epicScript/update',
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 选中剧本
|
||||
*/
|
||||
select(id) {
|
||||
return request({
|
||||
url: '/epicScript/select',
|
||||
method: 'put',
|
||||
params: { id }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除爽文剧本
|
||||
*/
|
||||
delete(id) {
|
||||
return request({
|
||||
url: '/epicScript/delete',
|
||||
method: 'delete',
|
||||
params: { id }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 实现路径 API
|
||||
*/
|
||||
export const lifePathApi = {
|
||||
/**
|
||||
* 分页查询实现路径
|
||||
*/
|
||||
getPage(params) {
|
||||
return request({
|
||||
url: '/lifePath/page',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取所有实现路径列表
|
||||
*/
|
||||
getList() {
|
||||
return request({
|
||||
url: '/lifePath/listAll',
|
||||
method: 'get'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据剧本ID获取实现路径
|
||||
*/
|
||||
getByScriptId(scriptId) {
|
||||
return request({
|
||||
url: '/lifePath/byScript',
|
||||
method: 'get',
|
||||
params: { scriptId }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取实现路径详情
|
||||
*/
|
||||
getById(id) {
|
||||
return request({
|
||||
url: '/lifePath/detail',
|
||||
method: 'get',
|
||||
params: { id }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建实现路径
|
||||
*/
|
||||
create(data) {
|
||||
return request({
|
||||
url: '/lifePath/create',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新实现路径
|
||||
*/
|
||||
update(data) {
|
||||
return request({
|
||||
url: '/lifePath/update',
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除实现路径
|
||||
*/
|
||||
delete(id) {
|
||||
return request({
|
||||
url: '/lifePath/delete',
|
||||
method: 'delete',
|
||||
params: { id }
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import request from '../utils/request';
|
||||
|
||||
export const userApi = {
|
||||
/**
|
||||
* 获取当前登录用户的档案
|
||||
*/
|
||||
getCurrentUser() {
|
||||
return request({
|
||||
url: '/user-profile/me',
|
||||
method: 'get'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 新增档案
|
||||
*/
|
||||
createUserProfile(data) {
|
||||
return request({
|
||||
url: '/user-profile/create',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 修改档案
|
||||
*/
|
||||
updateUserProfile(data) {
|
||||
return request({
|
||||
url: '/user-profile/update',
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据ID查询详情
|
||||
*/
|
||||
getProfileById(id) {
|
||||
return request({
|
||||
url: '/user-profile/detail',
|
||||
method: 'get',
|
||||
params: { id }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除档案
|
||||
*/
|
||||
deleteUserProfile(id) {
|
||||
return request({
|
||||
url: '/user-profile/delete',
|
||||
method: 'delete',
|
||||
params: { id }
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
|
||||
<filter id="noise" x="0" y="0">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/>
|
||||
<feBlend mode="screen"/>
|
||||
</filter>
|
||||
<rect width="500" height="500" filter="url(#noise)" opacity="0.5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 324 B |
@@ -1,312 +0,0 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useStoreData } from '../hooks/useStoreData';
|
||||
import { Store } from '../utils/store';
|
||||
import { userApi } from '../api/user';
|
||||
import { User, Settings, LogOut, X, Edit2 } from 'lucide-react';
|
||||
import { GlassCard } from './ui/GlassCard';
|
||||
import { Button } from './ui/Button';
|
||||
import clsx from 'clsx';
|
||||
|
||||
/**
|
||||
* 用户资料菜单组件
|
||||
* 显示用户信息、编辑资料和退出登录选项
|
||||
*/
|
||||
export function UserMenu({ isOpen, onClose, onLogout }) {
|
||||
const data = useStoreData();
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const menuRef = useRef(null);
|
||||
|
||||
// 每次打开菜单时,重置编辑模态框状态(模仿 PncyssD 的逻辑)
|
||||
// 确保每次打开菜单都显示主菜单界面,而不是编辑界面
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
// 菜单打开时,确保编辑模态框是关闭的
|
||||
setShowEditModal(false);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// 点击外部关闭菜单
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// 如果菜单未打开,不渲染任何内容(除非正在显示编辑模态框)
|
||||
if (!isOpen && !showEditModal) return null;
|
||||
|
||||
// 如果正在显示编辑模态框,只渲染编辑模态框
|
||||
if (showEditModal) {
|
||||
return (
|
||||
<EditProfileModal
|
||||
onClose={() => {
|
||||
setShowEditModal(false);
|
||||
// 编辑模态框关闭后,如果菜单是打开的,会显示主菜单
|
||||
}}
|
||||
userProfile={data.userProfile}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 显示主菜单(isOpen 为 true 且 showEditModal 为 false)
|
||||
|
||||
// 显示主菜单
|
||||
return (
|
||||
<>
|
||||
{/* 菜单遮罩层 */}
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* 用户菜单 */}
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="fixed top-24 md:top-4 left-4 md:left-[300px] z-50 w-[calc(100%-2rem)] md:w-80 animate-fade-in"
|
||||
>
|
||||
<div className="bg-[#1a1c2c]/95 border border-white/20 shadow-2xl rounded-2xl p-6 space-y-6 backdrop-blur-sm">
|
||||
{/* 用户信息头部 */}
|
||||
<div className="flex items-center gap-4 pb-4 border-b border-white/10">
|
||||
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-primary/30 to-blue-600/30 flex items-center justify-center text-2xl font-bold text-white border border-white/20 shadow-lg">
|
||||
{data.userProfile.nickname?.[0] || 'U'}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="font-bold text-gray-100 text-lg truncate">
|
||||
{data.userProfile.nickname || '旅人'}
|
||||
</div>
|
||||
<div className="text-xs text-primary flex items-center gap-1.5 mt-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-primary shadow-[0_0_5px_rgba(205,133,63,0.5)] animate-pulse"></span>
|
||||
{data.userProfile.mbti || '未知'} · {data.userProfile.zodiac || '未知'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计数据 */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="p-4 bg-white/5 rounded-xl border border-white/5 text-center">
|
||||
<div className="text-2xl font-bold text-primary">
|
||||
{data.lifeTimeline?.length || 0}
|
||||
</div>
|
||||
<div className="text-[10px] text-white/40 uppercase tracking-widest mt-1">
|
||||
生命足迹
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-white/5 rounded-xl border border-white/5 text-center">
|
||||
<div className="text-2xl font-bold text-blue-400">
|
||||
{data.generatedScripts?.length || 0}
|
||||
</div>
|
||||
<div className="text-[10px] text-white/40 uppercase tracking-widest mt-1">
|
||||
剧本生成
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="space-y-2 pt-2 border-t border-white/5">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="md"
|
||||
className="w-full justify-start"
|
||||
onClick={() => {
|
||||
setShowEditModal(true);
|
||||
// 不关闭主菜单,让编辑模态框显示在主菜单之上
|
||||
// 这样关闭编辑模态框后,主菜单仍然可见
|
||||
}}
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
编辑资料
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="md"
|
||||
className="w-full justify-start text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
onClick={() => {
|
||||
if (window.confirm('确定要退出登录并清除所有数据吗?此操作不可逆。')) {
|
||||
Store.reset();
|
||||
if (onLogout) onLogout();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LogOut className="w-4 h-4 mr-2" />
|
||||
退出登录
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑资料模态框组件
|
||||
*/
|
||||
function EditProfileModal({ onClose, userProfile }) {
|
||||
const [formData, setFormData] = useState({
|
||||
nickname: userProfile.nickname || '',
|
||||
mbti: userProfile.mbti || '',
|
||||
zodiac: userProfile.zodiac || '',
|
||||
hobbies: userProfile.hobbies?.join(', ') || '',
|
||||
gender: userProfile.gender || 'secret'
|
||||
});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
setError('');
|
||||
|
||||
const updatedProfile = {
|
||||
nickname: formData.nickname,
|
||||
mbti: formData.mbti,
|
||||
zodiac: formData.zodiac,
|
||||
hobbies: formData.hobbies.split(',').map(s => s.trim()).filter(s => s),
|
||||
gender: formData.gender
|
||||
};
|
||||
|
||||
try {
|
||||
// 1. 更新本地 Store
|
||||
Store.updateProfile(updatedProfile);
|
||||
|
||||
// 2. 同步到后端
|
||||
const currentProfile = await userApi.getCurrentUser();
|
||||
if (currentProfile.data && currentProfile.data.id) {
|
||||
await userApi.updateUserProfile({
|
||||
id: currentProfile.data.id,
|
||||
nickname: updatedProfile.nickname,
|
||||
mbti: updatedProfile.mbti,
|
||||
zodiac: updatedProfile.zodiac,
|
||||
hobbies: JSON.stringify(updatedProfile.hobbies),
|
||||
gender: updatedProfile.gender
|
||||
});
|
||||
}
|
||||
|
||||
onClose();
|
||||
} catch (e) {
|
||||
console.error('保存资料失败:', e);
|
||||
setError(e.response?.data?.message || '保存失败,请重试');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-xl">
|
||||
<GlassCard className="w-full max-w-lg p-8 relative max-h-[90vh] overflow-y-auto">
|
||||
{/* 关闭按钮 */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-6 right-6 text-white/40 hover:text-white transition-colors z-10"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* 标题 */}
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="w-12 h-12 rounded-xl bg-primary/20 flex items-center justify-center">
|
||||
<Edit2 className="text-primary w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-gray-100">编辑资料</h3>
|
||||
<p className="text-xs text-gray-400 mt-1">调整你的人生航向基础信息</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 表单 */}
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-300 mb-2 block">昵称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.nickname}
|
||||
onChange={(e) => setFormData({ ...formData, nickname: e.target.value })}
|
||||
placeholder="你想被如何称呼?"
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-300 mb-2 block">MBTI</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.mbti}
|
||||
onChange={(e) => setFormData({ ...formData, mbti: e.target.value })}
|
||||
placeholder="性格色彩"
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-300 mb-2 block">星座</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.zodiac}
|
||||
onChange={(e) => setFormData({ ...formData, zodiac: e.target.value })}
|
||||
placeholder="星辰指引"
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-300 mb-2 block">兴趣爱好</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.hobbies}
|
||||
onChange={(e) => setFormData({ ...formData, hobbies: e.target.value })}
|
||||
placeholder="让灵魂起舞的事物(用逗号分隔)"
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-300 mb-2 block">性别</label>
|
||||
<select
|
||||
value={formData.gender}
|
||||
onChange={(e) => setFormData({ ...formData, gender: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition-all"
|
||||
>
|
||||
<option value="secret">保密</option>
|
||||
<option value="male">男</option>
|
||||
<option value="female">女</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{error && (
|
||||
<div className="p-3 rounded-xl bg-red-500/10 border border-red-500/20 text-red-200 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex gap-4 mt-8 pt-6 border-t border-white/5">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="md"
|
||||
className="flex-1"
|
||||
onClick={handleSave}
|
||||
isLoading={isSaving}
|
||||
>
|
||||
保存修改
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="md"
|
||||
onClick={onClose}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* PncyssD Design System Button
|
||||
*
|
||||
* Variants:
|
||||
* - primary: Main action, gradient background
|
||||
* - secondary: Alternative action, glass effect
|
||||
* - outline: Bordered, transparent background
|
||||
* - ghost: Text only, hover effect
|
||||
*
|
||||
* Sizes:
|
||||
* - sm: Compact
|
||||
* - md: Default
|
||||
* - lg: Large/Hero
|
||||
*/
|
||||
export function Button({
|
||||
children,
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
className,
|
||||
isLoading = false,
|
||||
disabled,
|
||||
...props
|
||||
}) {
|
||||
const baseStyles = "relative inline-flex items-center justify-center font-bold tracking-wide transition-all duration-300 rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-deep-sea disabled:opacity-50 disabled:cursor-not-allowed overflow-hidden group";
|
||||
|
||||
const variants = {
|
||||
primary: "bg-primary text-white shadow-lg shadow-primary/30 hover:shadow-primary/50 hover:scale-[1.02] active:scale-95 border border-transparent",
|
||||
secondary: "bg-white/10 text-white backdrop-blur-md border border-white/10 hover:bg-white/20 hover:border-white/30 hover:scale-[1.02] active:scale-95",
|
||||
outline: "bg-transparent text-primary border-2 border-primary hover:bg-primary/10 active:scale-95",
|
||||
ghost: "bg-transparent text-gray-300 hover:text-white hover:bg-white/5 active:scale-95"
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
sm: "px-4 py-2 text-sm",
|
||||
md: "px-6 py-3 text-base",
|
||||
lg: "px-8 py-4 text-lg"
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
baseStyles,
|
||||
variants[variant],
|
||||
sizes[size],
|
||||
className
|
||||
)}
|
||||
disabled={isLoading || disabled}
|
||||
{...props}
|
||||
>
|
||||
{/* Shine Effect for Primary Buttons */}
|
||||
{variant === 'primary' && !disabled && !isLoading && (
|
||||
<div className="absolute inset-0 -translate-x-full group-hover:animate-[shimmer_2s_infinite] bg-gradient-to-r from-transparent via-white/20 to-transparent z-0 pointer-events-none" />
|
||||
)}
|
||||
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
{isLoading && <Loader2 className="w-4 h-4 animate-spin" />}
|
||||
{children}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Check } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export function Checkbox({ label, checked, onChange, className }) {
|
||||
return (
|
||||
<label className={clsx("flex items-center gap-2 cursor-pointer group", className)}>
|
||||
<div
|
||||
className={clsx(
|
||||
"w-5 h-5 rounded border flex items-center justify-center transition-all duration-200",
|
||||
checked
|
||||
? "bg-primary border-primary text-deep-sea"
|
||||
: "bg-white/5 border-white/20 group-hover:border-primary/50"
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="hidden"
|
||||
checked={checked}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
/>
|
||||
{checked && <Check className="w-3.5 h-3.5 stroke-[3]" />}
|
||||
</div>
|
||||
{label && <span className="text-sm text-gray-300 group-hover:text-white transition-colors select-none">{label}</span>}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
/**
|
||||
* PncyssD Design System GlassCard
|
||||
* Standard container for content with glassmorphism effect.
|
||||
*/
|
||||
export function GlassCard({ children, className, ...props }) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-white/10 backdrop-blur-xl border border-white/15 shadow-2xl rounded-2xl transition-all duration-300",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
/**
|
||||
* PncyssD Design System Input
|
||||
* Supports standard input, select, and textarea with consistent glassmorphism styling.
|
||||
*/
|
||||
|
||||
const baseStyles = "w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-gray-100 placeholder-gray-500 focus:outline-none focus:border-primary/50 focus:ring-1 focus:ring-primary/50 focus:bg-white/10 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed backdrop-blur-sm";
|
||||
const errorStyles = "border-red-500/50 focus:border-red-500 focus:ring-red-500/50 bg-red-500/5";
|
||||
|
||||
export const Input = React.forwardRef(({ className, error, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
className={clsx(baseStyles, error && errorStyles, className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const Select = React.forwardRef(({ className, children, error, ...props }, ref) => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<select
|
||||
ref={ref}
|
||||
className={clsx(baseStyles, "appearance-none pr-10", error && errorStyles, className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const Textarea = React.forwardRef(({ className, error, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
ref={ref}
|
||||
className={clsx(baseStyles, "resize-none min-h-[100px]", error && errorStyles, className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||