upload project source code
This commit is contained in:
1
前端源码/uni-app/.env
Normal file
1
前端源码/uni-app/.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_WECHAT_H5_APPID=wx1ca1ac7ad12123ac
|
||||||
36
前端源码/uni-app/.gitignore
vendored
Normal file
36
前端源码/uni-app/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# 依赖
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# 构建输出
|
||||||
|
unpackage/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# 本地环境配置
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# 编辑器/IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.vsix
|
||||||
|
*~
|
||||||
|
|
||||||
|
# 系统文件
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# 日志
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# 缓存
|
||||||
|
.cache/
|
||||||
|
.temp/
|
||||||
|
*.cache
|
||||||
|
|
||||||
|
# 小程序本地配置
|
||||||
|
project.private.config.json
|
||||||
91
前端源码/uni-app/App.vue
Normal file
91
前端源码/uni-app/App.vue
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: aliyun4247073344 893643761@qq.com
|
||||||
|
* @Date: 2026-02-27 11:35:54
|
||||||
|
* @LastEditors: aliyun4247073344 893643761@qq.com
|
||||||
|
* @LastEditTime: 2026-02-27 13:48:44
|
||||||
|
* @FilePath: \uni-app\App.vue
|
||||||
|
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="app-shell">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 标准 H5 应用入口
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* uni-app 组件兼容样式 */
|
||||||
|
view {
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
image {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll-view {
|
||||||
|
display: block;
|
||||||
|
overflow: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
swiper {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
swiper-item {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局工具类 */
|
||||||
|
.writing-vertical-rl {
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
text-orientation: upright;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏全局滚动条 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
68
前端源码/uni-app/FIXES.md
Normal file
68
前端源码/uni-app/FIXES.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# 问题修复总结
|
||||||
|
|
||||||
|
## 已解决的问题
|
||||||
|
|
||||||
|
### 1. API JSON 解析错误 ✅
|
||||||
|
**问题**:所有 API 返回 "Unexpected non-whitespace character after JSON at position 1"
|
||||||
|
**原因**:请求拦截器中对 token 使用 `JSON.parse()` 导致解析失败
|
||||||
|
**修复**:添加 try-catch 处理,兼容 JSON 和普通字符串格式的 token
|
||||||
|
|
||||||
|
### 2. 路由警告 ✅
|
||||||
|
**问题**:Vue Router 警告 "No match found for location with path '/home'"
|
||||||
|
**修复**:
|
||||||
|
- 添加 `/home` 路由重定向到 `/`
|
||||||
|
- 添加通配符路由捕获所有未匹配的路由
|
||||||
|
- 添加路由守卫和错误处理
|
||||||
|
|
||||||
|
### 3. 轮播图和 TabBar 不显示 ✅
|
||||||
|
**问题**:使用 `rpx` 单位但浏览器不识别
|
||||||
|
**修复**:将关键组件的 `rpx` 单位改为标准 CSS 单位(px)
|
||||||
|
|
||||||
|
## 修改的文件
|
||||||
|
|
||||||
|
1. `utils/request.ts` - 修复 token 解析和响应处理
|
||||||
|
2. `router/index.js` - 添加路由重定向和守卫
|
||||||
|
3. `postcss-rpx-to-vw.js` - 修复 PostCSS 插件导出
|
||||||
|
4. `components/CustomTabBar.vue` - 将 rpx 改为 px
|
||||||
|
5. `components/screens/Home.vue` - 轮播图样式改为 px
|
||||||
|
6. `pages/index/index.vue` - 内容区域间距改为 px
|
||||||
|
|
||||||
|
## 关键修改
|
||||||
|
|
||||||
|
### CustomTabBar.vue
|
||||||
|
- TabBar 高度:80rpx → 60px
|
||||||
|
- 图标大小:48rpx → 32px
|
||||||
|
- 字体大小:24rpx → 12px
|
||||||
|
|
||||||
|
### Home.vue 轮播图
|
||||||
|
- 轮播高度:40rpx → 24px
|
||||||
|
- 内边距:20rpx 24rpx → 12px 16px
|
||||||
|
- 圆点大小:16rpx → 8px
|
||||||
|
|
||||||
|
### pages/index/index.vue
|
||||||
|
- 内容内边距:32rpx → 20px
|
||||||
|
- TabBar 底部间距:120rpx → 80px
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
|
||||||
|
1. 确保开发服务器已重启
|
||||||
|
2. 刷新浏览器页面
|
||||||
|
3. 检查:
|
||||||
|
- ✓ 底部 TabBar 是否显示
|
||||||
|
- ✓ 轮播消息是否显示并滚动
|
||||||
|
- ✓ API 数据是否正常加载
|
||||||
|
- ✓ 页面布局是否正常
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 所有修改都使用标准 CSS 单位(px)而不是 rpx
|
||||||
|
- TabBar 固定在底部,z-index: 999
|
||||||
|
- 轮播图高度固定为 24px,确保可见
|
||||||
|
- 内容区域有足够的底部间距,不会被 TabBar 遮挡
|
||||||
|
|
||||||
|
## 如果问题仍然存在
|
||||||
|
|
||||||
|
1. 清除浏览器缓存(Ctrl+Shift+Delete)
|
||||||
|
2. 硬刷新页面(Ctrl+Shift+R)
|
||||||
|
3. 检查浏览器控制台是否有错误
|
||||||
|
4. 使用开发者工具检查元素的计算样式
|
||||||
158
前端源码/uni-app/LOGIN-REFACTOR.md
Normal file
158
前端源码/uni-app/LOGIN-REFACTOR.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# 登录系统重构总结
|
||||||
|
|
||||||
|
## 重构内容
|
||||||
|
|
||||||
|
已完全舍弃小程序登录逻辑,使用新的手机号登录/注册系统。
|
||||||
|
|
||||||
|
## 新增的 API 接口
|
||||||
|
|
||||||
|
### 1. 发送短信验证码
|
||||||
|
```typescript
|
||||||
|
userApi.sendSmsCode(mobile: string)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 手机号密码注册
|
||||||
|
```typescript
|
||||||
|
userApi.mobileRegister({
|
||||||
|
mobile: string,
|
||||||
|
password: string,
|
||||||
|
repassword: string,
|
||||||
|
verification_code: string,
|
||||||
|
inviter_id?: number
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 手机号密码登录
|
||||||
|
```typescript
|
||||||
|
userApi.mobileLogin({
|
||||||
|
mobile: string,
|
||||||
|
password: string,
|
||||||
|
inviter_id?: number
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 忘记密码(重置密码)
|
||||||
|
```typescript
|
||||||
|
userApi.forgotPassword({
|
||||||
|
mobile: string,
|
||||||
|
password: string,
|
||||||
|
repassword: string,
|
||||||
|
verification_code: string
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 新登录页面功能
|
||||||
|
|
||||||
|
### 1. 三个标签页
|
||||||
|
- **登录**:手机号 + 密码登录
|
||||||
|
- **注册**:手机号 + 验证码 + 密码注册
|
||||||
|
- **忘记密码**:手机号 + 验证码 + 新密码重置
|
||||||
|
|
||||||
|
### 2. 表单验证
|
||||||
|
- 手机号格式验证(1开头的11位数字)
|
||||||
|
- 密码长度验证(6-20位)
|
||||||
|
- 两次密码一致性验证
|
||||||
|
- 验证码长度验证(6位)
|
||||||
|
|
||||||
|
### 3. 验证码功能
|
||||||
|
- 60秒倒计时
|
||||||
|
- 防重复发送
|
||||||
|
- 自动禁用按钮
|
||||||
|
|
||||||
|
### 4. 密码显示/隐藏
|
||||||
|
- 点击眼睛图标切换密码可见性
|
||||||
|
- 支持所有密码输入框
|
||||||
|
|
||||||
|
### 5. 样式设计
|
||||||
|
- 与首页风格一致
|
||||||
|
- 使用宋体字体
|
||||||
|
- 古典中国风配色(#8b2323 主色调)
|
||||||
|
- 米纸纹理背景
|
||||||
|
- 圆润的卡片设计
|
||||||
|
- 流畅的动画效果
|
||||||
|
|
||||||
|
## 页面结构
|
||||||
|
|
||||||
|
```
|
||||||
|
pages/login/login.vue (容器页面)
|
||||||
|
└── components/screens/Login.vue (登录组件)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用流程
|
||||||
|
|
||||||
|
### 新用户注册
|
||||||
|
1. 点击"注册"标签
|
||||||
|
2. 输入手机号
|
||||||
|
3. 点击"获取验证码"
|
||||||
|
4. 输入收到的验证码
|
||||||
|
5. 设置密码并确认
|
||||||
|
6. 点击"注册"按钮
|
||||||
|
7. 自动登录并跳转
|
||||||
|
|
||||||
|
### 老用户登录
|
||||||
|
1. 默认在"登录"标签
|
||||||
|
2. 输入手机号和密码
|
||||||
|
3. 点击"登录"按钮
|
||||||
|
4. 自动跳转到首页
|
||||||
|
|
||||||
|
### 忘记密码
|
||||||
|
1. 点击"忘记密码?"
|
||||||
|
2. 输入手机号
|
||||||
|
3. 点击"获取验证码"
|
||||||
|
4. 输入验证码
|
||||||
|
5. 设置新密码并确认
|
||||||
|
6. 点击"重置密码"
|
||||||
|
7. 自动登录并跳转
|
||||||
|
|
||||||
|
## 技术特点
|
||||||
|
|
||||||
|
### 1. 响应式设计
|
||||||
|
- 最大宽度 500px
|
||||||
|
- 居中显示
|
||||||
|
- 适配各种屏幕尺寸
|
||||||
|
|
||||||
|
### 2. 用户体验
|
||||||
|
- 实时表单验证
|
||||||
|
- 按钮禁用状态
|
||||||
|
- 加载状态提示
|
||||||
|
- 错误提示
|
||||||
|
- 成功提示
|
||||||
|
|
||||||
|
### 3. 安全性
|
||||||
|
- 密码加密传输
|
||||||
|
- 验证码验证
|
||||||
|
- 防重复提交
|
||||||
|
- Token 认证
|
||||||
|
|
||||||
|
### 4. 代码质量
|
||||||
|
- TypeScript 类型安全
|
||||||
|
- 组件化设计
|
||||||
|
- 清晰的代码结构
|
||||||
|
- 完善的错误处理
|
||||||
|
|
||||||
|
## 样式特色
|
||||||
|
|
||||||
|
- 背景:米纸纹理 + 旋转渐变
|
||||||
|
- 主色:#8b2323(中国红)
|
||||||
|
- 字体:SimSun, Songti SC(宋体)
|
||||||
|
- 圆角:16px(卡片)、8px(输入框)、24px(按钮)
|
||||||
|
- 阴影:柔和的投影效果
|
||||||
|
- 动画:旋转背景、按钮点击效果
|
||||||
|
|
||||||
|
## 测试建议
|
||||||
|
|
||||||
|
1. 测试注册流程
|
||||||
|
2. 测试登录流程
|
||||||
|
3. 测试忘记密码流程
|
||||||
|
4. 测试表单验证
|
||||||
|
5. 测试验证码倒计时
|
||||||
|
6. 测试错误处理
|
||||||
|
7. 测试响应式布局
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 需要后端支持短信验证码发送
|
||||||
|
2. 验证码有效期由后端控制
|
||||||
|
3. 密码强度建议由后端验证
|
||||||
|
4. Token 过期处理已在 request.ts 中实现
|
||||||
|
5. 所有 API 调用都有 loading 状态
|
||||||
38
前端源码/uni-app/README.md
Normal file
38
前端源码/uni-app/README.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
### 3 分钟了解如何进入开发
|
||||||
|
|
||||||
|
欢迎使用云效代码管理 Codeup,通过阅读以下内容,你可以快速熟悉 Codeup ,并立即开始今天的工作。
|
||||||
|
|
||||||
|
### 提交**文件**
|
||||||
|
|
||||||
|
Codeup 支持两种方式进行代码提交:网页端提交,以及本地 Git 客户端提交。
|
||||||
|
|
||||||
|
* 如需体验本地命令行操作,请先安装 Git 工具,安装方法参见[安装Git](https://help.aliyun.com/document_detail/153800.html)。
|
||||||
|
|
||||||
|
* 如需体验 SSH 方式克隆和提交代码,请先在平台账号内配置 SSH 公钥,配置方法参见[配置 SSH 密钥](https://help.aliyun.com/document_detail/153709.html)。
|
||||||
|
|
||||||
|
* 如需体验 HTTP 方式克隆和提交代码,请先在平台账号内配置克隆账密,配置方法参见[配置 HTTPS 克隆账号密码](https://help.aliyun.com/document_detail/153710.html)。
|
||||||
|
|
||||||
|
现在,你可以在 Codeup 中提交代码文件了,跟着文档「[__提交第一行代码__](https://help.aliyun.com/document_detail/153707.html?spm=a2c4g.153710.0.0.3c213774PFSMIV#6a5dbb1063ai5)」一起操作试试看吧。
|
||||||
|
|
||||||
|
<img src="https://img.alicdn.com/imgextra/i3/O1CN013zHrNR1oXgGu8ccvY_!!6000000005235-0-tps-2866-1268.jpg" width="100%" />
|
||||||
|
|
||||||
|
|
||||||
|
### 进行代码检测
|
||||||
|
|
||||||
|
开发过程中,为了更好的维护你的代码质量,你可以开启 Codeup 内置开箱即用的「[代码检测服务](https://help.aliyun.com/document_detail/434321.html)」,开启后提交或合并请求的变更将自动触发检测,识别代码编写规范和安全漏洞问题,并及时提供结果报表和修复建议。
|
||||||
|
|
||||||
|
<img src="https://img.alicdn.com/imgextra/i2/O1CN01BRzI1I1IO0CR2i4Aw_!!6000000000882-0-tps-2862-1362.jpg" width="100%" />
|
||||||
|
|
||||||
|
### 开展代码评审
|
||||||
|
|
||||||
|
功能开发完毕后,通常你需要发起「[代码评审并执行合并](https://help.aliyun.com/document_detail/153872.html)」,Codeup 支持多人协作的代码评审服务,你可以通过「[保护分支设置合并规则](https://help.aliyun.com/document_detail/153873.html?spm=a2c4g.203108.0.0.430765d1l9tTRR#p-4on-aep-l5q)」策略及「[__合并请求设置__](https://help.aliyun.com/document_detail/153874.html?spm=a2c4g.153871.0.0.3d38686cJpcdJI)」对合并过程进行流程化管控,同时提供在线代码评审及冲突解决能力,让评审过程更加流畅。
|
||||||
|
|
||||||
|
<img src="https://img.alicdn.com/imgextra/i1/O1CN01MaBDFH1WWcGnQqMHy_!!6000000002796-0-tps-2592-1336.jpg" width="100%" />
|
||||||
|
|
||||||
|
### 成员协作
|
||||||
|
|
||||||
|
是时候邀请成员一起编写卓越的代码工程了,请点击左下角「成员」邀请你的小伙伴开始协作吧!
|
||||||
|
|
||||||
|
### 更多
|
||||||
|
|
||||||
|
Git 使用教学、高级功能指引等更多说明,参见[Codeup帮助文档](https://help.aliyun.com/document_detail/153402.html)。
|
||||||
114
前端源码/uni-app/UI-FIXES.md
Normal file
114
前端源码/uni-app/UI-FIXES.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# UI 显示问题修复总结
|
||||||
|
|
||||||
|
## 修复内容
|
||||||
|
|
||||||
|
### 1. TabBar 图标不显示 ✅
|
||||||
|
**问题**:使用 `<image>` 标签(uni-app 组件)在 H5 环境中不显示
|
||||||
|
**解决方案**:将所有图标改为直接使用 `<svg>` 标签
|
||||||
|
|
||||||
|
**修改的图标组件**:
|
||||||
|
- `HomeIcon.vue` - 首页图标
|
||||||
|
- `TestIcon.vue` - 测名图标
|
||||||
|
- `NamingIcon.vue` - 起名图标
|
||||||
|
- `RenamingIcon.vue` - 改名图标
|
||||||
|
- `ProfileIcon.vue` - 我的图标
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- SVG 直接渲染,无需加载外部资源
|
||||||
|
- 支持 `currentColor`,可以继承父元素颜色
|
||||||
|
- 更好的性能和兼容性
|
||||||
|
|
||||||
|
### 2. 轮播图挤在一起 ✅
|
||||||
|
**问题**:使用 `<swiper>` 组件(uni-app 组件)在 H5 环境中显示异常
|
||||||
|
**解决方案**:使用纯 CSS 动画实现垂直滚动效果
|
||||||
|
|
||||||
|
**实现方式**:
|
||||||
|
```css
|
||||||
|
.home-notification-scroll {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
animation: scroll-vertical 10s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scroll-vertical {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**特性**:
|
||||||
|
- 垂直滚动动画
|
||||||
|
- 10秒完成一轮循环
|
||||||
|
- 圆点呼吸动画效果
|
||||||
|
- 自动无限循环
|
||||||
|
|
||||||
|
### 3. TabBar 样式优化 ✅
|
||||||
|
**修改**:
|
||||||
|
- 高度:80rpx → 60px
|
||||||
|
- 图标大小:48rpx → 32px
|
||||||
|
- 字体大小:24rpx → 12px
|
||||||
|
- 间距:10rpx → 5px
|
||||||
|
|
||||||
|
### 4. 轮播图样式优化 ✅
|
||||||
|
**修改**:
|
||||||
|
- 容器高度:40rpx → 24px
|
||||||
|
- 内边距:20rpx 24rpx → 12px 16px
|
||||||
|
- 圆点大小:16rpx → 8px
|
||||||
|
- 添加圆点呼吸动画
|
||||||
|
|
||||||
|
## 技术要点
|
||||||
|
|
||||||
|
### SVG 图标使用
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
:width="size"
|
||||||
|
:height="size"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path stroke="currentColor" ... />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS 动画轮播
|
||||||
|
- 使用 `transform: translateY()` 实现垂直滚动
|
||||||
|
- `animation: scroll-vertical 10s linear infinite` 无限循环
|
||||||
|
- 容器 `overflow: hidden` 隐藏溢出内容
|
||||||
|
|
||||||
|
## 测试验证
|
||||||
|
|
||||||
|
刷新页面后应该看到:
|
||||||
|
|
||||||
|
1. ✅ 底部 TabBar 显示 5 个图标(首页、测名、起名、改名、我的)
|
||||||
|
2. ✅ 图标颜色正确(未选中灰色,选中红色)
|
||||||
|
3. ✅ 轮播消息垂直滚动,不再挤在一起
|
||||||
|
4. ✅ 圆点有呼吸动画效果
|
||||||
|
5. ✅ 点击 TabBar 可以切换页面
|
||||||
|
|
||||||
|
## 浏览器兼容性
|
||||||
|
|
||||||
|
- ✅ Chrome/Edge (现代浏览器)
|
||||||
|
- ✅ Firefox
|
||||||
|
- ✅ Safari
|
||||||
|
- ✅ 移动端浏览器
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 所有图标使用 SVG,确保清晰度
|
||||||
|
2. 使用 `currentColor` 继承父元素颜色
|
||||||
|
3. CSS 动画性能优秀,无需 JavaScript
|
||||||
|
4. 所有单位使用 px,避免 rpx 转换问题
|
||||||
|
|
||||||
|
## 如果问题仍然存在
|
||||||
|
|
||||||
|
1. 硬刷新浏览器(Ctrl+Shift+R 或 Cmd+Shift+R)
|
||||||
|
2. 清除浏览器缓存
|
||||||
|
3. 检查浏览器控制台是否有错误
|
||||||
|
4. 确认开发服务器已重启
|
||||||
643
前端源码/uni-app/affinity-api-spec.json
Normal file
643
前端源码/uni-app/affinity-api-spec.json
Normal file
@@ -0,0 +1,643 @@
|
|||||||
|
{
|
||||||
|
"缘分合盘接口规范": {
|
||||||
|
"接口名称": "缘分合盘测算",
|
||||||
|
"接口路径": "/api/affinity/calculate",
|
||||||
|
"请求方法": "POST",
|
||||||
|
"入参 (Request)": {
|
||||||
|
"relationship": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "缘分类型",
|
||||||
|
"enum": [
|
||||||
|
"couple",
|
||||||
|
"married",
|
||||||
|
"crush",
|
||||||
|
"partner",
|
||||||
|
"friend",
|
||||||
|
"family"
|
||||||
|
],
|
||||||
|
"enum_labels": {
|
||||||
|
"couple": "情侣",
|
||||||
|
"married": "夫妻",
|
||||||
|
"crush": "暗恋",
|
||||||
|
"partner": "合伙",
|
||||||
|
"friend": "知己",
|
||||||
|
"family": "亲缘"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"person1": {
|
||||||
|
"type": "object",
|
||||||
|
"required": true,
|
||||||
|
"description": "甲方信息(君)",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "姓名",
|
||||||
|
"example": "张三"
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "性别",
|
||||||
|
"enum": [
|
||||||
|
"male",
|
||||||
|
"female"
|
||||||
|
],
|
||||||
|
"enum_labels": {
|
||||||
|
"male": "郎君",
|
||||||
|
"female": "佳人"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"birthDate": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "生辰八字(显示格式)",
|
||||||
|
"example": "1990年1月1日 子时"
|
||||||
|
},
|
||||||
|
"birthDateApi": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "生辰八字(API格式)",
|
||||||
|
"format": "YYYY-MM-DD HH:mm:ss",
|
||||||
|
"example": "1990-01-01 00:30:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"person2": {
|
||||||
|
"type": "object",
|
||||||
|
"required": true,
|
||||||
|
"description": "乙方信息(卿)",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "姓名",
|
||||||
|
"example": "李四"
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "性别",
|
||||||
|
"enum": [
|
||||||
|
"male",
|
||||||
|
"female"
|
||||||
|
],
|
||||||
|
"enum_labels": {
|
||||||
|
"male": "郎君",
|
||||||
|
"female": "佳人"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"birthDate": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "生辰八字(显示格式)",
|
||||||
|
"example": "1992年3月15日 午时"
|
||||||
|
},
|
||||||
|
"birthDateApi": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "生辰八字(API格式)",
|
||||||
|
"format": "YYYY-MM-DD HH:mm:ss",
|
||||||
|
"example": "1992-03-15 12:30:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"入参示例": {
|
||||||
|
"relationship": "couple",
|
||||||
|
"person1": {
|
||||||
|
"name": "张三",
|
||||||
|
"gender": "male",
|
||||||
|
"birthDate": "1990年1月1日 子时",
|
||||||
|
"birthDateApi": "1990-01-01 00:30:00"
|
||||||
|
},
|
||||||
|
"person2": {
|
||||||
|
"name": "李四",
|
||||||
|
"gender": "female",
|
||||||
|
"birthDate": "1992年3月15日 午时",
|
||||||
|
"birthDateApi": "1992-03-15 12:30:00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"出参 (Response)": {
|
||||||
|
"code": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "状态码,200表示成功"
|
||||||
|
},
|
||||||
|
"msg": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "返回消息"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "缘分合盘结果数据",
|
||||||
|
"properties": {
|
||||||
|
"relationship": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "缘分类型",
|
||||||
|
"example": "couple"
|
||||||
|
},
|
||||||
|
"relationshipLabel": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "缘分类型标签",
|
||||||
|
"example": "情侣"
|
||||||
|
},
|
||||||
|
"person1": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "甲方信息",
|
||||||
|
"properties": {
|
||||||
|
"name": "string",
|
||||||
|
"gender": "string",
|
||||||
|
"birthDate": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"person2": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "乙方信息",
|
||||||
|
"properties": {
|
||||||
|
"name": "string",
|
||||||
|
"gender": "string",
|
||||||
|
"birthDate": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"score": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "默契指数总分",
|
||||||
|
"example": 88,
|
||||||
|
"range": "0-100"
|
||||||
|
},
|
||||||
|
"scoreBadge": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "婚配等级",
|
||||||
|
"example": "上上婚",
|
||||||
|
"enum": [
|
||||||
|
"上上婚",
|
||||||
|
"上等婚",
|
||||||
|
"中等婚",
|
||||||
|
"下等婚"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sixDimension": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "六维契合度评分",
|
||||||
|
"properties": {
|
||||||
|
"emotional": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "情感共鸣",
|
||||||
|
"example": 92,
|
||||||
|
"range": "0-100"
|
||||||
|
},
|
||||||
|
"communication": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "沟通模式",
|
||||||
|
"example": 78,
|
||||||
|
"range": "0-100"
|
||||||
|
},
|
||||||
|
"trust": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "信任安全",
|
||||||
|
"example": 95,
|
||||||
|
"range": "0-100"
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "价值观",
|
||||||
|
"example": 85,
|
||||||
|
"range": "0-100"
|
||||||
|
},
|
||||||
|
"compatibility": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "互补性",
|
||||||
|
"example": 88,
|
||||||
|
"range": "0-100"
|
||||||
|
},
|
||||||
|
"luck": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "互旺运势",
|
||||||
|
"example": 88,
|
||||||
|
"range": "0-100"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"radarDesc": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "六维雷达图描述",
|
||||||
|
"example": "双方在信任与情感维度表现卓越,虽在沟通方式上略有差异,但互补性极强。"
|
||||||
|
},
|
||||||
|
"analysisCards": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "分析卡片列表",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "卡片ID",
|
||||||
|
"example": "emotional"
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "图标emoji",
|
||||||
|
"example": "💗"
|
||||||
|
},
|
||||||
|
"iconBg": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "图标背景色",
|
||||||
|
"example": "rgba(236, 72, 153, 0.2)"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "标题",
|
||||||
|
"example": "情感共鸣"
|
||||||
|
},
|
||||||
|
"score": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "评分",
|
||||||
|
"example": "92"
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "摘要(折叠时显示)",
|
||||||
|
"example": "两人的情感连接深厚,前世羁绊颇深。在相处中,往往能心领神会,无需多言。"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "详细内容(展开时显示)",
|
||||||
|
"example": "从八字命盘来看,男方日元为甲木,女方日元为己土,甲己相合,为中正之合..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unlocked": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "解锁后的深度内容",
|
||||||
|
"properties": {
|
||||||
|
"pastLife": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "前世羁绊",
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "前世关系描述",
|
||||||
|
"example": "根据三世书推演,你们二人前世曾是同门师兄妹..."
|
||||||
|
},
|
||||||
|
"depth": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "缘分深浅",
|
||||||
|
"example": "三生石上旧精魂"
|
||||||
|
},
|
||||||
|
"relationship": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "还债关系",
|
||||||
|
"example": "互不相欠 · 共同成长"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"matchItems": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "正缘特征验证",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"label": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "验证项",
|
||||||
|
"example": "外貌特征"
|
||||||
|
},
|
||||||
|
"match": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "契合度百分比",
|
||||||
|
"example": 90,
|
||||||
|
"range": "0-100"
|
||||||
|
},
|
||||||
|
"desc": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "描述",
|
||||||
|
"example": "命中注定伴侣多为身材高大、眉眼深邃之人,与对方特征高度吻合。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"guide": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "潜意识与相处指南",
|
||||||
|
"properties": {
|
||||||
|
"realNeeds": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "TA的真实需求",
|
||||||
|
"example": "表面上TA看起来很独立、无所谓,其实内心极度渴望被坚定地选择..."
|
||||||
|
},
|
||||||
|
"redZone": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "绝对雷区",
|
||||||
|
"example": "千万不要在公开场合质疑TA的决定,或者拿TA与前任/别人做比较..."
|
||||||
|
},
|
||||||
|
"tips": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "如何拿捏TA",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"适当示弱:TA有很强的保护欲,你的偶尔依赖会让TA成就感爆棚。",
|
||||||
|
"制造反差:在平淡生活中突然制造一个小惊喜,能让TA记很久。"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"monthlyFortune": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "未来12个月感情运势",
|
||||||
|
"properties": {
|
||||||
|
"heats": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "12个月的热度值(0-3)",
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"heatLabels": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "热度标签",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"平",
|
||||||
|
"吉",
|
||||||
|
"大吉",
|
||||||
|
"如意"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"highlights": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "高光时刻提示",
|
||||||
|
"example": "3月、4月、10月是感情升温的最佳窗口期。"
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "预警时刻提示",
|
||||||
|
"example": "7月需防范外界诱惑或误会。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timeline": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "未来十年关键节点",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"year": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "年份",
|
||||||
|
"example": "2025"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "阶段标题",
|
||||||
|
"example": "升温期"
|
||||||
|
},
|
||||||
|
"desc": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "描述",
|
||||||
|
"example": "感情运势大吉,适合谈婚论嫁或共同置业。"
|
||||||
|
},
|
||||||
|
"highlight": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "是否高亮显示",
|
||||||
|
"example": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"masterAdvice": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "大师寄语与化解之道",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "寄语",
|
||||||
|
"example": "虽然你们是上上婚配,但仍需注意农历五月和十一月,这两个月份情绪易波动。"
|
||||||
|
},
|
||||||
|
"tips": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "开运建议",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"家中东南方摆放一对木质鸳鸯摆件,可增进感情。",
|
||||||
|
"多佩戴红色或紫色饰品,以火生土,旺运旺财。"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isUnlocked": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "是否已解锁深度报告",
|
||||||
|
"example": false
|
||||||
|
},
|
||||||
|
"unlockPrice": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "解锁价格(元)",
|
||||||
|
"example": 9.9
|
||||||
|
},
|
||||||
|
"unlockStats": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "解锁统计信息",
|
||||||
|
"properties": {
|
||||||
|
"unlockCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "已解锁人数",
|
||||||
|
"example": 12392
|
||||||
|
},
|
||||||
|
"accuracy": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "准确率",
|
||||||
|
"example": "98%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"出参示例": {
|
||||||
|
"code": 200,
|
||||||
|
"msg": "测算成功",
|
||||||
|
"data": {
|
||||||
|
"relationship": "couple",
|
||||||
|
"relationshipLabel": "情侣",
|
||||||
|
"person1": {
|
||||||
|
"name": "张三",
|
||||||
|
"gender": "male",
|
||||||
|
"birthDate": "1990年1月1日 子时"
|
||||||
|
},
|
||||||
|
"person2": {
|
||||||
|
"name": "李四",
|
||||||
|
"gender": "female",
|
||||||
|
"birthDate": "1992年3月15日 午时"
|
||||||
|
},
|
||||||
|
"score": 88,
|
||||||
|
"scoreBadge": "上上婚",
|
||||||
|
"sixDimension": {
|
||||||
|
"emotional": 92,
|
||||||
|
"communication": 78,
|
||||||
|
"trust": 95,
|
||||||
|
"values": 85,
|
||||||
|
"compatibility": 88,
|
||||||
|
"luck": 88
|
||||||
|
},
|
||||||
|
"radarDesc": "双方在信任与情感维度表现卓越,虽在沟通方式上略有差异,但互补性极强。",
|
||||||
|
"analysisCards": [
|
||||||
|
{
|
||||||
|
"id": "emotional",
|
||||||
|
"icon": "💗",
|
||||||
|
"iconBg": "rgba(236, 72, 153, 0.2)",
|
||||||
|
"title": "情感共鸣",
|
||||||
|
"score": "92",
|
||||||
|
"summary": "两人的情感连接深厚,前世羁绊颇深。在相处中,往往能心领神会,无需多言。",
|
||||||
|
"content": "从八字命盘来看,男方日元为甲木,女方日元为己土,甲己相合,为中正之合。这意味着你们在精神层面有着极高的共鸣度。\n\n前世羁绊:\n你们的缘分并非始于今生。星盘显示,你们在前世可能是一对共同经历过患难的知己。这种深刻的灵魂印记,让你们在今生初见时就有一种莫名的熟悉感。\n\n情感模式:\n你们的相处模式属于"润物细无声"的类型。虽然没有轰轰烈烈的开场,但随着时间的推移,感情会像陈年老酒一样越发醇厚。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "communication",
|
||||||
|
"icon": "💬",
|
||||||
|
"iconBg": "rgba(59, 130, 246, 0.2)",
|
||||||
|
"title": "沟通模式",
|
||||||
|
"score": "78",
|
||||||
|
"summary": "沟通上存在"一动一静"的格局。建议多换位思考,避免因表达节奏不同产生误解。",
|
||||||
|
"content": "你们的沟通宫位呈现出互补但偶尔摩擦的状态。\n\n思维差异:\n男方偏向于逻辑思维,遇到问题倾向于直接寻找解决方案;女方则更偏向于感受思维,更看重解决问题过程中的情绪价值。\n\n建议:\n1. 设立"冷静角":当发生争执时,不要急于说服对方,先各自冷静15分钟。\n2. 倾听练习:每周抽出半小时,只倾听对方这一周的喜怒哀乐,不作评判,只给拥抱。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trust",
|
||||||
|
"icon": "🛡️",
|
||||||
|
"iconBg": "rgba(34, 197, 94, 0.2)",
|
||||||
|
"title": "安全感与信任",
|
||||||
|
"score": "95",
|
||||||
|
"summary": "这是这段关系最坚固的基石。双方都极具责任感,能给予对方无条件的支持。",
|
||||||
|
"content": "信任宫位有吉星高照,显示出极其稳固的信任基础。\n\n忠诚度分析:\n双方命盘中均无明显的烂桃花干扰。男方责任感重,视家庭为奋斗的动力;女方心思细腻,能将家庭打理得井井有条。\n\n加分项:\n在财务管理上,你们有着惊人的一致性。这种在金钱观上的契合,大大减少了现实生活中的摩擦。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "luck",
|
||||||
|
"icon": "⚡",
|
||||||
|
"iconBg": "rgba(234, 179, 8, 0.2)",
|
||||||
|
"title": "互相旺运",
|
||||||
|
"score": "88",
|
||||||
|
"summary": "五行互补,结合后尤其对男方的事业运有显著提升,女方财运亦会水涨船高。",
|
||||||
|
"content": "你们是典型的"互旺"组合。\n\n五行分析:\n男方喜火,女方八字火旺,这就像是在寒冷的冬日里送来了一盆炭火,能极大地激发男方的潜能。\n\n财运影响:\n女方自带"聚宝盆"属性,能守住男方打拼来的财富。二人的结合实现了"开源节流"的最佳配置。"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unlocked": {
|
||||||
|
"pastLife": {
|
||||||
|
"description": "根据三世书推演,你们二人前世曾是同门师兄妹。彼时情深缘浅,因世俗礼教未能终成眷属,留下了深刻的遗憾。这份未了的情缘穿越轮回,在今生化作了你们初见时的"一见如故"。",
|
||||||
|
"depth": "三生石上旧精魂",
|
||||||
|
"relationship": "互不相欠 · 共同成长"
|
||||||
|
},
|
||||||
|
"matchItems": [
|
||||||
|
{
|
||||||
|
"label": "外貌特征",
|
||||||
|
"match": 90,
|
||||||
|
"desc": "命中注定伴侣多为身材高大、眉眼深邃之人,与对方特征高度吻合。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "性格互补",
|
||||||
|
"match": 85,
|
||||||
|
"desc": "您喜静,对方喜动,正符合"一阴一阳谓之道"的最佳互补模型。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "出现时机",
|
||||||
|
"match": 95,
|
||||||
|
"desc": "红鸾星动之年相遇,是天作之合的典型征兆。"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"guide": {
|
||||||
|
"realNeeds": "表面上TA看起来很独立、无所谓,其实内心极度渴望被坚定地选择。TA最怕的不是争吵,而是冷暴力和被忽略。",
|
||||||
|
"redZone": "千万不要在公开场合质疑TA的决定,或者拿TA与前任/别人做比较。这会瞬间触犯TA的自尊底线。",
|
||||||
|
"tips": [
|
||||||
|
"适当示弱:TA有很强的保护欲,你的偶尔依赖会让TA成就感爆棚。",
|
||||||
|
"制造反差:在平淡生活中突然制造一个小惊喜,能让TA记很久。"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"monthlyFortune": {
|
||||||
|
"heats": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"heatLabels": [
|
||||||
|
"平",
|
||||||
|
"吉",
|
||||||
|
"大吉",
|
||||||
|
"如意"
|
||||||
|
],
|
||||||
|
"highlights": "3月、4月、10月是感情升温的最佳窗口期。",
|
||||||
|
"warnings": "7月需防范外界诱惑或误会。"
|
||||||
|
},
|
||||||
|
"timeline": [
|
||||||
|
{
|
||||||
|
"year": "2024",
|
||||||
|
"title": "磨合期",
|
||||||
|
"desc": "需注意口舌之争,多包容对方的小缺点。",
|
||||||
|
"highlight": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"year": "2025",
|
||||||
|
"title": "升温期",
|
||||||
|
"desc": "感情运势大吉,适合谈婚论嫁或共同置业。",
|
||||||
|
"highlight": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"year": "2027",
|
||||||
|
"title": "子女缘",
|
||||||
|
"desc": "添丁进口之喜,家庭运势达到顶峰。",
|
||||||
|
"highlight": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"year": "2030",
|
||||||
|
"title": "稳定期",
|
||||||
|
"desc": "事业有成,二人重心回归家庭,享受生活。",
|
||||||
|
"highlight": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"masterAdvice": {
|
||||||
|
"message": "虽然你们是上上婚配,但仍需注意农历五月和十一月,这两个月份情绪易波动。",
|
||||||
|
"tips": [
|
||||||
|
"家中东南方摆放一对木质鸳鸯摆件,可增进感情。",
|
||||||
|
"多佩戴红色或紫色饰品,以火生土,旺运旺财。"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isUnlocked": false,
|
||||||
|
"unlockPrice": 9.9,
|
||||||
|
"unlockStats": {
|
||||||
|
"unlockCount": 12392,
|
||||||
|
"accuracy": "98%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
304
前端源码/uni-app/api/affinity.ts
Normal file
304
前端源码/uni-app/api/affinity.ts
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
/**
|
||||||
|
* 缘分合盘 API
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 请求参数类型
|
||||||
|
export interface AffinityPersonInfo {
|
||||||
|
name: string;
|
||||||
|
gender: 'male' | 'female';
|
||||||
|
birth_date: string; // 显示格式:1990年1月1日 子时
|
||||||
|
birth_date_api: string; // API格式:1990-01-01 00:30:00
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AffinityCalculateRequest {
|
||||||
|
relationship: 'couple' | 'married' | 'crush' | 'partner' | 'friend' | 'family';
|
||||||
|
person1: AffinityPersonInfo;
|
||||||
|
person2: AffinityPersonInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应数据类型
|
||||||
|
export interface AffinitySixDimension {
|
||||||
|
personality: number; // 性格
|
||||||
|
emotion: number; // 情感
|
||||||
|
family: number; // 家庭
|
||||||
|
wealth: number; // 财运
|
||||||
|
career: number; // 事业
|
||||||
|
health: number; // 健康
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AffinityAnalysisCard {
|
||||||
|
id: string;
|
||||||
|
icon: string;
|
||||||
|
iconBg: string;
|
||||||
|
title: string;
|
||||||
|
score: string;
|
||||||
|
summary: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AffinityPastLife {
|
||||||
|
description: string;
|
||||||
|
depth: string;
|
||||||
|
relationship: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AffinityMatchItem {
|
||||||
|
label: string;
|
||||||
|
match: number;
|
||||||
|
desc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AffinityGuide {
|
||||||
|
realNeeds: string;
|
||||||
|
redZone: string;
|
||||||
|
tips: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AffinityMonthlyFortune {
|
||||||
|
heats: number[];
|
||||||
|
heatLabels: string[];
|
||||||
|
highlights: string;
|
||||||
|
warnings: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AffinityTimelineItem {
|
||||||
|
year: string;
|
||||||
|
title: string;
|
||||||
|
desc: string;
|
||||||
|
highlight: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AffinityMasterAdvice {
|
||||||
|
message: string;
|
||||||
|
tips: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AffinityUnlockedContent {
|
||||||
|
pastLife: AffinityPastLife;
|
||||||
|
matchItems: AffinityMatchItem[];
|
||||||
|
guide: AffinityGuide;
|
||||||
|
monthlyFortune: AffinityMonthlyFortune;
|
||||||
|
timeline: AffinityTimelineItem[];
|
||||||
|
masterAdvice: AffinityMasterAdvice;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AffinityCalculateResponse {
|
||||||
|
relationship: string;
|
||||||
|
relationshipLabel: string;
|
||||||
|
person1: {
|
||||||
|
name: string;
|
||||||
|
gender: string;
|
||||||
|
birthDate: string;
|
||||||
|
};
|
||||||
|
person2: {
|
||||||
|
name: string;
|
||||||
|
gender: string;
|
||||||
|
birthDate: string;
|
||||||
|
};
|
||||||
|
score: number;
|
||||||
|
scoreBadge: string;
|
||||||
|
sixDimension: AffinitySixDimension;
|
||||||
|
radarDesc: string;
|
||||||
|
analysisCards: AffinityAnalysisCard[];
|
||||||
|
unlocked: AffinityUnlockedContent;
|
||||||
|
isUnlocked: boolean;
|
||||||
|
unlockPrice: number;
|
||||||
|
unlockStats: {
|
||||||
|
unlockCount: number;
|
||||||
|
accuracy: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表查询相关类型
|
||||||
|
export interface AffinityListRequest {
|
||||||
|
page_no?: number;
|
||||||
|
page_size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AffinityListItem {
|
||||||
|
id: number;
|
||||||
|
uuid: string;
|
||||||
|
user_id: number;
|
||||||
|
relationship: string;
|
||||||
|
person1_name: string;
|
||||||
|
person1_gender: string;
|
||||||
|
person1_birth_date: string;
|
||||||
|
person1_birth_date_api: string;
|
||||||
|
person2_name: string;
|
||||||
|
person2_gender: string;
|
||||||
|
person2_birth_date: string;
|
||||||
|
person2_birth_date_api: string;
|
||||||
|
score: number | null;
|
||||||
|
score_badge: string | null;
|
||||||
|
six_dimension: string | null;
|
||||||
|
radar_desc: string | null;
|
||||||
|
analysis_cards: string | null;
|
||||||
|
is_unlocked: number;
|
||||||
|
unlock_price: number;
|
||||||
|
unlocked_content: string | null;
|
||||||
|
task_status: number;
|
||||||
|
error_message: string | null;
|
||||||
|
created_time: string;
|
||||||
|
updated_time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AffinityListResponse {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
data: {
|
||||||
|
page_no: number;
|
||||||
|
page_size: number;
|
||||||
|
total: number;
|
||||||
|
has_next: boolean;
|
||||||
|
data: AffinityListItem[];
|
||||||
|
};
|
||||||
|
status_code: number;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缘分合盘测算
|
||||||
|
*/
|
||||||
|
export async function calculateAffinity(params: AffinityCalculateRequest): Promise<AffinityCalculateResponse> {
|
||||||
|
// 获取token(避免JSON解析错误)
|
||||||
|
let token = '';
|
||||||
|
try {
|
||||||
|
// 尝试从localStorage获取(Web环境)
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
token = localStorage.getItem('token') || '';
|
||||||
|
// 如果token是JSON字符串,解析它
|
||||||
|
if (token.startsWith('"') && token.endsWith('"')) {
|
||||||
|
token = JSON.parse(token);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// uni-app环境,直接获取字符串
|
||||||
|
token = uni.getStorageSync('token') || '';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取token失败:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await uni.request({
|
||||||
|
url: 'https://yifan.action-ai.cn/api/v1/yifan/yifan_affinity/calculate',
|
||||||
|
method: 'POST',
|
||||||
|
header: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': token ? `Bearer ${token}` : '',
|
||||||
|
},
|
||||||
|
data: params,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status_code !== 200) {
|
||||||
|
throw new Error(response.data?.msg || '网络请求失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理认证失败,跳转到登录页(必须在检查code之前)
|
||||||
|
if (response.data?.msg === '认证失败,请登录后再试') {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请先登录',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.reLaunch({
|
||||||
|
url: '/pages/login/login',
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
throw new Error(response.data.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data.code !== 200) {
|
||||||
|
throw new Error(response.data.msg || '测算失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询合盘列表
|
||||||
|
*/
|
||||||
|
export async function getAffinityList(params: AffinityListRequest = {}): Promise<AffinityListResponse | null> {
|
||||||
|
// 获取token(避免JSON解析错误)
|
||||||
|
let token = '';
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
token = localStorage.getItem('token') || '';
|
||||||
|
// 如果token是JSON字符串,解析它
|
||||||
|
if (token.startsWith('"') && token.endsWith('"')) {
|
||||||
|
token = token.slice(1, -1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// uni-app环境,直接获取字符串
|
||||||
|
token = uni.getStorageSync('token') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const { page_no = 1, page_size = 10 } = params;
|
||||||
|
|
||||||
|
const response = await uni.request({
|
||||||
|
url: `https://yifan.action-ai.cn/api/v1/yifan/yifan_affinity/list?page_no=${page_no}&page_size=${page_size}`,
|
||||||
|
method: 'GET',
|
||||||
|
header: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': token ? `Bearer ${token}` : '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data?.msg === '认证失败,请登录后再试') {
|
||||||
|
uni.showToast({ title: '请先登录', icon: 'none', duration: 2000 });
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.reLaunch({ url: '/pages/login/login' });
|
||||||
|
}, 2000);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data.code == 0) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据id获取缘分合盘详情
|
||||||
|
*/
|
||||||
|
export async function getAffinityDetail(id: number) {
|
||||||
|
let token = '';
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
token = localStorage.getItem('token') || '';
|
||||||
|
if (token.startsWith('"') && token.endsWith('"')) {
|
||||||
|
token = token.slice(1, -1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
token = uni.getStorageSync('token') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await uni.request({
|
||||||
|
url: `https://yifan.action-ai.cn/api/v1/yifan/yifan_affinity/${id}`,
|
||||||
|
method: 'GET',
|
||||||
|
header: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': token ? `Bearer ${token}` : '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data?.msg === '认证失败,请登录后再试') {
|
||||||
|
uni.showToast({ title: '请先登录', icon: 'none', duration: 2000 });
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.reLaunch({ url: '/pages/login/login' });
|
||||||
|
}, 2000);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data.code == 0) {
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const affinityApi = {
|
||||||
|
calculateAffinity,
|
||||||
|
getAffinityList,
|
||||||
|
getAffinityDetail,
|
||||||
|
};
|
||||||
221
前端源码/uni-app/api/bazi-zeji.ts
Normal file
221
前端源码/uni-app/api/bazi-zeji.ts
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
/**
|
||||||
|
* 八字择吉 API
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 请求参数类型
|
||||||
|
export interface BaziZejiCalculateRequest {
|
||||||
|
name: string;
|
||||||
|
gender: 'male' | 'female';
|
||||||
|
birth_date: string; // 显示格式:1999年11月15日 子时
|
||||||
|
birth_date_api: string; // API格式:1999-11-15 00:30:00
|
||||||
|
birth_place: string; // 出生地
|
||||||
|
zeji_type: 'wedding' | 'business' | 'move' | 'travel' | 'investment' | 'surgery' | 'contract' | 'other';
|
||||||
|
zeji_purpose: string; // 择吉目的描述
|
||||||
|
date_range_start: string; // 期望日期范围开始 YYYY-MM-DD
|
||||||
|
date_range_end: string; // 期望日期范围结束 YYYY-MM-DD
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应数据类型
|
||||||
|
export interface BaziZejiDate {
|
||||||
|
date: string; // 日期 YYYY-MM-DD
|
||||||
|
lunar: string; // 农历日期
|
||||||
|
desc: string; // 日期描述
|
||||||
|
score: number; // 吉运指数
|
||||||
|
hours: string; // 吉时
|
||||||
|
clash: string; // 冲煞
|
||||||
|
suitable: string[]; // 宜
|
||||||
|
avoid: string[]; // 忌
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaziZejiCalculateResponse {
|
||||||
|
name: string;
|
||||||
|
gender: string;
|
||||||
|
birth_date: string;
|
||||||
|
birth_place: string;
|
||||||
|
zeji_type: string;
|
||||||
|
zeji_purpose: string;
|
||||||
|
date_range_start: string;
|
||||||
|
date_range_end: string;
|
||||||
|
advice: string; // 择吉建议
|
||||||
|
dates: BaziZejiDate[]; // 吉日列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表查询相关类型
|
||||||
|
export interface BaziZejiListRequest {
|
||||||
|
page_no?: number;
|
||||||
|
page_size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaziZejiListItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
gender: string;
|
||||||
|
birth_date: string;
|
||||||
|
zeji_type: string;
|
||||||
|
zeji_type_label: string;
|
||||||
|
zeji_purpose: string;
|
||||||
|
date_range_start: string;
|
||||||
|
date_range_end: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaziZejiListResponse {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
data: {
|
||||||
|
page_no: number;
|
||||||
|
page_size: number;
|
||||||
|
total: number;
|
||||||
|
has_next: boolean;
|
||||||
|
data: BaziZejiListItem[];
|
||||||
|
};
|
||||||
|
status_code: number;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 八字择吉测算
|
||||||
|
*/
|
||||||
|
export async function calculateBaziZeji(params: BaziZejiCalculateRequest): Promise<BaziZejiCalculateResponse> {
|
||||||
|
// 获取token(避免JSON解析错误)
|
||||||
|
let token = '';
|
||||||
|
try {
|
||||||
|
// 尝试从localStorage获取(Web环境)
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
token = localStorage.getItem('token') || '';
|
||||||
|
// 如果token是JSON字符串,解析它
|
||||||
|
if (token.startsWith('"') && token.endsWith('"')) {
|
||||||
|
token = JSON.parse(token);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// uni-app环境,直接获取字符串
|
||||||
|
token = uni.getStorageSync('token') || '';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取token失败:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await uni.request({
|
||||||
|
url: 'https://yifan.action-ai.cn/api/v1/yifan/yifan_bazi_zeji/calculate',
|
||||||
|
method: 'POST',
|
||||||
|
header: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': token ? `Bearer ${token}` : '',
|
||||||
|
},
|
||||||
|
data: params,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status_code !== 200) {
|
||||||
|
throw new Error(response.data?.msg || '网络请求失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理认证失败,跳转到登录页
|
||||||
|
if (response.data?.msg === '认证失败,请登录后再试') {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请先登录',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.reLaunch({
|
||||||
|
url: '/pages/login/login',
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
throw new Error(response.data.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data.code !== 200) {
|
||||||
|
throw new Error(response.data.msg || '测算失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询八字择吉列表
|
||||||
|
*/
|
||||||
|
export async function getBaziZejiList(params: BaziZejiListRequest = {}): Promise<BaziZejiListResponse | null> {
|
||||||
|
// 获取token(避免JSON解析错误)
|
||||||
|
let token = '';
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
token = localStorage.getItem('token') || '';
|
||||||
|
// 如果token是JSON字符串,解析它
|
||||||
|
if (token.startsWith('"') && token.endsWith('"')) {
|
||||||
|
token = token.slice(1, -1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// uni-app环境,直接获取字符串
|
||||||
|
token = uni.getStorageSync('token') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const { page_no = 1, page_size = 10 } = params;
|
||||||
|
|
||||||
|
const response = await uni.request({
|
||||||
|
url: `https://yifan.action-ai.cn/api/v1/yifan/yifan_bazi_zeji/list?page_no=${page_no}&page_size=${page_size}`,
|
||||||
|
method: 'GET',
|
||||||
|
header: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': token ? `Bearer ${token}` : '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data?.msg === '认证失败,请登录后再试') {
|
||||||
|
uni.showToast({ title: '请先登录', icon: 'none', duration: 2000 });
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.reLaunch({ url: '/pages/login/login' });
|
||||||
|
}, 2000);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data.code == 0) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据id获取八字择吉详情
|
||||||
|
*/
|
||||||
|
export async function getBaziZejiDetail(id: number) {
|
||||||
|
let token = '';
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
token = localStorage.getItem('token') || '';
|
||||||
|
if (token.startsWith('"') && token.endsWith('"')) {
|
||||||
|
token = token.slice(1, -1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
token = uni.getStorageSync('token') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await uni.request({
|
||||||
|
url: `https://yifan.action-ai.cn/api/v1/yifan/yifan_bazi_zeji/${id}`,
|
||||||
|
method: 'GET',
|
||||||
|
header: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': token ? `Bearer ${token}` : '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data?.msg === '认证失败,请登录后再试') {
|
||||||
|
uni.showToast({ title: '请先登录', icon: 'none', duration: 2000 });
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.reLaunch({ url: '/pages/login/login' });
|
||||||
|
}, 2000);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data.code == 0) {
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const baziZejiApi = {
|
||||||
|
calculateBaziZeji,
|
||||||
|
getBaziZejiList,
|
||||||
|
getBaziZejiDetail,
|
||||||
|
};
|
||||||
119
前端源码/uni-app/api/cai-yun.ts
Normal file
119
前端源码/uni-app/api/cai-yun.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* 财运解析相关 API
|
||||||
|
*/
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export interface WealthAnalysisResponse {
|
||||||
|
jiexi_id: number;
|
||||||
|
report_id: number;
|
||||||
|
name: string;
|
||||||
|
wealth_score: number;
|
||||||
|
wealth_level: string;
|
||||||
|
wealth_trend: string;
|
||||||
|
yuedo_xiangpi?: any;
|
||||||
|
meiri_yuncheng?: any;
|
||||||
|
mingpan_jingpi: {
|
||||||
|
mingzao: string;
|
||||||
|
zhen_taiyang_shi: string;
|
||||||
|
nongli_shengchen: string;
|
||||||
|
qianshi_yinji: string;
|
||||||
|
jinsheng_keti: string;
|
||||||
|
bazi_paipan: {
|
||||||
|
nian: { gan: string; zhi: string };
|
||||||
|
yue: { gan: string; zhi: string };
|
||||||
|
ri: { gan: string; zhi: string };
|
||||||
|
shi: { gan: string; zhi: string };
|
||||||
|
};
|
||||||
|
mingge_cengci: string;
|
||||||
|
guji_duanyu: string;
|
||||||
|
qimen_paipan: string;
|
||||||
|
qimen_geju: string;
|
||||||
|
wuxing_nengliang: {
|
||||||
|
mu: number;
|
||||||
|
huo: number;
|
||||||
|
tu: number;
|
||||||
|
jin: number;
|
||||||
|
shui: number;
|
||||||
|
};
|
||||||
|
dashi_pizhu: string;
|
||||||
|
wuxing_kaiyun: string;
|
||||||
|
shishen_mangdian: string;
|
||||||
|
shiye_caiyun_dingshu: string;
|
||||||
|
hunyin_qinggan: string;
|
||||||
|
wuxing_jiankang: string;
|
||||||
|
shensha_guiren: string;
|
||||||
|
};
|
||||||
|
liunian_zongyun: {
|
||||||
|
dayun_zoushi: string;
|
||||||
|
taisui_jiangjun: string;
|
||||||
|
dangnian_liunian: string;
|
||||||
|
liunian_shensha: string;
|
||||||
|
fenqunti_zhuanyun: {
|
||||||
|
qingnian: string;
|
||||||
|
zhongnian: string;
|
||||||
|
laonian: string;
|
||||||
|
};
|
||||||
|
liunian_jixiong_fangwei: {
|
||||||
|
ji: string[];
|
||||||
|
xiong: string[];
|
||||||
|
};
|
||||||
|
caifu_laiyuan: string;
|
||||||
|
touzi_lingyu_zhiyin: {
|
||||||
|
fangdichan: string;
|
||||||
|
gupiao: string;
|
||||||
|
jijin: string;
|
||||||
|
huangjin: string;
|
||||||
|
};
|
||||||
|
dangnian_jiugong_feixing: string;
|
||||||
|
liunian_huajie: string;
|
||||||
|
};
|
||||||
|
fengshui_jinnang: {
|
||||||
|
guiren_huaxiang: string;
|
||||||
|
waiju_shaji_huajie: {
|
||||||
|
lu_chong: string;
|
||||||
|
jian_dao_sha: string;
|
||||||
|
fan_gong_sha: string;
|
||||||
|
};
|
||||||
|
jiaju_caiwei: {
|
||||||
|
ming_caiwei: string;
|
||||||
|
an_caiwei: string;
|
||||||
|
};
|
||||||
|
zhichang_gaosheng: string;
|
||||||
|
cuiwang_taohua: string;
|
||||||
|
jiaju_zhiwu: Array<{
|
||||||
|
name: string;
|
||||||
|
position: string;
|
||||||
|
effect: string;
|
||||||
|
}>;
|
||||||
|
mengchong_fengshui: string;
|
||||||
|
shuzi_nengliang: {
|
||||||
|
shouji_hao: string;
|
||||||
|
che_pai: string;
|
||||||
|
lou_ceng: string;
|
||||||
|
};
|
||||||
|
aiche_pingan: string;
|
||||||
|
xijin_qianbao: {
|
||||||
|
yanse: string;
|
||||||
|
zhidi: string;
|
||||||
|
shiyong: string;
|
||||||
|
};
|
||||||
|
xingyun_se_peidai: {
|
||||||
|
xingyun_se: string[];
|
||||||
|
peidai_shipin: Array<{
|
||||||
|
name: string;
|
||||||
|
effect: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
unlocked: null;
|
||||||
|
is_unlocked: boolean;
|
||||||
|
unlock_price: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据报告id获取财运解析结果
|
||||||
|
* @param reportId 报告ID
|
||||||
|
* @returns 财运解析结果
|
||||||
|
*/
|
||||||
|
export const getWealthAnalysisByReportId = (reportId: number) =>
|
||||||
|
request.get<WealthAnalysisResponse>(`/yifan_caiyun_jiexi/result/by-report/${reportId}`);
|
||||||
56
前端源码/uni-app/api/home.ts
Normal file
56
前端源码/uni-app/api/home.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* @Author: caoziyuan ziyuan.cao@zhuying.com
|
||||||
|
* @Date: 2026-01-13 14:31:18
|
||||||
|
* @LastEditors: aliyun4247073344 893643761@qq.com
|
||||||
|
* @LastEditTime: 2026-02-28 10:16:04
|
||||||
|
* @FilePath: \uni-app\api\home.ts
|
||||||
|
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 首页相关 API
|
||||||
|
*/
|
||||||
|
import { http } from '../utils/request';
|
||||||
|
import type { AboutUsInfo, AboutVideoInfo, CalendarInfo, RecommendedSolutionResponse, NoticeListResponse } from './types';
|
||||||
|
|
||||||
|
export const homePageApi = {
|
||||||
|
/**
|
||||||
|
* 获取文字介绍(关于我们)
|
||||||
|
*/
|
||||||
|
getAboutUs: () => {
|
||||||
|
return http.get<AboutUsInfo>('/yifan_about_us/miniapp/info');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取视频介绍
|
||||||
|
*/
|
||||||
|
getAboutVideo: () => {
|
||||||
|
return http.get<AboutVideoInfo>('/yifan_about_video/miniapp/info');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取今天日期(黄历)
|
||||||
|
*/
|
||||||
|
getCalendarToday: () => {
|
||||||
|
return http.get<CalendarInfo>('/yifan_naming_reports/calendar');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取方案推荐(佳名赏析)
|
||||||
|
*/
|
||||||
|
getRecommendedSolutions: (pageNo = 1, pageSize = 10) => {
|
||||||
|
return http.get<RecommendedSolutionResponse>('/yifan_naming_solutions/recommended', {
|
||||||
|
page_no: pageNo,
|
||||||
|
page_size: pageSize,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息轮播列表
|
||||||
|
*/
|
||||||
|
getNoticeList: (pageNo = 1, pageSize = 10) => {
|
||||||
|
return http.get<NoticeListResponse>('/notice/mini/list', {
|
||||||
|
page_no: pageNo,
|
||||||
|
page_size: pageSize,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
39
前端源码/uni-app/api/index.ts
Normal file
39
前端源码/uni-app/api/index.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* API 统一导出(当前仅保留真实的小程序登录接口)
|
||||||
|
*/
|
||||||
|
export * from './types';
|
||||||
|
export { userApi } from './user';
|
||||||
|
export { homePageApi } from './home';
|
||||||
|
export { namingApi } from './naming';
|
||||||
|
export { paymentApi } from './payment';
|
||||||
|
export { affinityApi } from './affinity';
|
||||||
|
export { baziZejiApi } from './bazi-zeji';
|
||||||
|
export type {
|
||||||
|
CompanyNamingRequest,
|
||||||
|
CompanyCoreMember,
|
||||||
|
CompanyNamingResponse,
|
||||||
|
SolutionDetailResponse,
|
||||||
|
CompanyAnalysisRequest,
|
||||||
|
CompanyAnalysisResponse,
|
||||||
|
CompanyAnalysisDetailNode,
|
||||||
|
CompanyAnalysisDetails,
|
||||||
|
} from './naming';
|
||||||
|
export type {
|
||||||
|
AffinityCalculateRequest,
|
||||||
|
AffinityCalculateResponse,
|
||||||
|
AffinityPersonInfo,
|
||||||
|
AffinitySixDimension,
|
||||||
|
AffinityAnalysisCard,
|
||||||
|
AffinityUnlockedContent,
|
||||||
|
AffinityListRequest,
|
||||||
|
AffinityListResponse,
|
||||||
|
AffinityListItem,
|
||||||
|
} from './affinity';
|
||||||
|
export type {
|
||||||
|
BaziZejiCalculateRequest,
|
||||||
|
BaziZejiCalculateResponse,
|
||||||
|
BaziZejiDate,
|
||||||
|
BaziZejiListRequest,
|
||||||
|
BaziZejiListResponse,
|
||||||
|
BaziZejiListItem,
|
||||||
|
} from './bazi-zeji';
|
||||||
350
前端源码/uni-app/api/naming.ts
Normal file
350
前端源码/uni-app/api/naming.ts
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
/*
|
||||||
|
* @Author: caoziyuan ziyuan.cao@zhuying.com
|
||||||
|
* @Date: 2026-01-14 09:43:15
|
||||||
|
* @LastEditors: caoziyuan ziyuan.cao@zhuying.com
|
||||||
|
* @LastEditTime: 2026-01-14 11:10:33
|
||||||
|
* @FilePath: \uni-app\api\naming.ts
|
||||||
|
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 起名相关 API
|
||||||
|
*/
|
||||||
|
import { http } from '../utils/request';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 起名/改名/测名返回的「方案 JSON」建议在根级包含五行生克与属相模块,字段约定见:
|
||||||
|
* `utils/backend-wuxing-zodiac-spec.ts`(bazi_name_fit、wuxing_bagua、zodiac_sign)。
|
||||||
|
* 生成提示词可用:`buildBackendWuxingZodiacPromptZh()`。
|
||||||
|
*/
|
||||||
|
export interface PersonalNamingRequest {
|
||||||
|
last_name: string;
|
||||||
|
gender: string;
|
||||||
|
birth_date: string;
|
||||||
|
birth_place: string;
|
||||||
|
style_label: string;
|
||||||
|
father_name?: string;
|
||||||
|
father_birth_date?: string;
|
||||||
|
mother_name?: string;
|
||||||
|
mother_birth_date?: string;
|
||||||
|
family_book?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PersonalNamingResponse {
|
||||||
|
report_id: number;
|
||||||
|
status?: number;
|
||||||
|
solutions?: Array<{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
pinyin?: string;
|
||||||
|
tags?: string[];
|
||||||
|
name_meaning?: string;
|
||||||
|
poetry_source?: string;
|
||||||
|
total_score?: number;
|
||||||
|
wuxing?: string;
|
||||||
|
zodiac?: string;
|
||||||
|
constellation?: string;
|
||||||
|
}>;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PersonalRenamingRequest {
|
||||||
|
last_name: string;
|
||||||
|
gender: string;
|
||||||
|
birth_date: string;
|
||||||
|
birth_place: string;
|
||||||
|
style_label: string;
|
||||||
|
father_name?: string;
|
||||||
|
father_birth_date?: string;
|
||||||
|
mother_name?: string;
|
||||||
|
mother_birth_date?: string;
|
||||||
|
family_book?: string;
|
||||||
|
original_name: string;
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 个人测名。建议后端同时接收:精确出生时辰、出生地/时区(若有),以便排出八字、喜用与生肖年柱;
|
||||||
|
* 详见 `utils/backend-wuxing-zodiac-spec.ts` 中 RECOMMENDED_PERSONAL_REQUEST_FIELDS。
|
||||||
|
*/
|
||||||
|
export interface PersonalScoringRequest {
|
||||||
|
surname: string;
|
||||||
|
given_name: string;
|
||||||
|
gender: string;
|
||||||
|
birthday: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公司起名核心成员
|
||||||
|
export interface CompanyCoreMember {
|
||||||
|
name: string;
|
||||||
|
birthday: string; // 格式: 'YYYY-MM-DD HH:mm:ss'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公司起名请求参数
|
||||||
|
export interface CompanyNamingRequest {
|
||||||
|
industry: string; // 行业,如 "科技"
|
||||||
|
address: string; // 地址,如 "临沂"
|
||||||
|
core_members: CompanyCoreMember[]; // 核心成员列表
|
||||||
|
report_id?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompanyRenamingRequest {
|
||||||
|
industry: string;
|
||||||
|
address: string;
|
||||||
|
core_members: CompanyCoreMember[];
|
||||||
|
vision: string;
|
||||||
|
preference: string;
|
||||||
|
original_name: string;
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompanyRenamingResponse {
|
||||||
|
report_id: number;
|
||||||
|
status?: number;
|
||||||
|
solutions?: Array<{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
pinyin?: string;
|
||||||
|
tags?: string[];
|
||||||
|
name_meaning?: string;
|
||||||
|
poetry_source?: string;
|
||||||
|
total_score?: number;
|
||||||
|
star_rating?: number;
|
||||||
|
is_recommended?: number;
|
||||||
|
}>;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公司起名响应 - request.ts 会解包,实际返回的是 data 部分
|
||||||
|
export interface CompanyNamingResponse {
|
||||||
|
report_id: number;
|
||||||
|
status: number;
|
||||||
|
solutions?: Array<{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
pinyin: string;
|
||||||
|
wuxing: string;
|
||||||
|
tags: string[];
|
||||||
|
name_meaning: string;
|
||||||
|
poetry_source: string;
|
||||||
|
total_score: number;
|
||||||
|
star_rating: number;
|
||||||
|
is_recommended: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方案详情响应 - request.ts 会解包,实际返回的是 data 部分
|
||||||
|
export interface SolutionDetailResponse {
|
||||||
|
id: number;
|
||||||
|
report_id: number;
|
||||||
|
name: string;
|
||||||
|
pinyin: string;
|
||||||
|
total_score: number;
|
||||||
|
star_rating: number;
|
||||||
|
wuxing: string;
|
||||||
|
shuxiang: string | null;
|
||||||
|
tags: string[];
|
||||||
|
name_meaning: string;
|
||||||
|
poetry_source: string;
|
||||||
|
is_recommended: number;
|
||||||
|
is_favorited: boolean;
|
||||||
|
sections: Array<{
|
||||||
|
id: number;
|
||||||
|
section_type: string;
|
||||||
|
title: string;
|
||||||
|
summary_data: any;
|
||||||
|
sort_order: number;
|
||||||
|
details: Array<{
|
||||||
|
id: number;
|
||||||
|
detail_type: string;
|
||||||
|
title: string;
|
||||||
|
content: any;
|
||||||
|
sort_order: number;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公司测名/企业运程(company_analysis)请求参数
|
||||||
|
export interface CompanyAnalysisRequest {
|
||||||
|
company_name: string;
|
||||||
|
industry: string;
|
||||||
|
address: string;
|
||||||
|
target_audience: string;
|
||||||
|
members: Array<{ name: string; birth_date: string }>; // 格式: 'YYYY-MM-DD HH:mm:ss'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompanyScoringRequest {
|
||||||
|
company_name: string;
|
||||||
|
industry: string;
|
||||||
|
address: string;
|
||||||
|
target_audience: string;
|
||||||
|
members: Array<{ name: string; birthday: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompanyAnalysisDetailNode =
|
||||||
|
| { type: 'text'; text: string }
|
||||||
|
| { type: 'list'; items: string[] }
|
||||||
|
| { type: 'kv'; items: Array<{ label: string; value: string }> };
|
||||||
|
|
||||||
|
export interface CompanyAnalysisDetails {
|
||||||
|
title: string;
|
||||||
|
nodes: CompanyAnalysisDetailNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公司测名/企业运程(company_analysis)响应 - request.ts 会解包,实际返回的是 data 部分
|
||||||
|
export interface CompanyAnalysisResponse {
|
||||||
|
header: {
|
||||||
|
name: string;
|
||||||
|
tagLeft: string;
|
||||||
|
tagRight: string;
|
||||||
|
score: number;
|
||||||
|
quote: string;
|
||||||
|
details?: CompanyAnalysisDetails;
|
||||||
|
};
|
||||||
|
characterAnalysis: {
|
||||||
|
characters: Array<{ char: string; stroke: number; element: string; meaning: string }>;
|
||||||
|
analysis: string;
|
||||||
|
details?: CompanyAnalysisDetails;
|
||||||
|
};
|
||||||
|
businessPattern: {
|
||||||
|
radar: { labels: string[]; values: number[] };
|
||||||
|
summary: Array<{ label: string; value: string }>;
|
||||||
|
details?: CompanyAnalysisDetails;
|
||||||
|
};
|
||||||
|
gua: {
|
||||||
|
bg: string;
|
||||||
|
name: string;
|
||||||
|
badge: string;
|
||||||
|
desc: string;
|
||||||
|
tags: string[];
|
||||||
|
insight: string;
|
||||||
|
details?: CompanyAnalysisDetails;
|
||||||
|
};
|
||||||
|
team: {
|
||||||
|
members: Array<{ role: string; score: number; desc: string; match: string }>;
|
||||||
|
note: string;
|
||||||
|
details?: CompanyAnalysisDetails;
|
||||||
|
};
|
||||||
|
years: {
|
||||||
|
items: Array<{ year: string; luck: string; text: string }>;
|
||||||
|
details?: CompanyAnalysisDetails;
|
||||||
|
};
|
||||||
|
wealthTrend: {
|
||||||
|
bars: number[];
|
||||||
|
note: string;
|
||||||
|
details?: CompanyAnalysisDetails;
|
||||||
|
};
|
||||||
|
direction: {
|
||||||
|
note: string;
|
||||||
|
goodDot?: { x: number; y: number };
|
||||||
|
details?: CompanyAnalysisDetails;
|
||||||
|
};
|
||||||
|
layout: {
|
||||||
|
items: Array<{
|
||||||
|
strong: string;
|
||||||
|
textBefore: string;
|
||||||
|
highlights?: string[];
|
||||||
|
textAfter: string;
|
||||||
|
}>;
|
||||||
|
details?: CompanyAnalysisDetails;
|
||||||
|
};
|
||||||
|
execution: {
|
||||||
|
text: string;
|
||||||
|
details?: CompanyAnalysisDetails;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NewCompanyRenamingRequest {
|
||||||
|
industry: string;
|
||||||
|
address: string;
|
||||||
|
core_members: CompanyCoreMember[];
|
||||||
|
vision: string;
|
||||||
|
preference: string;
|
||||||
|
original_name: string;
|
||||||
|
reason: string;
|
||||||
|
additional_info: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NewCompanyRenamingResponse {
|
||||||
|
report_id: number;
|
||||||
|
status?: number;
|
||||||
|
solutions?: Array<{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
pinyin?: string;
|
||||||
|
tags?: string[];
|
||||||
|
name_meaning?: string;
|
||||||
|
poetry_source?: string;
|
||||||
|
total_score?: number;
|
||||||
|
star_rating?: number;
|
||||||
|
is_recommended?: number;
|
||||||
|
}>;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const namingApi = {
|
||||||
|
personalNaming(params: PersonalNamingRequest): Promise<PersonalNamingResponse> {
|
||||||
|
return http.post('/yifan_naming_reports/personal-naming', params);
|
||||||
|
},
|
||||||
|
|
||||||
|
personalScoring(params: PersonalScoringRequest): Promise<any> {
|
||||||
|
return http.post('/yifan_naming_reports/personal-scoring', params);
|
||||||
|
},
|
||||||
|
|
||||||
|
personalScoringTrial(params: PersonalScoringRequest): Promise<any> {
|
||||||
|
return http.post('/yifan_naming_reports/personal-scoring-trial', params);
|
||||||
|
},
|
||||||
|
|
||||||
|
personalRenaming(params: PersonalRenamingRequest): Promise<PersonalNamingResponse> {
|
||||||
|
return http.post('/yifan_naming_reports/personal-renaming', params);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公司起名
|
||||||
|
*/
|
||||||
|
companyNaming(params: CompanyNamingRequest): Promise<CompanyNamingResponse> {
|
||||||
|
return http.post('/yifan_naming_reports/company-naming', params);
|
||||||
|
},
|
||||||
|
|
||||||
|
companyRenaming(params: CompanyRenamingRequest): Promise<CompanyRenamingResponse> {
|
||||||
|
return http.post('/yifan_naming_reports/company-renaming', params);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公司测名/企业运程
|
||||||
|
*/
|
||||||
|
companyAnalysis(params: CompanyAnalysisRequest): Promise<CompanyAnalysisResponse> {
|
||||||
|
return http.post('/yifan_naming_reports/company-analysis', params);
|
||||||
|
},
|
||||||
|
|
||||||
|
companyScoring(params: CompanyScoringRequest): Promise<any> {
|
||||||
|
return http.post('/yifan_naming_reports/company-scoring', params);
|
||||||
|
},
|
||||||
|
|
||||||
|
companyScoringTrial(params: CompanyScoringRequest): Promise<any> {
|
||||||
|
return http.post('/yifan_naming_reports/company-scoring-trial', params);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测名试用分享回调 - 好友通过分享链接进入小程序后,为分享者增加免费次数
|
||||||
|
*/
|
||||||
|
scoringTrialShareCallback(): Promise<any> {
|
||||||
|
return http.post('/yifan_naming_reports/scoring-trial-share-callback');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据方案ID获取详情
|
||||||
|
* @param solutionId 方案ID
|
||||||
|
*/
|
||||||
|
getSolutionDetail(solutionId: number | string): Promise<SolutionDetailResponse> {
|
||||||
|
return http.get(`/yifan_naming_solutions/full_detail/${solutionId}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据报告ID获取方案列表
|
||||||
|
* @param reportId 报告ID
|
||||||
|
*/
|
||||||
|
getSolutionsByReportId(reportId: number | string): Promise<any> {
|
||||||
|
return http.get(`/yifan_naming_reports/${reportId}/solutions`);
|
||||||
|
},
|
||||||
|
};
|
||||||
25
前端源码/uni-app/api/payment.ts
Normal file
25
前端源码/uni-app/api/payment.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* 支付相关接口
|
||||||
|
*/
|
||||||
|
import http from '@/utils/request';
|
||||||
|
import type { CreateOrderParams, CreateOrderResponse, QueryOrderResponse, CloseOrderResponse, ListPayOrdersParams, ListPayOrdersResponse } from './types';
|
||||||
|
|
||||||
|
export const paymentApi = {
|
||||||
|
// 创建支付订单
|
||||||
|
createOrder: (params: CreateOrderParams) =>
|
||||||
|
http.post<CreateOrderResponse>('/yifan_wx_pay/create-order', params, { showLoading: true }),
|
||||||
|
|
||||||
|
// 查询订单状态(使用路径参数)
|
||||||
|
queryOrder: (out_trade_no: string) =>
|
||||||
|
http.get<QueryOrderResponse>(`/yifan_wx_pay/query-order/${out_trade_no}`),
|
||||||
|
|
||||||
|
// 关闭订单(使用路径参数)
|
||||||
|
closeOrder: (out_trade_no: string) =>
|
||||||
|
http.post<CloseOrderResponse>(`/yifan_wx_pay/close-order/${out_trade_no}`, {}, { showLoading: true }),
|
||||||
|
|
||||||
|
// 获取支付订单列表
|
||||||
|
listOrders: (params?: ListPayOrdersParams) =>
|
||||||
|
http.get<ListPayOrdersResponse>('/yifan_wx_pay/list', params),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default paymentApi;
|
||||||
842
前端源码/uni-app/api/types.ts
Normal file
842
前端源码/uni-app/api/types.ts
Normal file
@@ -0,0 +1,842 @@
|
|||||||
|
/**
|
||||||
|
* 接口类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 首页滚动列表
|
||||||
|
export interface ActivityDisplayItem {
|
||||||
|
id: number;
|
||||||
|
user_name: string;
|
||||||
|
action: string;
|
||||||
|
service_name: string;
|
||||||
|
service_type?: string;
|
||||||
|
sort_order?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 万年历
|
||||||
|
export interface CalendarToday {
|
||||||
|
lunar: string; // 农历日期,如 "腊月 十二"
|
||||||
|
year: string; // 年份,如 "甲辰年"
|
||||||
|
solar: string; // 公历日期,如 "2024.01.22"
|
||||||
|
yi: string[]; // 宜
|
||||||
|
ji: string[]; // 忌
|
||||||
|
}
|
||||||
|
|
||||||
|
// 黄历信息(接口返回)
|
||||||
|
export interface CalendarInfo {
|
||||||
|
lunar_day: string; // 农历日,如 "一"
|
||||||
|
lunar_month: string; // 农历月,如 "正月"
|
||||||
|
lunar_year: string; // 农历年,如 "丙午年"
|
||||||
|
solar_date: string; // 公历日期,如 "2026.01.14"
|
||||||
|
solar_year: number; // 公历年
|
||||||
|
solar_month: number; // 公历月
|
||||||
|
solar_day: number; // 公历日
|
||||||
|
ganzhi_year: string; // 干支年,如 "丙午"
|
||||||
|
ganzhi_month: string; // 干支月,如 "庚寅"
|
||||||
|
ganzhi_day: string; // 干支日,如 "戊子"
|
||||||
|
zodiac: string; // 生肖,如 "马"
|
||||||
|
yi: string[]; // 宜
|
||||||
|
ji: string[]; // 忌
|
||||||
|
weekday: string; // 星期,如 "星期三"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 佳名赏析
|
||||||
|
export interface GoodNameItem {
|
||||||
|
id: number;
|
||||||
|
name: string; // 名字
|
||||||
|
source: string; // 出处
|
||||||
|
desc: string; // 描述
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方案推荐(佳名赏析)
|
||||||
|
export interface RecommendedSolution {
|
||||||
|
id: number;
|
||||||
|
name: string; // 名字
|
||||||
|
pinyin?: string; // 拼音
|
||||||
|
name_meaning?: string; // 寓意
|
||||||
|
poetry_source?: string; // 诗词出处
|
||||||
|
total_score?: number; // 总分
|
||||||
|
star_rating?: number; // 星级
|
||||||
|
wuxing?: string; // 五行
|
||||||
|
shuxiang?: string; // 属相
|
||||||
|
tags?: string[]; // 标签
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方案推荐分页响应
|
||||||
|
export interface RecommendedSolutionResponse {
|
||||||
|
total: number;
|
||||||
|
page_no: number;
|
||||||
|
page_size: number;
|
||||||
|
items: RecommendedSolution[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文字介绍(关于我们)
|
||||||
|
export interface AboutUsInfo {
|
||||||
|
id: number;
|
||||||
|
title: string; // 标题,如 "壹梵缘起"
|
||||||
|
content: string; // 内容,用 \n\n 分隔段落
|
||||||
|
}
|
||||||
|
|
||||||
|
// 视频介绍
|
||||||
|
export interface AboutVideoInfo {
|
||||||
|
id: number;
|
||||||
|
title: string; // 视频标题
|
||||||
|
description?: string; // 视频描述
|
||||||
|
video_url: string; // 视频地址
|
||||||
|
cover_url?: string; // 封面图地址
|
||||||
|
duration?: number; // 视频时长(秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户相关
|
||||||
|
export interface UserInfo {
|
||||||
|
id: string;
|
||||||
|
nickname: string;
|
||||||
|
avatar: string;
|
||||||
|
level: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 小程序登录
|
||||||
|
export interface WxLoginParams {
|
||||||
|
code: string; // 小程序wx.login()获取的code
|
||||||
|
encryptedData?: string; // 手机号加密数据
|
||||||
|
iv?: string; // 加密算法的初始向量
|
||||||
|
parent_id?: string; // 邀请人ID(扫码/分享进入时可选)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WxLoginResponse {
|
||||||
|
is_registered: boolean; // 是否已注册
|
||||||
|
access_token: string | null;
|
||||||
|
refresh_token: string | null;
|
||||||
|
token_type: string | null;
|
||||||
|
expires_in: number | null;
|
||||||
|
openid: string;
|
||||||
|
user_info: UserInfo | null;
|
||||||
|
phone?: string; // 解密后的手机号
|
||||||
|
}
|
||||||
|
|
||||||
|
// 小程序注册
|
||||||
|
export interface WxRegisterParams {
|
||||||
|
openid: string;
|
||||||
|
name: string;
|
||||||
|
avatar?: string;
|
||||||
|
mobile?: string;
|
||||||
|
parent_id?: string; // 邀请人ID
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WxRegisterResponse {
|
||||||
|
is_registered: boolean;
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
token_type: string;
|
||||||
|
expires_in: number;
|
||||||
|
openid: string;
|
||||||
|
user_info: UserInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 小程序绑定
|
||||||
|
export interface WxBindParams {
|
||||||
|
openid: string; // 登录返回的openid
|
||||||
|
username: string; // 已有账号用户名
|
||||||
|
password: string; // 已有账号密码
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WxBindResponse {
|
||||||
|
is_registered: boolean;
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
token_type: string;
|
||||||
|
expires_in: number;
|
||||||
|
openid: string;
|
||||||
|
user_info: UserInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手机号密码注册
|
||||||
|
export interface MobileRegisterParams {
|
||||||
|
mobile: string; // 手机号
|
||||||
|
password: string; // 密码
|
||||||
|
repassword: string; // 确认密码
|
||||||
|
verification_code: string; // 短信验证码
|
||||||
|
inviter_id?: string; // 邀请人id
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MobileRegisterResponse {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
token_type: string;
|
||||||
|
expires_in: number;
|
||||||
|
user_info: UserInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手机号密码登录
|
||||||
|
export interface MobileLoginParams {
|
||||||
|
mobile: string; // 手机号
|
||||||
|
password: string; // 密码
|
||||||
|
inviter_id?: string; // 邀请人id
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MobileLoginResponse {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
token_type: string;
|
||||||
|
expires_in: number;
|
||||||
|
user_info: UserInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前用户修改用户名与手机号(PUT /api/v1/system/user/current/username-mobile) */
|
||||||
|
export interface UpdateCurrentUserUsernameMobileParams {
|
||||||
|
username: string;
|
||||||
|
mobile: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 成功时 data 可能为更新后的用户信息片段或 null */
|
||||||
|
export type UpdateCurrentUserUsernameMobileResponse = Record<string, unknown> | null;
|
||||||
|
|
||||||
|
// 忘记密码
|
||||||
|
export interface ForgotPasswordParams {
|
||||||
|
mobile: string; // 手机号
|
||||||
|
password: string; // 新密码
|
||||||
|
repassword: string; // 确认密码
|
||||||
|
verification_code: string; // 短信验证码
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ForgotPasswordResponse {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
token_type: string;
|
||||||
|
expires_in: number;
|
||||||
|
user_info: UserInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
export interface WxLogoutResponse {
|
||||||
|
msg?: string;
|
||||||
|
}
|
||||||
|
// 起名相关
|
||||||
|
export interface NamingParams {
|
||||||
|
lastName: string;
|
||||||
|
firstName?: string;
|
||||||
|
gender: 'male' | 'female';
|
||||||
|
birthDate: string;
|
||||||
|
birthTime?: string;
|
||||||
|
style?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeneratedNameItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
score: number;
|
||||||
|
meaning: string;
|
||||||
|
wuxing: string;
|
||||||
|
zodiac: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NamingDetailResult {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
totalScore: number;
|
||||||
|
meaningAnalysis: string;
|
||||||
|
zodiacAnalysis: string;
|
||||||
|
familyAnalysis: string;
|
||||||
|
strokeAnalysis: string;
|
||||||
|
sancaiAnalysis: string;
|
||||||
|
personalityAnalysis: string;
|
||||||
|
yijingAnalysis: string;
|
||||||
|
luckyTips: {
|
||||||
|
luckyColor: string;
|
||||||
|
luckyNumber: string;
|
||||||
|
suggestedIndustry: string;
|
||||||
|
constellation: string;
|
||||||
|
healthTips: string;
|
||||||
|
};
|
||||||
|
sixDimension: {
|
||||||
|
career: number;
|
||||||
|
wealth: number;
|
||||||
|
health: number;
|
||||||
|
love: number;
|
||||||
|
wisdom: number;
|
||||||
|
social: number;
|
||||||
|
};
|
||||||
|
lifeSimulation: Array<{ age: string; event: string }>;
|
||||||
|
emotionalFortune: string;
|
||||||
|
dailyTips: string[];
|
||||||
|
avatarStyles: string[];
|
||||||
|
poetrySource: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测名相关
|
||||||
|
export interface TestNameParams {
|
||||||
|
lastName: string;
|
||||||
|
firstName: string;
|
||||||
|
gender: 'male' | 'female';
|
||||||
|
birthDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestNameResult {
|
||||||
|
score: number;
|
||||||
|
analysis: string;
|
||||||
|
wuxing: string;
|
||||||
|
sancai: string;
|
||||||
|
suggestions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 测名详解弹窗节点,与 TestNameDetail.vue 中 modal 渲染一致 */
|
||||||
|
export type TestNameDetailNode =
|
||||||
|
| { type: 'text'; text: string }
|
||||||
|
| { type: 'list'; items: string[] }
|
||||||
|
| { type: 'kv'; items: Array<{ label: string; value: string }> };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测名「详解」扩展模块(与 header、meaning_and_zodiac 等并列的同级字段)。
|
||||||
|
* 个人:可放在 personalScoring 或方案详情 JSON 根级。
|
||||||
|
* 商号:可放在 companyScoring / 商号详解同一 JSON 根级(CompanyNamingDetail 与个人测名字段名一致)。
|
||||||
|
*/
|
||||||
|
export interface TestNameDetailPhonetics {
|
||||||
|
/** 完整拼音,如 "Zhāng Sān" */
|
||||||
|
pinyin_full?: string;
|
||||||
|
/** 声调与平仄简述,如 "阴平-阳平-上声" / "仄平平" */
|
||||||
|
pingze?: string;
|
||||||
|
/** 音律流畅度、朗朗上口等一句话 */
|
||||||
|
rhythm_summary?: string;
|
||||||
|
/** 简评 */
|
||||||
|
judgement?: string;
|
||||||
|
details?: { nodes: TestNameDetailNode[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestNameDetailBaziNameFit {
|
||||||
|
/** 八字喜用神简述 */
|
||||||
|
xiyongshen?: string;
|
||||||
|
/** 姓名五行组合简述 */
|
||||||
|
name_wuxing_profile?: string;
|
||||||
|
/** 是否补益、注意点等 */
|
||||||
|
complement_summary?: string;
|
||||||
|
/** 与八字契合度 0–100,可选 */
|
||||||
|
fit_score?: number;
|
||||||
|
details?: { nodes: TestNameDetailNode[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestNameDetailNamePopularity {
|
||||||
|
/** 重名率或重名风险的人文描述 */
|
||||||
|
duplicate_rate_label?: string;
|
||||||
|
/** 如:偏冷门 / 较为常见 / 高频重名 */
|
||||||
|
popularity_tier?: string;
|
||||||
|
interpretation?: string;
|
||||||
|
details?: { nodes: TestNameDetailNode[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 六爻(与本卦 gua 并列,可写动爻、纳甲等延展) */
|
||||||
|
export interface TestNameDetailLiuyao {
|
||||||
|
/** 卦名,如「火水未济」 */
|
||||||
|
hexagram_title?: string;
|
||||||
|
/** 动爻、变卦等一句话 */
|
||||||
|
changing_summary?: string;
|
||||||
|
/** 与姓名关联的断语摘要 */
|
||||||
|
interpretation?: string;
|
||||||
|
/** 六爻爻辞行,字符串数组,最多前端展示 6 条,详文放 details */
|
||||||
|
yao_lines?: string[];
|
||||||
|
details?: { nodes: TestNameDetailNode[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 五行八卦(姓名与五行、八卦关系) */
|
||||||
|
export interface TestNameDetailWuxingBagua {
|
||||||
|
/** 姓名五行态势简述 */
|
||||||
|
wuxing_sketch?: string;
|
||||||
|
/** 相关卦象、宫位等 */
|
||||||
|
bagua_profile?: string;
|
||||||
|
/** 生克互助简述 */
|
||||||
|
mutual_sketch?: string;
|
||||||
|
summary?: string;
|
||||||
|
details?: { nodes: TestNameDetailNode[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 属相(出生年生肖专项,可与 meaning_and_zodiac 并存) */
|
||||||
|
export interface TestNameDetailZodiacSign {
|
||||||
|
/** 生肖名,如「龙」 */
|
||||||
|
animal?: string;
|
||||||
|
/** 展示用图标或 emoji */
|
||||||
|
animal_icon?: string;
|
||||||
|
/** 地支,如「辰」 */
|
||||||
|
earthly_branch?: string;
|
||||||
|
trait_summary?: string;
|
||||||
|
/** 与所测名字的生合冲刑等 */
|
||||||
|
name_harmony?: string;
|
||||||
|
details?: { nodes: TestNameDetailNode[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 事业规划 */
|
||||||
|
export interface TestNameDetailCareerPlan {
|
||||||
|
summary?: string;
|
||||||
|
milestones?: Array<{
|
||||||
|
phase?: string;
|
||||||
|
period?: string;
|
||||||
|
focus?: string;
|
||||||
|
advice?: string;
|
||||||
|
}>;
|
||||||
|
details?: { nodes: TestNameDetailNode[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 幸运数字 */
|
||||||
|
export interface TestNameDetailLuckyNumbers {
|
||||||
|
/** 主推文案,如「3、8、9」 */
|
||||||
|
primary?: string;
|
||||||
|
/** 备选数字,与 primary 二选一或并存 */
|
||||||
|
numbers?: Array<number | string>;
|
||||||
|
meaning?: string;
|
||||||
|
details?: { nodes: TestNameDetailNode[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 幸运颜色 */
|
||||||
|
export interface TestNameDetailLuckyColors {
|
||||||
|
/** 主推色文案 */
|
||||||
|
primary?: string;
|
||||||
|
colors?: Array<{ name?: string; hex?: string; note?: string }>;
|
||||||
|
meaning?: string;
|
||||||
|
details?: { nodes: TestNameDetailNode[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公司起名/测名
|
||||||
|
export interface CompanyNamingParams {
|
||||||
|
industry: string;
|
||||||
|
address: string;
|
||||||
|
targetAudience: string;
|
||||||
|
members: Array<{ name: string; birthDate: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 择吉相关
|
||||||
|
export interface AuspiciousParams {
|
||||||
|
birthDate: string;
|
||||||
|
birthTime: string;
|
||||||
|
eventType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuspiciousResult {
|
||||||
|
dates: Array<{
|
||||||
|
date: string;
|
||||||
|
lunar: string;
|
||||||
|
score: number;
|
||||||
|
suitable: string[];
|
||||||
|
avoid: string[];
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收藏相关
|
||||||
|
export interface FavoriteItem {
|
||||||
|
id: string;
|
||||||
|
action: 'naming' | 'renaming';
|
||||||
|
mode: 'personal' | 'company';
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
date: string;
|
||||||
|
status: 'completed' | 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 报告相关
|
||||||
|
export interface ReportItem {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
type: 'fortune' | 'naming' | 'renaming';
|
||||||
|
date: string;
|
||||||
|
price: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付相关
|
||||||
|
export interface PaymentOrder {
|
||||||
|
orderId: string;
|
||||||
|
payParams: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderStatus {
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 产品相关
|
||||||
|
export interface Product {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
price: number;
|
||||||
|
originalPrice?: number;
|
||||||
|
image?: string;
|
||||||
|
images?: string[];
|
||||||
|
category?: string;
|
||||||
|
categoryId?: string;
|
||||||
|
tags?: string[];
|
||||||
|
status: 'active' | 'inactive' | 'sold_out';
|
||||||
|
stock?: number;
|
||||||
|
salesCount?: number;
|
||||||
|
rating?: number;
|
||||||
|
reviewCount?: number;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
attributes?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductCategory {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
parentId?: string;
|
||||||
|
icon?: string;
|
||||||
|
sortOrder?: number;
|
||||||
|
children?: ProductCategory[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductListParams {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
categoryId?: string;
|
||||||
|
keyword?: string;
|
||||||
|
minPrice?: number;
|
||||||
|
maxPrice?: number;
|
||||||
|
sortBy?: 'price' | 'sales' | 'rating' | 'created_at';
|
||||||
|
sortOrder?: 'asc' | 'desc';
|
||||||
|
status?: 'active' | 'inactive' | 'sold_out';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductListResponse {
|
||||||
|
items: Product[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductDetail extends Product {
|
||||||
|
content?: string;
|
||||||
|
specifications?: Array<{ name: string; value: string }>;
|
||||||
|
relatedProducts?: Product[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductCreateParams {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
price: number;
|
||||||
|
originalPrice?: number;
|
||||||
|
image?: string;
|
||||||
|
images?: string[];
|
||||||
|
categoryId?: string;
|
||||||
|
tags?: string[];
|
||||||
|
stock?: number;
|
||||||
|
status?: 'active' | 'inactive';
|
||||||
|
attributes?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductUpdateParams extends Partial<ProductCreateParams> {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 合伙人申请
|
||||||
|
export interface PartnerApplyParams {
|
||||||
|
real_name: string; // 真实姓名
|
||||||
|
phone: string; // 手机号码
|
||||||
|
wechat_id?: string; // 微信号(选填)
|
||||||
|
member_level?: 'junior' | 'senior'; // 会员等级(初级/高级)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PartnerApplyResponse {
|
||||||
|
id?: number;
|
||||||
|
msg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 会员折扣配置
|
||||||
|
export interface MembershipDiscountConfigResponse {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务价格配置(起名/改名/每日运程/每月运程)
|
||||||
|
export interface MembershipServicePriceConfigResponse {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 我的会员等级与额度
|
||||||
|
export interface MyMembershipQuotaResponse {
|
||||||
|
// 兼容后端可能返回的多种字段命名
|
||||||
|
level?: string;
|
||||||
|
membership_level?: string;
|
||||||
|
level_name?: string;
|
||||||
|
tier_name?: string;
|
||||||
|
quota?: number;
|
||||||
|
total_quota?: number;
|
||||||
|
used_quota?: number;
|
||||||
|
remain_quota?: number;
|
||||||
|
remaining_quota?: number;
|
||||||
|
left_quota?: number;
|
||||||
|
/** 剩余免费改名等额度(后端约定字段) */
|
||||||
|
free_rename_quota?: number;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 我的方案
|
||||||
|
export interface MyReportsParams {
|
||||||
|
page_no?: number;
|
||||||
|
page_size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MyReportItem {
|
||||||
|
id: number;
|
||||||
|
title: string; // 标题
|
||||||
|
subtitle: string; // 副标题
|
||||||
|
category: string; // 类型:personal/company
|
||||||
|
service_type: string; // 服务类型:naming/renaming/test
|
||||||
|
has_solutions: boolean; // 是否有方案
|
||||||
|
created_time: string; // 创建时间
|
||||||
|
relative_time: string; // 相对时间
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MyReportsResponse {
|
||||||
|
total: number;
|
||||||
|
page_no: number;
|
||||||
|
page_size: number;
|
||||||
|
items: MyReportItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 方案详情
|
||||||
|
export interface SolutionFullDetail {
|
||||||
|
id: number;
|
||||||
|
name: string; // 名字
|
||||||
|
pinyin?: string; // 拼音
|
||||||
|
gender?: string; // 性别
|
||||||
|
birth_date?: string; // 出生日期
|
||||||
|
birth_time?: string; // 出生时间
|
||||||
|
lunar_date?: string; // 农历日期
|
||||||
|
total_score?: number; // 总分
|
||||||
|
star_rating?: number; // 星级
|
||||||
|
name_meaning?: string; // 名字寓意
|
||||||
|
poetry_source?: string; // 诗词出处
|
||||||
|
wuxing_analysis?: string; // 五行分析
|
||||||
|
zodiac_analysis?: string; // 生肖分析
|
||||||
|
sancai_analysis?: string; // 三才分析
|
||||||
|
stroke_analysis?: string; // 笔画分析
|
||||||
|
six_dimension?: { // 六维评分
|
||||||
|
career: number;
|
||||||
|
wealth: number;
|
||||||
|
health: number;
|
||||||
|
love: number;
|
||||||
|
wisdom: number;
|
||||||
|
social: number;
|
||||||
|
};
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 我的收藏
|
||||||
|
export interface MyFavoritesParams {
|
||||||
|
page_no?: number;
|
||||||
|
page_size?: number;
|
||||||
|
category?: 'personal' | 'company'; // 可选,personal=个人名,company=商号,不传查全部
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MyFavoriteItem {
|
||||||
|
id: number;
|
||||||
|
name: string; // 名字
|
||||||
|
pinyin?: string; // 拼音
|
||||||
|
category: string; // 类型:personal/company
|
||||||
|
created_time: string; // 收藏时间
|
||||||
|
solution_id?: number; // 方案ID
|
||||||
|
tags?: string[]; // 标签
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MyFavoritesResponse {
|
||||||
|
total: number;
|
||||||
|
page_no: number;
|
||||||
|
page_size: number;
|
||||||
|
items: MyFavoriteItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加/取消收藏
|
||||||
|
export interface ToggleFavoriteParams {
|
||||||
|
solution_id: number; // 方案ID
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToggleFavoriteResponse {
|
||||||
|
is_favorited: boolean; // 收藏状态
|
||||||
|
msg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收藏方案
|
||||||
|
export interface FavoriteSolutionParams {
|
||||||
|
solution_id: number;
|
||||||
|
category: 'personal' | 'company';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FavoriteSolutionResponse {
|
||||||
|
is_favorited?: boolean;
|
||||||
|
msg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消收藏
|
||||||
|
export interface UnfavoriteSolutionParams {
|
||||||
|
solution_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnfavoriteSolutionResponse {
|
||||||
|
is_favorited?: boolean;
|
||||||
|
msg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 常见问题
|
||||||
|
export interface FAQItem {
|
||||||
|
id: number;
|
||||||
|
question: string; // 问题
|
||||||
|
answer: string; // 答案
|
||||||
|
is_hot: number; // 是否热门 (1=是, 0=否)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FAQGroup {
|
||||||
|
category: string; // 分类标识
|
||||||
|
category_name: string; // 分类名称
|
||||||
|
items: FAQItem[]; // 问题列表
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FAQResponse {
|
||||||
|
data: FAQGroup[]; // 分组列表(注意:返回的是data数组,不是groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐私政策
|
||||||
|
export interface PrivacyPolicyResponse {
|
||||||
|
id: number;
|
||||||
|
title: string; // 标题
|
||||||
|
content: string; // 内容(HTML或Markdown格式)
|
||||||
|
version: string; // 版本号
|
||||||
|
effective_date: string; // 生效日期
|
||||||
|
updated_at: string; // 更新时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// 意见反馈
|
||||||
|
export interface FeedbackParams {
|
||||||
|
content: string; // 反馈内容(必填)
|
||||||
|
images?: string; // 图片URL,多个用逗号分隔(可选)
|
||||||
|
contact?: string; // 联系方式(可选)
|
||||||
|
feedback_type?: 'suggestion' | 'bug' | 'complaint' | 'other'; // 反馈类型(默认other)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FeedbackResponse {
|
||||||
|
msg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成小程序码
|
||||||
|
export interface GenerateQRCodeParams {
|
||||||
|
scene: string; // 场景值,最大32个字符,格式如 "uid=123"
|
||||||
|
page?: string; // 跳转页面路径,默认为首页
|
||||||
|
width?: number; // 二维码宽度,默认430px
|
||||||
|
auto_color?: boolean; // 自动配置线条颜色
|
||||||
|
line_color?: { // 线条颜色
|
||||||
|
r: number;
|
||||||
|
g: number;
|
||||||
|
b: number;
|
||||||
|
};
|
||||||
|
is_hyaline?: boolean; // 是否需要透明底色
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GenerateQRCodeResponse {
|
||||||
|
qrcode_url: string; // 生成的小程序码图片URL
|
||||||
|
}
|
||||||
|
|
||||||
|
// 微信支付
|
||||||
|
export interface CreateOrderParams {
|
||||||
|
description: string; // 商品描述
|
||||||
|
total_amount: number; // 支付金额(分)
|
||||||
|
business_type: 'company_naming' | 'company_renaming' | 'company_name_analysis' | 'personal_naming' | 'personal_renaming' | 'personal_name_analysis' | 'wealth_analysis' | 'affinity_unlock' | 'partner_apply'; // 业务类型
|
||||||
|
business_id: number; // 业务ID - 用于关联具体的业务记录
|
||||||
|
pay_type: string; // 支付方式,固定为jsapi
|
||||||
|
code?: string; // 用户code,若用户未绑定微信需要传递code信息
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateOrderResponse {
|
||||||
|
out_trade_no: string; // 商户订单号
|
||||||
|
prepay_id: string; // 预支付交易会话标识
|
||||||
|
pay_type: string; // 支付方式
|
||||||
|
h5_url: string | null; // H5支付链接
|
||||||
|
appId: string; // 公众号appId
|
||||||
|
timeStamp: string; // 时间戳
|
||||||
|
nonceStr: string; // 随机字符串
|
||||||
|
package: string; // prepay_id=xxx
|
||||||
|
signType: string; // 签名类型
|
||||||
|
paySign: string; // 签名
|
||||||
|
payment_params?: { // 兼容旧结构
|
||||||
|
appId?: string;
|
||||||
|
timeStamp: string;
|
||||||
|
nonceStr: string;
|
||||||
|
package: string;
|
||||||
|
signType: string;
|
||||||
|
paySign: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryOrderParams {
|
||||||
|
out_trade_no: string; // 商户订单号
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryOrderResponse {
|
||||||
|
out_trade_no: string; // 商户订单号
|
||||||
|
transaction_id?: string; // 微信支付订单号
|
||||||
|
status: 'pending' | 'paid' | 'cancelled' | 'refunded'; // 订单状态
|
||||||
|
total_amount: number; // 支付金额
|
||||||
|
paid_amount?: number; // 实付金额
|
||||||
|
paid_at?: string; // 支付时间
|
||||||
|
business_type: string; // 业务类型
|
||||||
|
business_id: number; // 业务ID
|
||||||
|
description?: string; // 商品描述
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付订单列表
|
||||||
|
export interface ListPayOrdersParams {
|
||||||
|
page_no?: number | string;
|
||||||
|
page_size?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListPayOrdersItem {
|
||||||
|
id: number;
|
||||||
|
out_trade_no: string;
|
||||||
|
transaction_id?: string;
|
||||||
|
description?: string;
|
||||||
|
total_amount: number;
|
||||||
|
business_type: string;
|
||||||
|
trade_state: 'NOTPAY' | 'SUCCESS' | 'REFUND' | 'CLOSED' | string;
|
||||||
|
success_time?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListPayOrdersResponse {
|
||||||
|
total: number;
|
||||||
|
items: ListPayOrdersItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭订单
|
||||||
|
export interface CloseOrderResponse {
|
||||||
|
msg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传图片
|
||||||
|
export interface UploadImageResponse {
|
||||||
|
file_path: string; // 文件路径
|
||||||
|
file_name: string; // 文件名
|
||||||
|
origin_name: string; // 原始文件名
|
||||||
|
file_url: string; // 图片URL(用于回显和上传)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消息轮播
|
||||||
|
export interface NoticeItem {
|
||||||
|
id: number;
|
||||||
|
uuid?: string;
|
||||||
|
notice_title: string; // 消息标题
|
||||||
|
notice_type: string; // 消息类型
|
||||||
|
notice_content: string; // 消息内容
|
||||||
|
status?: string;
|
||||||
|
description?: string;
|
||||||
|
created_time?: string;
|
||||||
|
updated_time?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoticeListParams {
|
||||||
|
page_no?: number;
|
||||||
|
page_size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoticeListResponse {
|
||||||
|
total: number;
|
||||||
|
page_no: number;
|
||||||
|
page_size: number;
|
||||||
|
has_next: boolean;
|
||||||
|
items: NoticeItem[];
|
||||||
|
}
|
||||||
167
前端源码/uni-app/api/user.ts
Normal file
167
前端源码/uni-app/api/user.ts
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
/**
|
||||||
|
* 用户相关接口
|
||||||
|
*/
|
||||||
|
import http, { BASE_URL, type RequestConfig } from '@/utils/request';
|
||||||
|
import type { WxLoginParams, WxLoginResponse, WxRegisterParams, WxRegisterResponse, WxBindParams, WxBindResponse, MobileRegisterParams, MobileRegisterResponse, MobileLoginParams, MobileLoginResponse, ForgotPasswordParams, ForgotPasswordResponse, WxLogoutResponse, PartnerApplyParams, PartnerApplyResponse, MembershipDiscountConfigResponse, MembershipServicePriceConfigResponse, MyMembershipQuotaResponse, MyReportsParams, MyReportsResponse, SolutionFullDetail, MyFavoritesParams, MyFavoritesResponse, ToggleFavoriteParams, ToggleFavoriteResponse, FavoriteSolutionParams, FavoriteSolutionResponse, UnfavoriteSolutionParams, UnfavoriteSolutionResponse, FAQResponse, PrivacyPolicyResponse, FeedbackParams, FeedbackResponse, GenerateQRCodeResponse, UploadImageResponse, UpdateCurrentUserUsernameMobileParams, UpdateCurrentUserUsernameMobileResponse } from './types';
|
||||||
|
|
||||||
|
/** 与 yifan 同域下的 system 模块接口根路径 */
|
||||||
|
const systemApiBaseUrl = () => String(BASE_URL || '').replace(/\/yifan\/?$/, '/system');
|
||||||
|
|
||||||
|
export const userApi = {
|
||||||
|
// 发送手机短信验证码
|
||||||
|
sendSmsCode: (mobile: string) =>
|
||||||
|
http.post<{ code: number; msg: string; data: any }>('/yifan_wx_auth/send-sms', { mobile }, { showLoading: true }),
|
||||||
|
|
||||||
|
// 手机号密码注册
|
||||||
|
mobileRegister: (params: MobileRegisterParams) =>
|
||||||
|
http.post<MobileRegisterResponse>('/yifan_wx_auth/mobile-register', params, { showLoading: true }),
|
||||||
|
|
||||||
|
// 手机号密码登录
|
||||||
|
mobileLogin: (params: MobileLoginParams) =>
|
||||||
|
http.post<MobileLoginResponse>('/yifan_wx_auth/mobile-login', params, { showLoading: true }),
|
||||||
|
|
||||||
|
// 忘记密码
|
||||||
|
forgotPassword: (params: ForgotPasswordParams) =>
|
||||||
|
http.post<ForgotPasswordResponse>('/yifan_wx_auth/forgot-password', params, { showLoading: true }),
|
||||||
|
|
||||||
|
// 小程序登录
|
||||||
|
wxLogin: (params: WxLoginParams) =>
|
||||||
|
http.post<WxLoginResponse>('/yifan_wx_auth/login', params, { showLoading: true, timeout: 9999999 }),
|
||||||
|
|
||||||
|
// 小程序注册(超时时间设置为30秒)
|
||||||
|
wxRegister: (params: WxRegisterParams) =>
|
||||||
|
http.post<WxRegisterResponse>('/yifan_wx_auth/register', params, { showLoading: true, timeout: 30000 }),
|
||||||
|
|
||||||
|
// 小程序绑定
|
||||||
|
wxBind: (params: WxBindParams) =>
|
||||||
|
http.post<WxBindResponse>('/yifan_wx_auth/bind', params, { showLoading: true }),
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
wxLogout: () =>
|
||||||
|
http.post<WxLogoutResponse>('/yifan_wx_auth/logout', {}, { showLoading: true }),
|
||||||
|
|
||||||
|
// 申请成为合伙人
|
||||||
|
applyPartner: (params: PartnerApplyParams) =>
|
||||||
|
http.post<PartnerApplyResponse>('/yifan_partner_apply/apply', params, { showLoading: true }),
|
||||||
|
|
||||||
|
// 我的会员等级与额度
|
||||||
|
getMyMembershipQuota: (options?: Partial<RequestConfig>) =>
|
||||||
|
http.get<MyMembershipQuotaResponse>('/yifan_membership/my-quota', undefined, options),
|
||||||
|
|
||||||
|
// 获取会员折扣配置(初级会员/高级会员)
|
||||||
|
getMembershipDiscountConfig: (options?: Partial<RequestConfig>) =>
|
||||||
|
http.get<MembershipDiscountConfigResponse>('/yifan_membership/discount-config', undefined, options),
|
||||||
|
|
||||||
|
// 获取服务价格配置(起名/改名/每日运程/每月运程)
|
||||||
|
getMembershipServicePriceConfig: (options?: Partial<RequestConfig>) =>
|
||||||
|
http.get<MembershipServicePriceConfigResponse>('/yifan_membership/service-price-config', undefined, options),
|
||||||
|
|
||||||
|
// 修改当前用户用户名与手机号(成功后仅前端合并缓存,不因本接口误清 token/userInfo)
|
||||||
|
updateCurrentUserUsernameMobile: (params: UpdateCurrentUserUsernameMobileParams) =>
|
||||||
|
http.put<UpdateCurrentUserUsernameMobileResponse>(
|
||||||
|
`${systemApiBaseUrl()}/user/current/username-mobile`,
|
||||||
|
params,
|
||||||
|
{ showLoading: true, clearAuthOnError: false }
|
||||||
|
),
|
||||||
|
|
||||||
|
// 我的方案
|
||||||
|
getMyReports: (params?: MyReportsParams, options?: Partial<RequestConfig>) =>
|
||||||
|
http.get<MyReportsResponse>('/yifan_naming_reports/my_reports', params, options),
|
||||||
|
|
||||||
|
// 根据方案id获取详情
|
||||||
|
getSolutionDetail: (id: number) =>
|
||||||
|
http.get<SolutionFullDetail>(`/yifan_naming_solutions/full_detail/${id}`),
|
||||||
|
|
||||||
|
// 我的收藏
|
||||||
|
getMyFavorites: (params?: MyFavoritesParams, options?: Partial<RequestConfig>) =>
|
||||||
|
http.get<MyFavoritesResponse>('/yifan_naming_favorites/my_favorites', params, options),
|
||||||
|
|
||||||
|
// 添加/取消收藏
|
||||||
|
toggleFavorite: (params: ToggleFavoriteParams) =>
|
||||||
|
http.post<ToggleFavoriteResponse>('/yifan_naming_favorites/toggle', params, { showLoading: true }),
|
||||||
|
|
||||||
|
// 收藏方案
|
||||||
|
favoriteSolution: (params: FavoriteSolutionParams) =>
|
||||||
|
http.post<FavoriteSolutionResponse>('/yifan_naming_favorites/favorite', params, { showLoading: true }),
|
||||||
|
|
||||||
|
// 取消收藏
|
||||||
|
unfavoriteSolution: (params: UnfavoriteSolutionParams) =>
|
||||||
|
http.post<UnfavoriteSolutionResponse>('/yifan_naming_favorites/unfavorite', params, { showLoading: true }),
|
||||||
|
|
||||||
|
// 获取常见问题
|
||||||
|
getFAQ: () =>
|
||||||
|
http.get<FAQResponse>('/yifan_faq/mini/grouped'),
|
||||||
|
|
||||||
|
// 获取隐私政策
|
||||||
|
getPrivacyPolicy: () =>
|
||||||
|
http.get<PrivacyPolicyResponse>('/yifan_privacy_policy/mini/current'),
|
||||||
|
|
||||||
|
// 提交意见反馈
|
||||||
|
submitFeedback: (params: FeedbackParams) =>
|
||||||
|
http.post<FeedbackResponse>('/yifan_feedback/mini/submit', params, { showLoading: true }),
|
||||||
|
|
||||||
|
// 生成分享二维码(小程序码)
|
||||||
|
generateShareQRCode: () =>
|
||||||
|
http.post<GenerateQRCodeResponse>('/yifan_wx_auth/generate-qrcode', {}, { showLoading: true }),
|
||||||
|
|
||||||
|
// 上传图片 (H5 版本)
|
||||||
|
uploadImage: async (file: File): Promise<UploadImageResponse> => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('base_url', BASE_URL);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await http.post<UploadImageResponse>(
|
||||||
|
'/yifan_wx_auth/upload-image',
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
},
|
||||||
|
showLoading: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.msg || '上传失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 选择并上传图片(H5 版本)
|
||||||
|
chooseAndUploadImage: (options?: { count?: number; accept?: string }): Promise<UploadImageResponse[]> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = options?.accept || 'image/*';
|
||||||
|
input.multiple = (options?.count || 1) > 1;
|
||||||
|
|
||||||
|
input.onchange = async (e: Event) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
const files = target.files;
|
||||||
|
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
reject(new Error('未选择文件'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results: UploadImageResponse[] = [];
|
||||||
|
const maxCount = options?.count || 1;
|
||||||
|
const filesToUpload = Array.from(files).slice(0, maxCount);
|
||||||
|
|
||||||
|
for (const file of filesToUpload) {
|
||||||
|
const result = await userApi.uploadImage(file);
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
resolve(results);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default userApi;
|
||||||
658
前端源码/uni-app/auspicious-api-spec.json
Normal file
658
前端源码/uni-app/auspicious-api-spec.json
Normal file
@@ -0,0 +1,658 @@
|
|||||||
|
{
|
||||||
|
"精准八字择吉接口规范": {
|
||||||
|
"接口名称": "精准八字择吉",
|
||||||
|
"接口路径": "/api/auspicious/calculate",
|
||||||
|
"请求方法": "POST",
|
||||||
|
"入参 (Request)": {
|
||||||
|
"eventType": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "求测事项类型",
|
||||||
|
"enum": [
|
||||||
|
"kaichang",
|
||||||
|
"jiehun",
|
||||||
|
"qiaoqian",
|
||||||
|
"dongtu",
|
||||||
|
"qiuzi",
|
||||||
|
"other"
|
||||||
|
],
|
||||||
|
"enum_labels": {
|
||||||
|
"kaichang": "开业开市",
|
||||||
|
"jiehun": "嫁娶结婚",
|
||||||
|
"qiaoqian": "乔迁入宅",
|
||||||
|
"dongtu": "动土修造",
|
||||||
|
"qiuzi": "求嗣祈福",
|
||||||
|
"other": "手动填写"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"customEvent": {
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"description": "自定义事项(当eventType为other时必填)",
|
||||||
|
"example": "签约"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "福主姓名",
|
||||||
|
"example": "张三"
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "性别",
|
||||||
|
"enum": [
|
||||||
|
"male",
|
||||||
|
"female"
|
||||||
|
],
|
||||||
|
"enum_labels": {
|
||||||
|
"male": "男",
|
||||||
|
"female": "女"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"birthDateDisplay": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "出生日期时辰(显示格式)",
|
||||||
|
"example": "1990年1月1日 子时"
|
||||||
|
},
|
||||||
|
"birthDateApi": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "出生日期时辰(API格式)",
|
||||||
|
"format": "YYYY-MM-DD HH:mm:ss",
|
||||||
|
"example": "1990-01-01 00:30:00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"入参示例": {
|
||||||
|
"eventType": "jiehun",
|
||||||
|
"customEvent": "",
|
||||||
|
"name": "张三",
|
||||||
|
"gender": "male",
|
||||||
|
"birthDateDisplay": "1990年1月1日 子时",
|
||||||
|
"birthDateApi": "1990-01-01 00:30:00"
|
||||||
|
},
|
||||||
|
"出参 (Response)": {
|
||||||
|
"code": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "状态码,200表示成功"
|
||||||
|
},
|
||||||
|
"msg": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "返回消息"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "择吉结果数据",
|
||||||
|
"properties": {
|
||||||
|
"report_id": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "报告ID(用于支付解锁)",
|
||||||
|
"example": 12345
|
||||||
|
},
|
||||||
|
"business_id": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "业务ID(同report_id)",
|
||||||
|
"example": 12345
|
||||||
|
},
|
||||||
|
"business_type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "业务类型",
|
||||||
|
"example": "auspicious_report"
|
||||||
|
},
|
||||||
|
"eventType": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "事项类型",
|
||||||
|
"example": "jiehun"
|
||||||
|
},
|
||||||
|
"eventLabel": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "事项标签",
|
||||||
|
"example": "嫁娶结婚"
|
||||||
|
},
|
||||||
|
"customEvent": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "自定义事项",
|
||||||
|
"example": ""
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "福主姓名",
|
||||||
|
"example": "张三"
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "性别",
|
||||||
|
"example": "male"
|
||||||
|
},
|
||||||
|
"birthDate": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "出生日期时辰(显示格式)",
|
||||||
|
"example": "1990年1月1日 子时"
|
||||||
|
},
|
||||||
|
"birthDateApi": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "出生日期时辰(API格式)",
|
||||||
|
"example": "1990-01-01 00:30:00"
|
||||||
|
},
|
||||||
|
"advice": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "择吉分析建议",
|
||||||
|
"example": "红鸾星动,天喜临门。本月利于婚嫁,宜选双日,寓意成双成对。忌选父母生辰冲克之日。"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "吉日列表",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"date": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "公历日期",
|
||||||
|
"format": "YYYY-MM-DD",
|
||||||
|
"example": "2024-05-18"
|
||||||
|
},
|
||||||
|
"lunar": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "农历日期",
|
||||||
|
"example": "四月十一"
|
||||||
|
},
|
||||||
|
"desc": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "日期描述",
|
||||||
|
"example": "天德合日,官印相生"
|
||||||
|
},
|
||||||
|
"score": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "吉运指数",
|
||||||
|
"example": 98,
|
||||||
|
"range": "0-100"
|
||||||
|
},
|
||||||
|
"hours": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "吉时(逗号分隔)",
|
||||||
|
"example": "巳时(09-11), 未时(13-15)"
|
||||||
|
},
|
||||||
|
"clash": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "冲煞",
|
||||||
|
"example": "冲猪"
|
||||||
|
},
|
||||||
|
"suitable": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "宜做事项",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"嫁娶",
|
||||||
|
"开市",
|
||||||
|
"出行",
|
||||||
|
"祈福"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"avoid": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "忌做事项",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"动土",
|
||||||
|
"破土",
|
||||||
|
"安葬"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ganZhi": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "干支信息",
|
||||||
|
"properties": {
|
||||||
|
"year": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "年干支",
|
||||||
|
"example": "甲辰"
|
||||||
|
},
|
||||||
|
"month": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "月干支",
|
||||||
|
"example": "己巳"
|
||||||
|
},
|
||||||
|
"day": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "日干支",
|
||||||
|
"example": "庚午"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wuxing": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "五行信息",
|
||||||
|
"properties": {
|
||||||
|
"day": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "日五行",
|
||||||
|
"example": "金"
|
||||||
|
},
|
||||||
|
"nayin": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "纳音",
|
||||||
|
"example": "路旁土"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stars": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "吉星",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"天德",
|
||||||
|
"月德",
|
||||||
|
"三合"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gods": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "神煞",
|
||||||
|
"properties": {
|
||||||
|
"good": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "吉神",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"天德合",
|
||||||
|
"月德合",
|
||||||
|
"天喜"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bad": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "凶神",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"五鬼",
|
||||||
|
"血支"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unlockInfo": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "解锁信息",
|
||||||
|
"properties": {
|
||||||
|
"freeCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "免费查看数量",
|
||||||
|
"example": 2
|
||||||
|
},
|
||||||
|
"basicCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "基础版解锁数量(近期吉日)",
|
||||||
|
"example": 5
|
||||||
|
},
|
||||||
|
"premiumCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "尊享版解锁数量(全年吉日)",
|
||||||
|
"example": 30
|
||||||
|
},
|
||||||
|
"basicPrice": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "基础版价格(元)",
|
||||||
|
"example": 9.9
|
||||||
|
},
|
||||||
|
"premiumPrice": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "尊享版价格(元)",
|
||||||
|
"example": 38
|
||||||
|
},
|
||||||
|
"premiumOriginalPrice": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "尊享版原价(元)",
|
||||||
|
"example": 99.9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"statistics": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "统计信息",
|
||||||
|
"properties": {
|
||||||
|
"totalUsers": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "已测算人数",
|
||||||
|
"example": 28392
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"出参示例": {
|
||||||
|
"code": 200,
|
||||||
|
"msg": "测算成功",
|
||||||
|
"data": {
|
||||||
|
"report_id": 12345,
|
||||||
|
"business_id": 12345,
|
||||||
|
"business_type": "auspicious_report",
|
||||||
|
"eventType": "jiehun",
|
||||||
|
"eventLabel": "嫁娶结婚",
|
||||||
|
"customEvent": "",
|
||||||
|
"name": "张三",
|
||||||
|
"gender": "male",
|
||||||
|
"birthDate": "1990年1月1日 子时",
|
||||||
|
"birthDateApi": "1990-01-01 00:30:00",
|
||||||
|
"advice": "红鸾星动,天喜临门。本月利于婚嫁,宜选双日,寓意成双成对。忌选父母生辰冲克之日。",
|
||||||
|
"dates": [
|
||||||
|
{
|
||||||
|
"date": "2024-05-18",
|
||||||
|
"lunar": "四月十一",
|
||||||
|
"desc": "天德合日,官印相生",
|
||||||
|
"score": 98,
|
||||||
|
"hours": "巳时(09-11), 未时(13-15)",
|
||||||
|
"clash": "冲猪",
|
||||||
|
"suitable": [
|
||||||
|
"嫁娶",
|
||||||
|
"开市",
|
||||||
|
"出行",
|
||||||
|
"祈福",
|
||||||
|
"纳采",
|
||||||
|
"订盟"
|
||||||
|
],
|
||||||
|
"avoid": [
|
||||||
|
"动土",
|
||||||
|
"破土",
|
||||||
|
"安葬",
|
||||||
|
"修坟"
|
||||||
|
],
|
||||||
|
"ganZhi": {
|
||||||
|
"year": "甲辰",
|
||||||
|
"month": "己巳",
|
||||||
|
"day": "庚午"
|
||||||
|
},
|
||||||
|
"wuxing": {
|
||||||
|
"day": "金",
|
||||||
|
"nayin": "路旁土"
|
||||||
|
},
|
||||||
|
"stars": [
|
||||||
|
"天德",
|
||||||
|
"月德",
|
||||||
|
"三合",
|
||||||
|
"天喜"
|
||||||
|
],
|
||||||
|
"gods": {
|
||||||
|
"good": [
|
||||||
|
"天德合",
|
||||||
|
"月德合",
|
||||||
|
"天喜",
|
||||||
|
"天医",
|
||||||
|
"福星"
|
||||||
|
],
|
||||||
|
"bad": [
|
||||||
|
"五鬼",
|
||||||
|
"血支"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2024-05-22",
|
||||||
|
"lunar": "四月十五",
|
||||||
|
"desc": "月德合日,三合临旺",
|
||||||
|
"score": 95,
|
||||||
|
"hours": "辰时(07-09), 午时(11-13)",
|
||||||
|
"clash": "冲兔",
|
||||||
|
"suitable": [
|
||||||
|
"嫁娶",
|
||||||
|
"祈福",
|
||||||
|
"纳采",
|
||||||
|
"开市",
|
||||||
|
"立券"
|
||||||
|
],
|
||||||
|
"avoid": [
|
||||||
|
"动土",
|
||||||
|
"破土",
|
||||||
|
"安葬"
|
||||||
|
],
|
||||||
|
"ganZhi": {
|
||||||
|
"year": "甲辰",
|
||||||
|
"month": "己巳",
|
||||||
|
"day": "甲戌"
|
||||||
|
},
|
||||||
|
"wuxing": {
|
||||||
|
"day": "木",
|
||||||
|
"nayin": "山头火"
|
||||||
|
},
|
||||||
|
"stars": [
|
||||||
|
"月德",
|
||||||
|
"三合",
|
||||||
|
"天喜"
|
||||||
|
],
|
||||||
|
"gods": {
|
||||||
|
"good": [
|
||||||
|
"月德合",
|
||||||
|
"三合",
|
||||||
|
"天喜",
|
||||||
|
"天贵"
|
||||||
|
],
|
||||||
|
"bad": [
|
||||||
|
"天刑",
|
||||||
|
"朱雀"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2024-06-05",
|
||||||
|
"lunar": "四月廿九",
|
||||||
|
"desc": "天赦日,百无禁忌",
|
||||||
|
"score": 92,
|
||||||
|
"hours": "卯时(05-07), 酉时(17-19)",
|
||||||
|
"clash": "冲蛇",
|
||||||
|
"suitable": [
|
||||||
|
"嫁娶",
|
||||||
|
"祈福",
|
||||||
|
"开市",
|
||||||
|
"出行",
|
||||||
|
"纳采",
|
||||||
|
"订盟",
|
||||||
|
"安床"
|
||||||
|
],
|
||||||
|
"avoid": [
|
||||||
|
"安葬",
|
||||||
|
"修坟"
|
||||||
|
],
|
||||||
|
"ganZhi": {
|
||||||
|
"year": "甲辰",
|
||||||
|
"month": "庚午",
|
||||||
|
"day": "戊子"
|
||||||
|
},
|
||||||
|
"wuxing": {
|
||||||
|
"day": "土",
|
||||||
|
"nayin": "霹雳火"
|
||||||
|
},
|
||||||
|
"stars": [
|
||||||
|
"天赦",
|
||||||
|
"天德",
|
||||||
|
"福星"
|
||||||
|
],
|
||||||
|
"gods": {
|
||||||
|
"good": [
|
||||||
|
"天赦",
|
||||||
|
"天德",
|
||||||
|
"福星",
|
||||||
|
"天贵",
|
||||||
|
"天医"
|
||||||
|
],
|
||||||
|
"bad": [
|
||||||
|
"劫煞"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2024-06-18",
|
||||||
|
"lunar": "五月十三",
|
||||||
|
"desc": "黄道吉日,诸事皆宜",
|
||||||
|
"score": 88,
|
||||||
|
"hours": "巳时(09-11), 申时(15-17)",
|
||||||
|
"clash": "冲鼠",
|
||||||
|
"suitable": [
|
||||||
|
"嫁娶",
|
||||||
|
"开市",
|
||||||
|
"出行",
|
||||||
|
"祈福",
|
||||||
|
"纳采"
|
||||||
|
],
|
||||||
|
"avoid": [
|
||||||
|
"动土",
|
||||||
|
"破土"
|
||||||
|
],
|
||||||
|
"ganZhi": {
|
||||||
|
"year": "甲辰",
|
||||||
|
"month": "庚午",
|
||||||
|
"day": "辛丑"
|
||||||
|
},
|
||||||
|
"wuxing": {
|
||||||
|
"day": "金",
|
||||||
|
"nayin": "壁上土"
|
||||||
|
},
|
||||||
|
"stars": [
|
||||||
|
"黄道",
|
||||||
|
"天德"
|
||||||
|
],
|
||||||
|
"gods": {
|
||||||
|
"good": [
|
||||||
|
"黄道",
|
||||||
|
"天德",
|
||||||
|
"天喜"
|
||||||
|
],
|
||||||
|
"bad": [
|
||||||
|
"白虎"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2024-07-02",
|
||||||
|
"lunar": "五月廿七",
|
||||||
|
"desc": "六合吉日,贵人得位",
|
||||||
|
"score": 90,
|
||||||
|
"hours": "辰时(07-09), 未时(13-15)",
|
||||||
|
"clash": "冲虎",
|
||||||
|
"suitable": [
|
||||||
|
"嫁娶",
|
||||||
|
"祈福",
|
||||||
|
"开市",
|
||||||
|
"纳采",
|
||||||
|
"订盟"
|
||||||
|
],
|
||||||
|
"avoid": [
|
||||||
|
"动土",
|
||||||
|
"安葬"
|
||||||
|
],
|
||||||
|
"ganZhi": {
|
||||||
|
"year": "甲辰",
|
||||||
|
"month": "庚午",
|
||||||
|
"day": "乙卯"
|
||||||
|
},
|
||||||
|
"wuxing": {
|
||||||
|
"day": "木",
|
||||||
|
"nayin": "大溪水"
|
||||||
|
},
|
||||||
|
"stars": [
|
||||||
|
"六合",
|
||||||
|
"天贵"
|
||||||
|
],
|
||||||
|
"gods": {
|
||||||
|
"good": [
|
||||||
|
"六合",
|
||||||
|
"天贵",
|
||||||
|
"天喜",
|
||||||
|
"福星"
|
||||||
|
],
|
||||||
|
"bad": [
|
||||||
|
"天刑"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unlockInfo": {
|
||||||
|
"freeCount": 2,
|
||||||
|
"basicCount": 5,
|
||||||
|
"premiumCount": 30,
|
||||||
|
"basicPrice": 9.9,
|
||||||
|
"premiumPrice": 38,
|
||||||
|
"premiumOriginalPrice": 99.9
|
||||||
|
},
|
||||||
|
"statistics": {
|
||||||
|
"totalUsers": 28392
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"解锁等级说明": {
|
||||||
|
"none": {
|
||||||
|
"description": "未解锁",
|
||||||
|
"viewCount": 2,
|
||||||
|
"features": [
|
||||||
|
"查看前2个吉日的基本信息",
|
||||||
|
"吉时和冲煞信息被锁定"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"basic": {
|
||||||
|
"description": "基础版(近期吉日)",
|
||||||
|
"price": 9.9,
|
||||||
|
"viewCount": 5,
|
||||||
|
"features": [
|
||||||
|
"查看未来30天精选吉日",
|
||||||
|
"完整吉时信息",
|
||||||
|
"冲煞详情",
|
||||||
|
"宜忌事项"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"premium": {
|
||||||
|
"description": "尊享版(全年吉日)",
|
||||||
|
"price": 38,
|
||||||
|
"originalPrice": 99.9,
|
||||||
|
"viewCount": 30,
|
||||||
|
"features": [
|
||||||
|
"查看1年内所有吉日",
|
||||||
|
"完整吉时信息",
|
||||||
|
"冲煞详情",
|
||||||
|
"宜忌事项",
|
||||||
|
"干支五行",
|
||||||
|
"吉星神煞",
|
||||||
|
"生成分享海报"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"事项类型建议映射": {
|
||||||
|
"kaichang": {
|
||||||
|
"label": "开业开市",
|
||||||
|
"advice": "财星高照,禄马同乡。宜选水日或金日开市,取金水相生之意。店铺朝向宜避开五黄煞位。"
|
||||||
|
},
|
||||||
|
"jiehun": {
|
||||||
|
"label": "嫁娶结婚",
|
||||||
|
"advice": "红鸾星动,天喜临门。本月利于婚嫁,宜选双日,寓意成双成对。忌选父母生辰冲克之日。"
|
||||||
|
},
|
||||||
|
"qiaoqian": {
|
||||||
|
"label": "乔迁入宅",
|
||||||
|
"advice": "安床纳财,移徙大吉。入宅时宜手持贵重物品,忌空手入屋。当日宜开火煮汤圆,寓意团圆美满。"
|
||||||
|
},
|
||||||
|
"dongtu": {
|
||||||
|
"label": "动土修造",
|
||||||
|
"advice": "择吉动土,百事顺遂。宜选土旺之日,避开岁破、月破之时。动土前宜祭拜土地神,以求平安。"
|
||||||
|
},
|
||||||
|
"qiuzi": {
|
||||||
|
"label": "求嗣祈福",
|
||||||
|
"advice": "天喜临门,子孙昌盛。宜选月德、天德之日祈福,诚心祷告,必得神佑。"
|
||||||
|
},
|
||||||
|
"other": {
|
||||||
|
"label": "祈福择吉",
|
||||||
|
"advice": "顺应天时,趋吉避凶。所选吉日均为黄道瑞日,结合福主八字喜用神,可助事半功倍,平安顺遂。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
284
前端源码/uni-app/bazi-zeji-api-spec.json
Normal file
284
前端源码/uni-app/bazi-zeji-api-spec.json
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
{
|
||||||
|
"八字择吉接口规范": {
|
||||||
|
"接口名称": "八字择吉测算",
|
||||||
|
"接口路径": "/api/v1/yifan/yifan_bazi_zeji/calculate",
|
||||||
|
"请求方法": "POST",
|
||||||
|
"入参 (Request)": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "姓名",
|
||||||
|
"example": "王召阳"
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "性别",
|
||||||
|
"enum": [
|
||||||
|
"male",
|
||||||
|
"female"
|
||||||
|
],
|
||||||
|
"enum_labels": {
|
||||||
|
"male": "男",
|
||||||
|
"female": "女"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"birth_date": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "生辰八字(显示格式)",
|
||||||
|
"example": "1999年11月15日 子时"
|
||||||
|
},
|
||||||
|
"birth_date_api": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "生辰八字(API格式)",
|
||||||
|
"format": "YYYY-MM-DD HH:mm:ss",
|
||||||
|
"example": "1999-11-15 00:30:00"
|
||||||
|
},
|
||||||
|
"birth_place": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "出生地",
|
||||||
|
"example": "临沂市"
|
||||||
|
},
|
||||||
|
"zeji_type": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "择吉类型",
|
||||||
|
"enum": [
|
||||||
|
"wedding",
|
||||||
|
"business",
|
||||||
|
"move",
|
||||||
|
"travel",
|
||||||
|
"investment",
|
||||||
|
"surgery",
|
||||||
|
"contract",
|
||||||
|
"other"
|
||||||
|
],
|
||||||
|
"enum_labels": {
|
||||||
|
"wedding": "婚嫁择吉(结婚、订婚、求婚)",
|
||||||
|
"business": "开业择吉(开店、公司成立、项目启动)",
|
||||||
|
"move": "搬家择吉(搬迁、入宅、装修)",
|
||||||
|
"travel": "出行择吉(旅游、出差、远行)",
|
||||||
|
"investment": "投资择吉(投资、理财、购买)",
|
||||||
|
"surgery": "手术择吉(医疗手术、体检)",
|
||||||
|
"contract": "签约择吉(合同签署、协议达成)",
|
||||||
|
"other": "其他择吉(其他重要事项)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zeji_purpose": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "择吉目的描述",
|
||||||
|
"example": "选择结婚吉日,希望婚姻美满幸福"
|
||||||
|
},
|
||||||
|
"date_range_start": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "期望日期范围开始",
|
||||||
|
"format": "YYYY-MM-DD",
|
||||||
|
"example": "2026-05-01"
|
||||||
|
},
|
||||||
|
"date_range_end": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "期望日期范围结束",
|
||||||
|
"format": "YYYY-MM-DD",
|
||||||
|
"example": "2027-05-01"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"入参示例": {
|
||||||
|
"name": "王召阳",
|
||||||
|
"gender": "male",
|
||||||
|
"birth_date": "1999年11月15日 子时",
|
||||||
|
"birth_date_api": "1999-11-15 00:30:00",
|
||||||
|
"birth_place": "临沂市",
|
||||||
|
"zeji_type": "wedding",
|
||||||
|
"zeji_purpose": "选择结婚吉日,希望婚姻美满幸福",
|
||||||
|
"date_range_start": "2026-05-01",
|
||||||
|
"date_range_end": "2027-05-01"
|
||||||
|
},
|
||||||
|
"出参 (Response)": {
|
||||||
|
"code": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "状态码,200表示成功"
|
||||||
|
},
|
||||||
|
"msg": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "返回消息"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "八字择吉结果数据",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "姓名",
|
||||||
|
"example": "王召阳"
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "性别",
|
||||||
|
"example": "male"
|
||||||
|
},
|
||||||
|
"birth_date": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "生辰八字",
|
||||||
|
"example": "1999年11月15日 子时"
|
||||||
|
},
|
||||||
|
"birth_place": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "出生地",
|
||||||
|
"example": "临沂市"
|
||||||
|
},
|
||||||
|
"zeji_type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "择吉类型",
|
||||||
|
"example": "wedding"
|
||||||
|
},
|
||||||
|
"zeji_purpose": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "择吉目的",
|
||||||
|
"example": "选择结婚吉日,希望婚姻美满幸福"
|
||||||
|
},
|
||||||
|
"date_range_start": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "日期范围开始",
|
||||||
|
"example": "2026-05-01"
|
||||||
|
},
|
||||||
|
"date_range_end": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "日期范围结束",
|
||||||
|
"example": "2027-05-01"
|
||||||
|
},
|
||||||
|
"advice": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "择吉建议",
|
||||||
|
"example": "红鸾星动,天喜临门。本月利于婚嫁,宜选双日,寓意成双成对。忌选父母生辰冲克之日。"
|
||||||
|
},
|
||||||
|
"dates": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "吉日列表",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"date": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "日期",
|
||||||
|
"example": "2026-05-18"
|
||||||
|
},
|
||||||
|
"lunar": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "农历日期",
|
||||||
|
"example": "四月十一"
|
||||||
|
},
|
||||||
|
"desc": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "日期描述",
|
||||||
|
"example": "天德合日,官印相生"
|
||||||
|
},
|
||||||
|
"score": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "吉运指数",
|
||||||
|
"example": 98,
|
||||||
|
"range": "0-100"
|
||||||
|
},
|
||||||
|
"hours": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "吉时",
|
||||||
|
"example": "巳时(09-11), 未时(13-15)"
|
||||||
|
},
|
||||||
|
"clash": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "冲煞",
|
||||||
|
"example": "冲猪"
|
||||||
|
},
|
||||||
|
"suitable": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "宜",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"嫁娶",
|
||||||
|
"开市",
|
||||||
|
"出行"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"avoid": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "忌",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"动土",
|
||||||
|
"破土",
|
||||||
|
"安葬"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"出参示例": {
|
||||||
|
"code": 200,
|
||||||
|
"msg": "测算成功",
|
||||||
|
"data": {
|
||||||
|
"name": "王召阳",
|
||||||
|
"gender": "male",
|
||||||
|
"birth_date": "1999年11月15日 子时",
|
||||||
|
"birth_place": "临沂市",
|
||||||
|
"zeji_type": "wedding",
|
||||||
|
"zeji_purpose": "选择结婚吉日,希望婚姻美满幸福",
|
||||||
|
"date_range_start": "2026-05-01",
|
||||||
|
"date_range_end": "2027-05-01",
|
||||||
|
"advice": "红鸾星动,天喜临门。本月利于婚嫁,宜选双日,寓意成双成对。忌选父母生辰冲克之日。",
|
||||||
|
"dates": [
|
||||||
|
{
|
||||||
|
"date": "2026-05-18",
|
||||||
|
"lunar": "四月十一",
|
||||||
|
"desc": "天德合日,官印相生",
|
||||||
|
"score": 98,
|
||||||
|
"hours": "巳时(09-11), 未时(13-15)",
|
||||||
|
"clash": "冲猪",
|
||||||
|
"suitable": [
|
||||||
|
"嫁娶",
|
||||||
|
"开市",
|
||||||
|
"出行",
|
||||||
|
"祈福",
|
||||||
|
"纳采"
|
||||||
|
],
|
||||||
|
"avoid": [
|
||||||
|
"动土",
|
||||||
|
"破土",
|
||||||
|
"安葬"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-05-22",
|
||||||
|
"lunar": "四月十五",
|
||||||
|
"desc": "月德合日,三合临旺",
|
||||||
|
"score": 95,
|
||||||
|
"hours": "辰时(07-09), 午时(11-13)",
|
||||||
|
"clash": "冲兔",
|
||||||
|
"suitable": [
|
||||||
|
"嫁娶",
|
||||||
|
"祭祀",
|
||||||
|
"祈福",
|
||||||
|
"求嗣"
|
||||||
|
],
|
||||||
|
"avoid": [
|
||||||
|
"开市",
|
||||||
|
"动土",
|
||||||
|
"修造"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1763
前端源码/uni-app/components/AnalysisResult.vue
Normal file
1763
前端源码/uni-app/components/AnalysisResult.vue
Normal file
File diff suppressed because it is too large
Load Diff
115
前端源码/uni-app/components/CustomTabBar.vue
Normal file
115
前端源码/uni-app/components/CustomTabBar.vue
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<view class="tabbar">
|
||||||
|
<view class="tabbar-inner">
|
||||||
|
<view v-for="tab in tabs" :key="tab.id" class="tab-btn" :class="{ active: currentTab === tab.id }"
|
||||||
|
@click="$emit('change', tab.id)">
|
||||||
|
<view class="tab-icon">
|
||||||
|
<HomeIcon v-if="tab.id === 'home'" size="24" class="tab-icon-svg" />
|
||||||
|
<TestIcon v-else-if="tab.id === 'test'" size="24" class="tab-icon-svg" />
|
||||||
|
<NamingIcon v-else-if="tab.id === 'naming'" size="24" class="tab-icon-svg" />
|
||||||
|
<RenamingIcon v-else-if="tab.id === 'renaming'" size="24" class="tab-icon-svg" />
|
||||||
|
<ProfileIcon v-else-if="tab.id === 'profile'" size="24" class="tab-icon-svg" />
|
||||||
|
<text v-else class="icon-dot" />
|
||||||
|
</view>
|
||||||
|
<text class="tab-label">{{ tab.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import HomeIcon from "./icons/HomeIcon.vue";
|
||||||
|
import TestIcon from "./icons/TestIcon.vue";
|
||||||
|
import NamingIcon from "./icons/NamingIcon.vue";
|
||||||
|
import RenamingIcon from "./icons/RenamingIcon.vue";
|
||||||
|
import ProfileIcon from "./icons/ProfileIcon.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
currentTab: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: "home", label: "首页" },
|
||||||
|
{ id: "test", label: "测名" },
|
||||||
|
{ id: "naming", label: "起名" },
|
||||||
|
{ id: "renaming", label: "改名" },
|
||||||
|
{ id: "profile", label: "我的" }
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tabbar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #fdfbf7;
|
||||||
|
border-top: 2px solid #dcd3c9;
|
||||||
|
box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.05);
|
||||||
|
z-index: 999;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
padding-bottom: constant(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabbar-inner {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
height: 60px;
|
||||||
|
padding: 10px 0;
|
||||||
|
max-width: 750px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
color: #5a5a5a;
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
color: #8b2323;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(139, 35, 35, 0.05);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn:not(.active) .tab-icon {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-icon-svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0.9;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
508
前端源码/uni-app/components/MarketingNavBar.vue
Normal file
508
前端源码/uni-app/components/MarketingNavBar.vue
Normal file
@@ -0,0 +1,508 @@
|
|||||||
|
<!--
|
||||||
|
响应式营销站顶栏(参考桌面端横向导航 + 手机端抽屉菜单)。
|
||||||
|
用法:在页面中 <MarketingNavBar />,按需改 navSections / brand / @login
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<view class="mnav">
|
||||||
|
<view class="mnav-inner">
|
||||||
|
<view class="mnav-brand">
|
||||||
|
<text class="mnav-logo">H</text>
|
||||||
|
<text class="mnav-title">海珀AI</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 桌面:横向链接 -->
|
||||||
|
<view class="mnav-desktop">
|
||||||
|
<text
|
||||||
|
v-for="link in topLinks"
|
||||||
|
:key="link.id"
|
||||||
|
class="mnav-link"
|
||||||
|
:class="{ 'mnav-link--active': activeDropdown === link.id }"
|
||||||
|
@mouseenter="onDesktopEnter(link)"
|
||||||
|
@mouseleave="onDesktopLeave"
|
||||||
|
@click="onTopLinkClick(link)"
|
||||||
|
>
|
||||||
|
{{ link.label }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="mnav-right">
|
||||||
|
<text class="mnav-login mnav-login--bar" @click="emit('login')">登录</text>
|
||||||
|
<!-- 手机:汉堡 -->
|
||||||
|
<view class="mnav-burger" @click="openDrawer">
|
||||||
|
<view class="mnav-burger-line" />
|
||||||
|
<view class="mnav-burger-line" />
|
||||||
|
<view class="mnav-burger-line" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 桌面:下拉面板 -->
|
||||||
|
<view
|
||||||
|
v-if="activeDropdown === 'solutions' && showMega"
|
||||||
|
class="mnav-mega"
|
||||||
|
@mouseenter="setMegaHover(true)"
|
||||||
|
@mouseleave="setMegaHover(false)"
|
||||||
|
>
|
||||||
|
<view class="mnav-mega-inner">
|
||||||
|
<view v-for="col in megaColumns" :key="col.title" class="mnav-mega-col">
|
||||||
|
<text class="mnav-mega-title">{{ col.title }}</text>
|
||||||
|
<text v-for="(item, i) in col.items" :key="i" class="mnav-mega-item" @click="emitNav(item)">{{ item }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 手机:遮罩 + 抽屉 -->
|
||||||
|
<view v-if="drawerOpen" class="mnav-overlay" @click="closeDrawer" />
|
||||||
|
<view class="mnav-drawer" :class="{ 'mnav-drawer--open': drawerOpen }">
|
||||||
|
<view class="mnav-drawer-head">
|
||||||
|
<text class="mnav-drawer-title">菜单</text>
|
||||||
|
<view class="mnav-drawer-close" @click="closeDrawer">
|
||||||
|
<text class="mnav-drawer-close-x">×</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<scroll-view scroll-y class="mnav-drawer-scroll">
|
||||||
|
<view
|
||||||
|
v-for="sec in navSections"
|
||||||
|
:key="sec.id"
|
||||||
|
class="mnav-acc"
|
||||||
|
>
|
||||||
|
<view class="mnav-acc-head" @click="toggleAcc(sec.id)">
|
||||||
|
<text class="mnav-acc-title">{{ sec.title }}</text>
|
||||||
|
<text class="mnav-acc-chevron">{{ expandedId === sec.id ? '−' : '+' }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="expandedId === sec.id" class="mnav-acc-body">
|
||||||
|
<text
|
||||||
|
v-for="(line, li) in sec.items"
|
||||||
|
:key="li"
|
||||||
|
class="mnav-acc-link"
|
||||||
|
@click="onDrawerItem(line)"
|
||||||
|
>
|
||||||
|
{{ line }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="mnav-drawer-login-wrap">
|
||||||
|
<text class="mnav-drawer-login" @click="onDrawerLogin">登录</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
login: [];
|
||||||
|
navigate: [payload: { label: string; section?: string }];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
/** 顶部一级链接(无下拉的直接 emit) */
|
||||||
|
const topLinks = [
|
||||||
|
{ id: 'home', label: '首页' },
|
||||||
|
{ id: 'cases', label: '客户案例' },
|
||||||
|
{ id: 'rpa', label: '电商RPA实例' },
|
||||||
|
{ id: 'solutions', label: '行业解决方案', mega: true },
|
||||||
|
{ id: 'eco', label: '生态合作' },
|
||||||
|
{ id: 'about', label: '关于HyperAigc' },
|
||||||
|
{ id: 'user', label: '个人中心' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** 桌面端「行业解决方案」三列(可按后端配置替换) */
|
||||||
|
const megaColumns = [
|
||||||
|
{
|
||||||
|
title: '政务与公用事业',
|
||||||
|
items: ['党务政务', '研政购务', '政府政策运营', '舆情分析'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '企业解决方案',
|
||||||
|
items: ['智能知识', '供应链管理', 'CRM', '对话管理', '人力资源', '营销自动化', '安全监督', '行业培训'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '领域解决方案',
|
||||||
|
items: ['金融科技', '电商零售', '在线教育'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 手机抽屉:分组折叠(与桌面信息等价,纵向更易读) */
|
||||||
|
const navSections = [
|
||||||
|
{
|
||||||
|
id: 'gov',
|
||||||
|
title: '政务与公用事业',
|
||||||
|
items: ['党务政务', '研政购务', '政府政策运营', '舆情分析'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ent',
|
||||||
|
title: '企业解决方案',
|
||||||
|
items: ['智能知识', '供应链管理', 'CRM', '对话管理', '人力资源', '营销自动化', '安全监督', '行业培训'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'domain',
|
||||||
|
title: '领域解决方案',
|
||||||
|
items: ['金融科技', '电商零售', '在线教育'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'more',
|
||||||
|
title: '更多',
|
||||||
|
items: ['首页', '万年历', '起名服务', '姓名测试', '缘分合盘', '客户案例', '电商RPA实例', '生态合作', '关于HyperAigc', '个人中心'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const drawerOpen = ref(false);
|
||||||
|
const expandedId = ref<string | null>('gov');
|
||||||
|
const activeDropdown = ref<string | null>(null);
|
||||||
|
const showMega = ref(false);
|
||||||
|
let megaTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
const megaEnter = ref(false);
|
||||||
|
|
||||||
|
function openDrawer() {
|
||||||
|
drawerOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDrawer() {
|
||||||
|
drawerOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAcc(id: string) {
|
||||||
|
expandedId.value = expandedId.value === id ? null : id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrawerItem(label: string) {
|
||||||
|
emit('navigate', { label });
|
||||||
|
closeDrawer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrawerLogin() {
|
||||||
|
emit('login');
|
||||||
|
closeDrawer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDesktopEnter(link: (typeof topLinks)[number]) {
|
||||||
|
if (megaTimer) {
|
||||||
|
clearTimeout(megaTimer);
|
||||||
|
megaTimer = null;
|
||||||
|
}
|
||||||
|
activeDropdown.value = link.id;
|
||||||
|
showMega.value = link.id === 'solutions' && 'mega' in link && link.mega;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDesktopLeave() {
|
||||||
|
megaTimer = setTimeout(() => {
|
||||||
|
if (!megaEnter.value) {
|
||||||
|
activeDropdown.value = null;
|
||||||
|
showMega.value = false;
|
||||||
|
}
|
||||||
|
}, 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMegaHover(v: boolean) {
|
||||||
|
if (v && megaTimer) {
|
||||||
|
clearTimeout(megaTimer);
|
||||||
|
megaTimer = null;
|
||||||
|
}
|
||||||
|
megaEnter.value = v;
|
||||||
|
if (!v) {
|
||||||
|
megaTimer = setTimeout(() => {
|
||||||
|
if (!megaEnter.value) {
|
||||||
|
activeDropdown.value = null;
|
||||||
|
showMega.value = false;
|
||||||
|
}
|
||||||
|
}, 80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTopLinkClick(link: (typeof topLinks)[number]) {
|
||||||
|
if (link.id === 'solutions') {
|
||||||
|
// 窄屏无 hover:点击展开/收起 mega;宽屏以悬停为准,避免误触反复开关
|
||||||
|
if (typeof window !== 'undefined' && window.innerWidth < 992) {
|
||||||
|
showMega.value = !showMega.value;
|
||||||
|
activeDropdown.value = showMega.value ? 'solutions' : null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit('navigate', { label: link.label });
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitNav(label: string) {
|
||||||
|
emit('navigate', { label, section: '行业解决方案' });
|
||||||
|
activeDropdown.value = null;
|
||||||
|
showMega.value = false;
|
||||||
|
megaEnter.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mnav {
|
||||||
|
position: relative;
|
||||||
|
z-index: 200;
|
||||||
|
background: rgba(8, 12, 28, 0.85);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-inner {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 12px 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-logo {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(145deg, #3b82f6, #1d4ed8);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e8eefc;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-desktop {
|
||||||
|
display: none;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-link {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(226, 232, 255, 0.85);
|
||||||
|
padding: 6px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-link--active {
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-login {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #e8eefc;
|
||||||
|
padding: 8px 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-burger {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-burger-line {
|
||||||
|
height: 2px;
|
||||||
|
background: #e8eefc;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-mega {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 100%;
|
||||||
|
padding: 16px 20px 24px;
|
||||||
|
background: rgba(255, 255, 255, 0.98);
|
||||||
|
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-mega-inner {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-mega-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-bottom: 2px solid #1e3a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-mega-item {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #334155;
|
||||||
|
padding: 6px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-mega-item:active {
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
z-index: 210;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-drawer {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: min(88vw, 360px);
|
||||||
|
height: 100%;
|
||||||
|
background: #f8fafc;
|
||||||
|
z-index: 220;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.28s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 8px 0 32px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-drawer--open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-drawer-head {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 18px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-drawer-title {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-drawer-close {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-drawer-close-x {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-drawer-scroll {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
padding: 8px 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-acc {
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-acc-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-acc-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-acc-chevron {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-acc-body {
|
||||||
|
padding: 0 18px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-acc-link {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #475569;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-acc-link:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-drawer-login-wrap {
|
||||||
|
padding: 20px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-drawer-login {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #f8fafc;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.mnav-desktop {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-burger {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
.mnav-mega {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnav-login--bar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
376
前端源码/uni-app/components/MysticCompass.vue
Normal file
376
前端源码/uni-app/components/MysticCompass.vue
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
<template>
|
||||||
|
<view class="mystic-compass-wrapper" :class="{ 'mystic-compass-wrapper--desktop': desktop }">
|
||||||
|
<!-- 手机端:可退出;电脑端测名:仅等待结果,不展示退出 -->
|
||||||
|
<view v-if="!desktop" class="compass-back-btn" @click="handleBack">
|
||||||
|
<text class="compass-back-icon">×</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="mystic-compass">
|
||||||
|
<view class="compass-glow"></view>
|
||||||
|
<view class="compass-svg-wrap">
|
||||||
|
<!-- 外圈虚线圆 -->
|
||||||
|
<view class="circle-outer"></view>
|
||||||
|
<view class="circle-main"></view>
|
||||||
|
|
||||||
|
<!-- 天干地支 -->
|
||||||
|
<view v-for="(char, i) in runes" :key="'rune-' + i" class="rune-char" :class="{ active: i === activeCharIndex }"
|
||||||
|
:style="getRuneStyle(i)">
|
||||||
|
{{ char }}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 八卦符号 - 旋转圈 -->
|
||||||
|
<view class="bagua-circle" :style="{ transform: 'rotate(' + baGuaRotation + 'deg)' }">
|
||||||
|
<view v-for="(gua, i) in baGua" :key="'gua-' + i" class="bagua-char" :style="getBaGuaStyle(i)">
|
||||||
|
{{ gua }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 中心罗盘指针 -->
|
||||||
|
<view class="compass-pointer" :style="{ transform: 'rotate(' + pointerRotation + 'deg)' }">
|
||||||
|
<view class="pointer-bar"></view>
|
||||||
|
<view class="pointer-dot"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载文本 -->
|
||||||
|
<view class="loading-text">
|
||||||
|
<view class="loading-title">{{ title }}</view>
|
||||||
|
<view class="loading-subtitle">{{ subtitle }}</view>
|
||||||
|
<view v-if="!desktop" class="loading-tip">
|
||||||
|
分析时间预计1-2分钟,可点击返回按钮退出,并在我的方案中查看结果
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
/** 电脑端网页:不显示退出按钮与「预计时间 / 我的方案」提示 */
|
||||||
|
desktop?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
title: '天机推演中',
|
||||||
|
subtitle: '易经数理 · 五行生克 · 三才五格',
|
||||||
|
desktop: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
back: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
emit('back');
|
||||||
|
};
|
||||||
|
|
||||||
|
const runes = [
|
||||||
|
"甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸",
|
||||||
|
"子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"
|
||||||
|
];
|
||||||
|
|
||||||
|
const baGua = ["☰", "☱", "☲", "☳", "☴", "☵", "☶", "☷"];
|
||||||
|
|
||||||
|
const activeCharIndex = ref(-1);
|
||||||
|
const baGuaRotation = ref(0);
|
||||||
|
const pointerRotation = ref(0);
|
||||||
|
|
||||||
|
let runeInterval: number | null = null;
|
||||||
|
let baGuaInterval: number | null = null;
|
||||||
|
let pointerInterval: number | null = null;
|
||||||
|
|
||||||
|
const getRuneStyle = (index: number) => {
|
||||||
|
const angle = (index * 360) / runes.length;
|
||||||
|
const radius = 120; // px
|
||||||
|
const angleRad = (angle - 90) * (Math.PI / 180);
|
||||||
|
const x = 150 + radius * Math.cos(angleRad);
|
||||||
|
const y = 150 + radius * Math.sin(angleRad);
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: x + 'px',
|
||||||
|
top: y + 'px',
|
||||||
|
transform: `translate(-50%, -50%) rotate(${angle}deg)`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBaGuaStyle = (index: number) => {
|
||||||
|
const angle = (index * 360) / 8;
|
||||||
|
const radius = 72.5; // px
|
||||||
|
const angleRad = (angle - 90) * (Math.PI / 180);
|
||||||
|
// .bagua-circle 的宽高是 180px,中心点在 (90, 90)
|
||||||
|
const x = 90 + radius * Math.cos(angleRad);
|
||||||
|
const y = 90 + radius * Math.sin(angleRad);
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: x + 'px',
|
||||||
|
top: y + 'px',
|
||||||
|
transform: `translate(-50%, -50%) rotate(${angle}deg)`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 天干地支闪烁
|
||||||
|
let count = 0;
|
||||||
|
runeInterval = setInterval(() => {
|
||||||
|
activeCharIndex.value = count % runes.length;
|
||||||
|
count++;
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// 八卦旋转
|
||||||
|
baGuaInterval = setInterval(() => {
|
||||||
|
baGuaRotation.value += 0.25;
|
||||||
|
}, 30);
|
||||||
|
|
||||||
|
// 指针旋转
|
||||||
|
let pointerCount = 0;
|
||||||
|
pointerInterval = setInterval(() => {
|
||||||
|
pointerCount += 1;
|
||||||
|
pointerRotation.value = pointerCount * 1.2;
|
||||||
|
}, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (runeInterval) clearInterval(runeInterval);
|
||||||
|
if (baGuaInterval) clearInterval(baGuaInterval);
|
||||||
|
if (pointerInterval) clearInterval(pointerInterval);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mystic-compass-wrapper--desktop {
|
||||||
|
background: radial-gradient(ellipse 80% 60% at 50% 30%, rgba(212, 175, 55, 0.06), #0a0a0a 55%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-compass-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
min-height: -webkit-fill-available;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 48px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-back-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 24px;
|
||||||
|
left: 24px;
|
||||||
|
top: calc(24px + env(safe-area-inset-top, 0px));
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(212, 175, 55, 0.1);
|
||||||
|
border: 1px solid rgba(212, 175, 55, 0.3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10000;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-back-btn:active {
|
||||||
|
background: rgba(212, 175, 55, 0.2);
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-back-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #d4af37;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-compass {
|
||||||
|
position: relative;
|
||||||
|
width: min(300px, 80vw);
|
||||||
|
height: min(300px, 80vw);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 260px;
|
||||||
|
min-height: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-glow {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #d4af37;
|
||||||
|
filter: blur(30px);
|
||||||
|
opacity: 0.15;
|
||||||
|
animation: pulse 4s ease-in-out infinite;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.1;
|
||||||
|
transform: translate(-50%, -50%) scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: translate(-50%, -50%) scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-svg-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
transform: scale(min(1, calc(80vw / 300)));
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-outer {
|
||||||
|
position: absolute;
|
||||||
|
left: 5px;
|
||||||
|
top: 5px;
|
||||||
|
width: 290px;
|
||||||
|
height: 290px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px dashed #d4af37;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-main {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
top: 10px;
|
||||||
|
width: 280px;
|
||||||
|
height: 280px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #d4af37;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rune-char {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5a5a5a;
|
||||||
|
font-family: SimSun, serif;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rune-char.active {
|
||||||
|
color: #d4af37;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 0 0 5px #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bagua-circle {
|
||||||
|
position: absolute;
|
||||||
|
left: 60px;
|
||||||
|
top: 60px;
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid rgba(212, 175, 55, 0.1);
|
||||||
|
transition: transform 0.3s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bagua-char {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #d4af37;
|
||||||
|
opacity: 0.6;
|
||||||
|
font-family: SimSun, serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-pointer {
|
||||||
|
position: absolute;
|
||||||
|
left: 150px;
|
||||||
|
top: 150px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
transition: transform 0.05s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointer-bar {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: -80px;
|
||||||
|
width: 2px;
|
||||||
|
height: 80px;
|
||||||
|
background-color: #9c2a2a;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointer-dot {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #d4af37;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Text */
|
||||||
|
.loading-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
animation: fade 2.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-title {
|
||||||
|
color: #d4af37;
|
||||||
|
letter-spacing: 0.4em;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 0 0 10px rgba(212, 175, 55, 0.5);
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-subtitle {
|
||||||
|
color: #a0a0a0;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 0 20px;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-tip {
|
||||||
|
margin-top: 6px;
|
||||||
|
max-width: min(520px, 86vw);
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(226, 226, 226, 0.72);
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
576
前端源码/uni-app/components/MysticDatePicker.vue
Normal file
576
前端源码/uni-app/components/MysticDatePicker.vue
Normal file
@@ -0,0 +1,576 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="mystic-date-picker">
|
||||||
|
<div v-if="isOpen" class="mystic-date-picker-overlay">
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<div class="mystic-date-picker-backdrop" @click="handleClose"></div>
|
||||||
|
|
||||||
|
<!-- Modal Content -->
|
||||||
|
<div class="mystic-date-picker-modal">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mystic-date-picker-header">
|
||||||
|
<div class="mystic-date-picker-close" @click="handleClose">
|
||||||
|
<CloseIcon :size="22" class="mystic-date-picker-icon" />
|
||||||
|
</div>
|
||||||
|
<span class="mystic-date-picker-title">{{ title }}</span>
|
||||||
|
<div class="mystic-date-picker-confirm" @click="handleConfirm">
|
||||||
|
<CheckIcon :size="22" class="mystic-date-picker-icon" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Picker View -->
|
||||||
|
<div class="mystic-date-picker-view">
|
||||||
|
<div class="mystic-date-picker-indicator"></div>
|
||||||
|
|
||||||
|
<!-- Year Column -->
|
||||||
|
<div class="mystic-date-picker-column" @scroll="handleScroll($event, 0)">
|
||||||
|
<div class="mystic-date-picker-padding"></div>
|
||||||
|
<div v-for="(y, idx) in years" :key="y" :ref="el => setColumnRef(el, 0, idx)"
|
||||||
|
class="mystic-date-picker-item" :class="{ active: pickerValue[0] === idx }" @click="selectItem(0, idx)">
|
||||||
|
<span>{{ y }}年</span>
|
||||||
|
</div>
|
||||||
|
<div class="mystic-date-picker-padding"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Month Column -->
|
||||||
|
<div class="mystic-date-picker-column" @scroll="handleScroll($event, 1)">
|
||||||
|
<div class="mystic-date-picker-padding"></div>
|
||||||
|
<div v-for="(m, idx) in months" :key="idx" :ref="el => setColumnRef(el, 1, idx)"
|
||||||
|
class="mystic-date-picker-item" :class="{ active: pickerValue[1] === idx }" @click="selectItem(1, idx)">
|
||||||
|
<span>{{ m }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mystic-date-picker-padding"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Day Column -->
|
||||||
|
<div class="mystic-date-picker-column" @scroll="handleScroll($event, 2)">
|
||||||
|
<div class="mystic-date-picker-padding"></div>
|
||||||
|
<div v-for="(d, idx) in days" :key="d.val" :ref="el => setColumnRef(el, 2, idx)"
|
||||||
|
class="mystic-date-picker-item" :class="{ active: pickerValue[2] === idx }" @click="selectItem(2, idx)">
|
||||||
|
<span>{{ d.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mystic-date-picker-padding"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Time Column -->
|
||||||
|
<div class="mystic-date-picker-column" @scroll="handleScroll($event, 3)">
|
||||||
|
<div class="mystic-date-picker-padding"></div>
|
||||||
|
<div v-for="(s, idx) in shichenOptions" :key="s.id" :ref="el => setColumnRef(el, 3, idx)"
|
||||||
|
class="mystic-date-picker-item mystic-date-picker-item-time" :class="{ active: pickerValue[3] === idx }"
|
||||||
|
@click="selectItem(3, idx)">
|
||||||
|
<span class="mystic-date-picker-time-name">{{ s.name }}</span>
|
||||||
|
<span class="mystic-date-picker-time-detail">{{ s.time }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mystic-date-picker-padding"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer Tip -->
|
||||||
|
<div class="mystic-date-picker-footer">
|
||||||
|
<span class="mystic-date-picker-tip">{{ footerTip || '滑动列表选择 · 系统自动换算干支' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, nextTick } from 'vue';
|
||||||
|
import CloseIcon from './icons/CloseIcon.vue';
|
||||||
|
import CheckIcon from './icons/CheckIcon.vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
title?: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
/** 年份列下界(公历,含)。与 maxYear 同时用于自定义区间(如择吉期望范围可选至未来多年) */
|
||||||
|
minYear?: number;
|
||||||
|
/** 年份列上界(公历,含) */
|
||||||
|
maxYear?: number;
|
||||||
|
/** 底部提示,便于与其它场景日期选择区分 */
|
||||||
|
footerTip?: string;
|
||||||
|
/** 为 true 时公历日期不可晚于「今天」(用于择吉期望开始日等) */
|
||||||
|
capAtToday?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: '请择良辰',
|
||||||
|
defaultValue: '',
|
||||||
|
minYear: undefined,
|
||||||
|
maxYear: undefined,
|
||||||
|
footerTip: '',
|
||||||
|
capAtToday: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
confirm: [val: string, apiVal: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 时辰对照表
|
||||||
|
const SHI_CHEN = [
|
||||||
|
{ id: 'zi', name: '子时', time: '23:00-00:59', hour: '00:00:00' },
|
||||||
|
{ id: 'chou', name: '丑时', time: '01:00-02:59', hour: '02:00:00' },
|
||||||
|
{ id: 'yin', name: '寅时', time: '03:00-04:59', hour: '04:00:00' },
|
||||||
|
{ id: 'mao', name: '卯时', time: '05:00-06:59', hour: '06:00:00' },
|
||||||
|
{ id: 'chen', name: '辰时', time: '07:00-08:59', hour: '08:00:00' },
|
||||||
|
{ id: 'si', name: '巳时', time: '09:00-10:59', hour: '10:00:00' },
|
||||||
|
{ id: 'wu', name: '午时', time: '11:00-12:59', hour: '12:00:00' },
|
||||||
|
{ id: 'wei', name: '未时', time: '13:00-14:59', hour: '14:00:00' },
|
||||||
|
{ id: 'shen', name: '申时', time: '15:00-16:59', hour: '16:00:00' },
|
||||||
|
{ id: 'you', name: '酉时', time: '17:00-18:59', hour: '18:00:00' },
|
||||||
|
{ id: 'xu', name: '戌时', time: '19:00-20:59', hour: '20:00:00' },
|
||||||
|
{ id: 'hai', name: '亥时', time: '21:00-22:59', hour: '22:00:00' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const CH_NUM = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
|
||||||
|
const MONTHS = ['正月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '冬月', '腊月'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 年份列(降序):默认从当年起共 86 年(生辰等场景,不可超过当年);
|
||||||
|
* 若传入 minYear/maxYear 则按该区间生成(用于择吉期望日期等可选未来多年)。
|
||||||
|
*/
|
||||||
|
const years = computed(() => {
|
||||||
|
const nowY = new Date().getFullYear();
|
||||||
|
const useCustom = props.minYear !== undefined || props.maxYear !== undefined;
|
||||||
|
if (!useCustom) {
|
||||||
|
const maxY = nowY;
|
||||||
|
const minY = maxY - 85;
|
||||||
|
return Array.from({ length: 86 }, (_, i) => maxY - i);
|
||||||
|
}
|
||||||
|
const maxY = props.maxYear ?? nowY;
|
||||||
|
const minY = props.minYear ?? maxY - 85;
|
||||||
|
const hi = Math.max(minY, maxY);
|
||||||
|
const lo = Math.min(minY, maxY);
|
||||||
|
return Array.from({ length: hi - lo + 1 }, (_, i) => hi - i);
|
||||||
|
});
|
||||||
|
const months = MONTHS;
|
||||||
|
const shichenOptions = SHI_CHEN;
|
||||||
|
|
||||||
|
const pickerValue = ref([0, 0, 0, 0]);
|
||||||
|
const columnRefs = ref<Array<Array<HTMLElement | null>>>([[], [], [], []]);
|
||||||
|
const ITEM_HEIGHT = 50;
|
||||||
|
|
||||||
|
const setColumnRef = (el: any, columnIdx: number, itemIdx: number) => {
|
||||||
|
if (el) {
|
||||||
|
if (!columnRefs.value[columnIdx]) {
|
||||||
|
columnRefs.value[columnIdx] = [];
|
||||||
|
}
|
||||||
|
columnRefs.value[columnIdx][itemIdx] = el;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLeapYear = (year: number): boolean => {
|
||||||
|
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDaysInMonth = (year: number, month: number): number => {
|
||||||
|
const daysMap = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||||
|
if (month === 1 && isLeapYear(year)) {
|
||||||
|
return 29;
|
||||||
|
}
|
||||||
|
return daysMap[month];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDayName = (d: number): string => {
|
||||||
|
if (d <= 10) return `初${CH_NUM[d]}`;
|
||||||
|
if (d === 20) return '二十';
|
||||||
|
if (d === 30) return '三十';
|
||||||
|
if (d < 20) return `十${CH_NUM[d - 10]}`;
|
||||||
|
if (d < 30) return `廿${CH_NUM[d - 20]}`;
|
||||||
|
return `三十${CH_NUM[d - 30]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const days = computed(() => {
|
||||||
|
const [yearIdx, monthIdx] = pickerValue.value;
|
||||||
|
const year = years.value[yearIdx] || 2000;
|
||||||
|
const daysCount = getDaysInMonth(year, monthIdx);
|
||||||
|
|
||||||
|
return Array.from({ length: daysCount }, (_, i) => ({
|
||||||
|
val: i + 1,
|
||||||
|
name: getDayName(i + 1)
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const scrollToIndex = (columnIdx: number, index: number, smooth = true) => {
|
||||||
|
const column = document.querySelectorAll('.mystic-date-picker-column')[columnIdx] as HTMLElement;
|
||||||
|
if (column) {
|
||||||
|
const scrollTop = index * ITEM_HEIGHT;
|
||||||
|
column.scrollTo({
|
||||||
|
top: scrollTop,
|
||||||
|
behavior: smooth ? 'smooth' : 'auto'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectItem = (columnIdx: number, index: number) => {
|
||||||
|
let newValue = [...pickerValue.value];
|
||||||
|
newValue[columnIdx] = index;
|
||||||
|
|
||||||
|
// 检查日期是否超出当月天数
|
||||||
|
if (columnIdx === 0 || columnIdx === 1) {
|
||||||
|
const [yearIdx, monthIdx] = newValue;
|
||||||
|
const year = years.value[yearIdx] || 2000;
|
||||||
|
const maxDays = getDaysInMonth(year, monthIdx);
|
||||||
|
if (newValue[2] >= maxDays) {
|
||||||
|
newValue[2] = maxDays - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newValue = clampPickerValueToTodayMax(newValue, years.value);
|
||||||
|
pickerValue.value = newValue;
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
pickerValue.value.forEach((val, idx) => {
|
||||||
|
scrollToIndex(idx, val, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let scrollTimer: number | null = null;
|
||||||
|
|
||||||
|
const handleScroll = (e: Event, columnIdx: number) => {
|
||||||
|
if (scrollTimer) {
|
||||||
|
clearTimeout(scrollTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollTimer = window.setTimeout(() => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const scrollTop = target.scrollTop;
|
||||||
|
const index = Math.round(scrollTop / ITEM_HEIGHT);
|
||||||
|
|
||||||
|
selectItem(columnIdx, index);
|
||||||
|
}, 150);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseDefaultValue = (val: string, yearList: number[]) => {
|
||||||
|
if (!val) return null;
|
||||||
|
|
||||||
|
const yearMatch = val.match(/(\d+)年/);
|
||||||
|
const monthMatch = val.match(/年(.+?)(?:初|十|廿|三十)/);
|
||||||
|
const shichenMatch = val.match(/(子|丑|寅|卯|辰|巳|午|未|申|酉|戌|亥)时/);
|
||||||
|
|
||||||
|
if (!yearMatch) return null;
|
||||||
|
|
||||||
|
const year = parseInt(yearMatch[1]);
|
||||||
|
const yearIdx = yearList.findIndex(y => y === year);
|
||||||
|
|
||||||
|
let monthIdx = 0;
|
||||||
|
if (monthMatch) {
|
||||||
|
monthIdx = MONTHS.findIndex(m => m === monthMatch[1]);
|
||||||
|
if (monthIdx < 0) monthIdx = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dayIdx = 0;
|
||||||
|
const dayPart = val.replace(/\d+年/, '').replace(/.*月/, '').replace(/(子|丑|寅|卯|辰|巳|午|未|申|酉|戌|亥)时/, '');
|
||||||
|
if (dayPart) {
|
||||||
|
for (let i = 1; i <= 31; i++) {
|
||||||
|
if (getDayName(i) === dayPart) {
|
||||||
|
dayIdx = i - 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let shichenIdx = 0;
|
||||||
|
if (shichenMatch) {
|
||||||
|
shichenIdx = SHI_CHEN.findIndex(s => s.name.startsWith(shichenMatch[1]));
|
||||||
|
if (shichenIdx < 0) shichenIdx = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
yearIdx: yearIdx >= 0 ? yearIdx : 0,
|
||||||
|
monthIdx,
|
||||||
|
dayIdx,
|
||||||
|
shichenIdx
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const clampPickerDay = (parts: number[], yearList: number[]): number[] => {
|
||||||
|
if (!yearList.length) return [0, 0, 0, 0];
|
||||||
|
const yearIdx = Math.min(Math.max(0, parts[0]), yearList.length - 1);
|
||||||
|
const monthIdx = Math.min(Math.max(0, parts[1]), 11);
|
||||||
|
const y = yearList[yearIdx] ?? new Date().getFullYear();
|
||||||
|
const maxD = getDaysInMonth(y, monthIdx);
|
||||||
|
const dayIdx = Math.min(Math.max(0, parts[2]), maxD - 1);
|
||||||
|
const shichenIdx = Math.min(Math.max(0, parts[3]), SHI_CHEN.length - 1);
|
||||||
|
return [yearIdx, monthIdx, dayIdx, shichenIdx];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTodayCalendar = () => {
|
||||||
|
const n = new Date();
|
||||||
|
return { y: n.getFullYear(), m: n.getMonth(), d: n.getDate() };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** capAtToday:将公历日期限制在「今天」及之前 */
|
||||||
|
const clampPickerValueToTodayMax = (parts: number[], yearList: number[]): number[] => {
|
||||||
|
let next = clampPickerDay(parts, yearList);
|
||||||
|
if (!props.capAtToday || !yearList.length) return next;
|
||||||
|
|
||||||
|
const t = getTodayCalendar();
|
||||||
|
let [yi, mi, di, si] = next;
|
||||||
|
const sy = yearList[yi];
|
||||||
|
|
||||||
|
if (sy > t.y) {
|
||||||
|
const ti = yearList.findIndex((yr) => yr === t.y);
|
||||||
|
if (ti >= 0) next = clampPickerDay([ti, t.m, t.d - 1, si], yearList);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
if (sy < t.y) return next;
|
||||||
|
|
||||||
|
if (mi > t.m) {
|
||||||
|
next = clampPickerDay([yi, t.m, t.d - 1, si], yearList);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
if (mi < t.m) return next;
|
||||||
|
|
||||||
|
if (di > t.d - 1) {
|
||||||
|
next = clampPickerDay([yi, mi, t.d - 1, si], yearList);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => props.isOpen, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
const ylist = years.value;
|
||||||
|
const parsed = parseDefaultValue(props.defaultValue, ylist);
|
||||||
|
if (parsed) {
|
||||||
|
const yi = Math.min(Math.max(0, parsed.yearIdx), Math.max(0, ylist.length - 1));
|
||||||
|
pickerValue.value = clampPickerValueToTodayMax(
|
||||||
|
[yi, parsed.monthIdx, parsed.dayIdx, parsed.shichenIdx],
|
||||||
|
ylist
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const now = new Date();
|
||||||
|
const currentYear = now.getFullYear();
|
||||||
|
const yearIdx = ylist.findIndex(y => y === currentYear);
|
||||||
|
const monthIdx = now.getMonth();
|
||||||
|
const dayIdx = now.getDate() - 1;
|
||||||
|
const y = yearIdx >= 0 ? ylist[yearIdx] : ylist[0];
|
||||||
|
const pickY = yearIdx >= 0 ? yearIdx : 0;
|
||||||
|
|
||||||
|
pickerValue.value = clampPickerValueToTodayMax(
|
||||||
|
[
|
||||||
|
pickY,
|
||||||
|
monthIdx,
|
||||||
|
Math.min(dayIdx, getDaysInMonth(y, monthIdx) - 1),
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
ylist
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
pickerValue.value.forEach((val, idx) => {
|
||||||
|
scrollToIndex(idx, val, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
const ylist = years.value;
|
||||||
|
pickerValue.value = clampPickerValueToTodayMax(pickerValue.value, ylist);
|
||||||
|
const [yearIdx, monthIdx, dayIdx, shichenIdx] = pickerValue.value;
|
||||||
|
const selectedYear = years.value[yearIdx] ?? years.value[0];
|
||||||
|
const selectedMonth = MONTHS[monthIdx];
|
||||||
|
const selectedDay = days.value[dayIdx]?.name || getDayName(dayIdx + 1);
|
||||||
|
const selectedShichen = shichenOptions[shichenIdx].name;
|
||||||
|
|
||||||
|
const displayStr = `${selectedYear}年${selectedMonth}${selectedDay}${selectedShichen}`;
|
||||||
|
|
||||||
|
const month = String(monthIdx + 1).padStart(2, '0');
|
||||||
|
const day = String(dayIdx + 1).padStart(2, '0');
|
||||||
|
const hour = shichenOptions[shichenIdx].hour;
|
||||||
|
const apiStr = `${selectedYear}-${month}-${day} ${hour}`;
|
||||||
|
|
||||||
|
emit('confirm', displayStr, apiStr);
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mystic-date-picker-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(26, 26, 26, 0.6);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-modal {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
background: #fcfaf5;
|
||||||
|
border-top-left-radius: 16px;
|
||||||
|
border-top-right-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.25);
|
||||||
|
border-top: 4px solid #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #eaddcf;
|
||||||
|
background: #f9f7f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-close,
|
||||||
|
.mystic-date-picker-confirm {
|
||||||
|
padding: 8px;
|
||||||
|
color: #5a5a5a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-close:active,
|
||||||
|
.mystic-date-picker-confirm:active {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-confirm {
|
||||||
|
color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-icon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c2c2c;
|
||||||
|
letter-spacing: 0.3em;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-view {
|
||||||
|
height: 300px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 50px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
border-top: 1px solid #dcd3c9;
|
||||||
|
border-bottom: 1px solid #dcd3c9;
|
||||||
|
background: rgba(139, 35, 35, 0.02);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-column {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
scroll-snap-type: y mandatory;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-column::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-padding {
|
||||||
|
height: 125px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-item {
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
scroll-snap-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-item.active {
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-item-time {
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-time-name {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-item.active .mystic-date-picker-time-name {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-time-detail {
|
||||||
|
font-size: 10px;
|
||||||
|
opacity: 0.6;
|
||||||
|
color: #8a8a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-footer {
|
||||||
|
background: #f9f7f2;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 1px solid #eaddcf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-tip {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transition */
|
||||||
|
.mystic-date-picker-enter-active,
|
||||||
|
.mystic-date-picker-leave-active {
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-enter-active .mystic-date-picker-modal,
|
||||||
|
.mystic-date-picker-leave-active .mystic-date-picker-modal {
|
||||||
|
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-enter-from,
|
||||||
|
.mystic-date-picker-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-date-picker-enter-from .mystic-date-picker-modal,
|
||||||
|
.mystic-date-picker-leave-to .mystic-date-picker-modal {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
487
前端源码/uni-app/components/MysticFantasyBg.vue
Normal file
487
前端源码/uni-app/components/MysticFantasyBg.vue
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
<template>
|
||||||
|
<view class="mfgb" aria-hidden="true">
|
||||||
|
<view class="mfgb__base" />
|
||||||
|
<!-- 暗纹:万字锦地(极低透明度) -->
|
||||||
|
<view class="mfgb__wanzi" />
|
||||||
|
<view class="mfgb__ink" />
|
||||||
|
<view
|
||||||
|
v-for="c in clouds"
|
||||||
|
:key="c.id"
|
||||||
|
class="mfgb__cloud"
|
||||||
|
:style="getCloudStyle(c)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 八卦 + 太极:先天卦序,极慢旋转 -->
|
||||||
|
<view class="mfgb__bagua-wrap">
|
||||||
|
<svg
|
||||||
|
class="mfgb__bagua-svg mfgb__bagua-spin"
|
||||||
|
viewBox="-100 -100 200 200"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="mfgb-gold-stroke" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color: rgba(212, 175, 55, 0.55)" />
|
||||||
|
<stop offset="100%" style="stop-color: rgba(212, 175, 55, 0.2)" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<circle
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="90"
|
||||||
|
fill="none"
|
||||||
|
stroke="url(#mfgb-gold-stroke)"
|
||||||
|
stroke-width="0.6"
|
||||||
|
opacity="0.35"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="78"
|
||||||
|
fill="none"
|
||||||
|
stroke="rgba(212, 175, 55, 0.12)"
|
||||||
|
stroke-width="0.4"
|
||||||
|
stroke-dasharray="4 6"
|
||||||
|
opacity="0.5"
|
||||||
|
/>
|
||||||
|
<g
|
||||||
|
v-for="(trig, ti) in baguaTrigrams"
|
||||||
|
:key="'bg-' + ti"
|
||||||
|
:transform="'rotate(' + (ti * 45 - 90) + ') translate(0 -72)'"
|
||||||
|
>
|
||||||
|
<g v-for="(solid, li) in trig" :key="'ln-' + ti + '-' + li">
|
||||||
|
<line
|
||||||
|
v-if="solid"
|
||||||
|
x1="-11"
|
||||||
|
:y1="8 - li * 8"
|
||||||
|
x2="11"
|
||||||
|
:y2="8 - li * 8"
|
||||||
|
stroke="rgba(212, 175, 55, 0.42)"
|
||||||
|
stroke-width="2.4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<g v-else>
|
||||||
|
<line
|
||||||
|
x1="-11"
|
||||||
|
:y1="8 - li * 8"
|
||||||
|
x2="-3.5"
|
||||||
|
:y2="8 - li * 8"
|
||||||
|
stroke="rgba(212, 175, 55, 0.42)"
|
||||||
|
stroke-width="2.4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="3.5"
|
||||||
|
:y1="8 - li * 8"
|
||||||
|
x2="11"
|
||||||
|
:y2="8 - li * 8"
|
||||||
|
stroke="rgba(212, 175, 55, 0.42)"
|
||||||
|
stroke-width="2.4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<!-- 太极简形:阴阳鱼 -->
|
||||||
|
<circle cx="0" cy="0" r="20" fill="none" stroke="rgba(212, 175, 55, 0.28)" stroke-width="0.8" />
|
||||||
|
<path
|
||||||
|
d="M0-20 A20 20 0 0 1 0 20 A10 10 0 0 1 0 0 A10 10 0 0 0 0-20 Z"
|
||||||
|
fill="rgba(212, 175, 55, 0.08)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M0 20 A20 20 0 0 1 0-20 A10 10 0 0 1 0 0 A10 10 0 0 0 0 20 Z"
|
||||||
|
fill="rgba(15, 23, 42, 0.45)"
|
||||||
|
/>
|
||||||
|
<circle cx="0" cy="-10" r="3.2" fill="rgba(15, 23, 42, 0.75)" />
|
||||||
|
<circle cx="0" cy="10" r="3.2" fill="rgba(212, 175, 55, 0.4)" />
|
||||||
|
</svg>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 四角卷云纹 -->
|
||||||
|
<view
|
||||||
|
v-for="corner in cornerKeys"
|
||||||
|
:key="corner"
|
||||||
|
class="mfgb__corner"
|
||||||
|
:class="'mfgb__corner--' + corner"
|
||||||
|
>
|
||||||
|
<svg class="mfgb__corner-svg" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
class="mfgb__corner-path"
|
||||||
|
d="M6 58 C6 14 14 6 58 6 M10 54 C10 22 22 10 54 10 M16 48 C18 28 28 18 48 16"
|
||||||
|
fill="none"
|
||||||
|
stroke="rgba(212, 175, 55, 0.22)"
|
||||||
|
stroke-width="1.2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
class="mfgb__corner-path"
|
||||||
|
d="M52 12 Q40 12 36 20 Q32 28 38 36"
|
||||||
|
fill="none"
|
||||||
|
stroke="rgba(212, 175, 55, 0.16)"
|
||||||
|
stroke-width="0.9"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
class="mfgb__sigil mfgb__sigil--outer"
|
||||||
|
:class="sigilModifier"
|
||||||
|
/>
|
||||||
|
<view
|
||||||
|
class="mfgb__sigil mfgb__sigil--inner"
|
||||||
|
:class="sigilModifier"
|
||||||
|
/>
|
||||||
|
<view v-for="s in stars" :key="s.id" class="mfgb__star" :style="getStarStyle(s)" />
|
||||||
|
<view class="mfgb__glow" />
|
||||||
|
<view class="mfgb__mist" />
|
||||||
|
<view class="mfgb__vignette" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
export type MysticRoundPhase = 'ready' | 'loading' | 'result';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
/** 测名页:测算中加快阵法转速;登录页可省略(默认 ready) */
|
||||||
|
roundPhase?: MysticRoundPhase;
|
||||||
|
}>(),
|
||||||
|
{ roundPhase: 'ready' },
|
||||||
|
);
|
||||||
|
|
||||||
|
const sigilModifier = computed(() => {
|
||||||
|
if (props.roundPhase === 'loading') return 'mfgb__sigil--phase-loading';
|
||||||
|
if (props.roundPhase === 'result') return 'mfgb__sigil--phase-result';
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 先天八卦卦序(自下而上爻):乾 兑 离 震 巽 坎 艮 坤 */
|
||||||
|
const baguaTrigrams: boolean[][] = [
|
||||||
|
[true, true, true],
|
||||||
|
[true, true, false],
|
||||||
|
[true, false, true],
|
||||||
|
[true, false, false],
|
||||||
|
[false, true, true],
|
||||||
|
[false, true, false],
|
||||||
|
[false, false, true],
|
||||||
|
[false, false, false],
|
||||||
|
];
|
||||||
|
|
||||||
|
const cornerKeys = ['tl', 'tr', 'bl', 'br'] as const;
|
||||||
|
|
||||||
|
const stars = Array.from({ length: 36 }, (_, i) => ({
|
||||||
|
id: `mfgb-st-${i}`,
|
||||||
|
top: Math.random() * 100,
|
||||||
|
left: Math.random() * 100,
|
||||||
|
size: Math.random() * 2.2 + 0.8,
|
||||||
|
opacity: Math.random() * 0.35 + 0.12,
|
||||||
|
duration: Math.random() * 4 + 3,
|
||||||
|
delay: Math.random() * 4,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const clouds = Array.from({ length: 7 }, (_, i) => ({
|
||||||
|
id: `mfgb-cl-${i}`,
|
||||||
|
top: Math.random() * 70 - 10,
|
||||||
|
left: Math.random() * 90 - 5,
|
||||||
|
w: 180 + Math.random() * 220,
|
||||||
|
h: 60 + Math.random() * 80,
|
||||||
|
opacity: 0.08 + Math.random() * 0.12,
|
||||||
|
duration: 28 + Math.random() * 18,
|
||||||
|
delay: Math.random() * -40,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getStarStyle = (s: (typeof stars)[number]) => ({
|
||||||
|
top: `${s.top}%`,
|
||||||
|
left: `${s.left}%`,
|
||||||
|
width: `${s.size}px`,
|
||||||
|
height: `${s.size}px`,
|
||||||
|
opacity: s.opacity,
|
||||||
|
animationDuration: `${s.duration}s`,
|
||||||
|
animationDelay: `${s.delay}s`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCloudStyle = (c: (typeof clouds)[number]) => ({
|
||||||
|
top: `${c.top}%`,
|
||||||
|
left: `${c.left}%`,
|
||||||
|
width: `${c.w}px`,
|
||||||
|
height: `${c.h}px`,
|
||||||
|
opacity: c.opacity,
|
||||||
|
animationDuration: `${c.duration}s`,
|
||||||
|
animationDelay: `${c.delay}s`,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mfgb {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__base {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 120% 80% at 50% -20%, rgba(212, 175, 55, 0.14), transparent 52%),
|
||||||
|
radial-gradient(ellipse 90% 60% at 80% 20%, rgba(88, 28, 135, 0.18), transparent 45%),
|
||||||
|
radial-gradient(ellipse 70% 50% at 10% 30%, rgba(127, 29, 29, 0.12), transparent 50%),
|
||||||
|
linear-gradient(165deg, #070a12 0%, #12102a 38%, #0a1628 72%, #05080f 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 万字锦地暗纹 */
|
||||||
|
.mfgb__wanzi {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0.04;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 0 48 48'%3E%3Cpath fill='none' stroke='%23d4af37' stroke-width='0.6' d='M24 4v40M4 24h40M12 12l24 24M36 12L12 36'/%3E%3C/svg%3E");
|
||||||
|
background-size: 48px 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__ink {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: -5%;
|
||||||
|
height: 42%;
|
||||||
|
opacity: 0.35;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 100% 55% at 20% 100%, rgba(15, 23, 42, 0.95), transparent 70%),
|
||||||
|
radial-gradient(ellipse 90% 50% at 55% 100%, rgba(30, 41, 59, 0.85), transparent 68%),
|
||||||
|
radial-gradient(ellipse 80% 45% at 85% 100%, rgba(15, 23, 42, 0.9), transparent 65%);
|
||||||
|
filter: blur(1px);
|
||||||
|
mask-image: linear-gradient(to top, black 0%, black 55%, transparent 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__cloud {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(ellipse at center, rgba(212, 175, 55, 0.2) 0%, rgba(212, 175, 55, 0.06) 45%, transparent 70%);
|
||||||
|
filter: blur(18px);
|
||||||
|
animation: mfgb-cloud-drift linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mfgb-cloud-drift {
|
||||||
|
0% {
|
||||||
|
transform: translate(0, 0) scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate(-2%, 1.5%) scale(1.03);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(0, 0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 八卦层:置于祥云之下、阵环之上,略透明 */
|
||||||
|
.mfgb__bagua-wrap {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 52%;
|
||||||
|
width: min(92vw, 680px);
|
||||||
|
height: min(92vw, 680px);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
opacity: 0.38;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__bagua-svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__bagua-spin {
|
||||||
|
animation: mfgb-bagua-spin 200s linear infinite;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mfgb-bagua-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__corner {
|
||||||
|
position: absolute;
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
z-index: 1;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__corner-svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__corner--tl {
|
||||||
|
top: calc(12px + env(safe-area-inset-top, 0px));
|
||||||
|
left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__corner--tr {
|
||||||
|
top: calc(12px + env(safe-area-inset-top, 0px));
|
||||||
|
right: 12px;
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__corner--bl {
|
||||||
|
bottom: calc(12px + env(safe-area-inset-bottom, 0px));
|
||||||
|
left: 12px;
|
||||||
|
transform: scaleY(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__corner--br {
|
||||||
|
bottom: calc(12px + env(safe-area-inset-bottom, 0px));
|
||||||
|
right: 12px;
|
||||||
|
transform: scale(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__sigil {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 54%;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border: 1px dashed rgba(212, 175, 55, 0.28);
|
||||||
|
box-shadow:
|
||||||
|
0 0 40px rgba(212, 175, 55, 0.08),
|
||||||
|
inset 0 0 30px rgba(212, 175, 55, 0.04);
|
||||||
|
opacity: 0.65;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__sigil--outer {
|
||||||
|
width: min(72vw, 560px);
|
||||||
|
height: min(72vw, 560px);
|
||||||
|
animation: mfgb-sigil-cw 48s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__sigil--inner {
|
||||||
|
width: min(52vw, 400px);
|
||||||
|
height: min(52vw, 400px);
|
||||||
|
border-style: dotted;
|
||||||
|
border-color: rgba(212, 175, 55, 0.2);
|
||||||
|
animation: mfgb-sigil-ccw 32s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__sigil--outer.mfgb__sigil--phase-loading {
|
||||||
|
animation-duration: 2.8s;
|
||||||
|
border-color: rgba(212, 175, 55, 0.55);
|
||||||
|
opacity: 0.85;
|
||||||
|
box-shadow:
|
||||||
|
0 0 56px rgba(212, 175, 55, 0.15),
|
||||||
|
inset 0 0 36px rgba(212, 175, 55, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__sigil--inner.mfgb__sigil--phase-loading {
|
||||||
|
animation-duration: 2s;
|
||||||
|
border-color: rgba(212, 175, 55, 0.42);
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__sigil--outer.mfgb__sigil--phase-result {
|
||||||
|
animation-duration: 5.5s;
|
||||||
|
border-color: rgba(212, 175, 55, 0.35);
|
||||||
|
opacity: 0.62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__sigil--inner.mfgb__sigil--phase-result {
|
||||||
|
animation-duration: 4s;
|
||||||
|
border-color: rgba(212, 175, 55, 0.22);
|
||||||
|
opacity: 0.58;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mfgb-sigil-cw {
|
||||||
|
from {
|
||||||
|
transform: translate(-50%, -50%) rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate(-50%, -50%) rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mfgb-sigil-ccw {
|
||||||
|
from {
|
||||||
|
transform: translate(-50%, -50%) rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate(-50%, -50%) rotate(-360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__star {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 248, 220, 0.95);
|
||||||
|
box-shadow: 0 0 12px rgba(212, 175, 55, 0.45), 0 0 2px rgba(255, 255, 255, 0.8);
|
||||||
|
animation: mfgb-twinkle 2.8s ease-in-out infinite;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mfgb-twinkle {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(0.9);
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__glow {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 48%;
|
||||||
|
width: min(90vw, 680px);
|
||||||
|
height: min(90vw, 680px);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: radial-gradient(circle at 50% 35%, rgba(212, 175, 55, 0.22), rgba(139, 92, 246, 0.06) 45%, transparent 65%);
|
||||||
|
filter: blur(36px);
|
||||||
|
opacity: 0.9;
|
||||||
|
animation: mfgb-glow-pulse 4.2s ease-in-out infinite;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mfgb-glow-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -50%) scale(0.94);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate(-50%, -50%) scale(1.06);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__mist {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 38%;
|
||||||
|
background: linear-gradient(to top, rgba(2, 6, 23, 0.75), rgba(2, 6, 23, 0));
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfgb__vignette {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(ellipse 75% 65% at 50% 45%, transparent 30%, rgba(0, 0, 0, 0.45) 100%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
317
前端源码/uni-app/components/MysticLoading.vue
Normal file
317
前端源码/uni-app/components/MysticLoading.vue
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="mystic-loading">
|
||||||
|
<div v-if="isOpen" class="mystic-loading-overlay">
|
||||||
|
<!-- 背景 -->
|
||||||
|
<div class="mystic-loading-bg"></div>
|
||||||
|
|
||||||
|
<!-- 主要内容 -->
|
||||||
|
<div class="mystic-loading-content">
|
||||||
|
<!-- 八卦罗盘 -->
|
||||||
|
<div class="mystic-compass">
|
||||||
|
<!-- 外圈 - 八卦符号 -->
|
||||||
|
<div class="mystic-compass-outer">
|
||||||
|
<svg class="mystic-compass-bagua" viewBox="0 0 200 200">
|
||||||
|
<!-- 八卦符号 -->
|
||||||
|
<g class="bagua-symbol" v-for="(gua, i) in bagua" :key="i"
|
||||||
|
:transform="`rotate(${i * 45} 100 100)`">
|
||||||
|
<text x="100" y="30" text-anchor="middle" class="bagua-text">{{ gua }}</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 中圈 - 天干地支 -->
|
||||||
|
<div class="mystic-compass-middle">
|
||||||
|
<svg class="mystic-compass-tiangan" viewBox="0 0 200 200">
|
||||||
|
<g class="tiangan-symbol" v-for="(tg, i) in tiangan" :key="i"
|
||||||
|
:transform="`rotate(${i * 36} 100 100)`">
|
||||||
|
<text x="100" y="50" text-anchor="middle" class="tiangan-text">{{ tg }}</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内圈 - 太极图 -->
|
||||||
|
<div class="mystic-compass-inner">
|
||||||
|
<svg class="mystic-taiji" viewBox="0 0 100 100">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="yin">
|
||||||
|
<path d="M 50 0 A 50 50 0 0 1 50 100 A 25 25 0 0 1 50 50 A 25 25 0 0 0 50 0 Z" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="yang">
|
||||||
|
<path d="M 50 0 A 50 50 0 0 0 50 100 A 25 25 0 0 0 50 50 A 25 25 0 0 1 50 0 Z" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<!-- 阴 -->
|
||||||
|
<circle cx="50" cy="50" r="50" fill="#2c2c2c" clip-path="url(#yin)" />
|
||||||
|
<circle cx="50" cy="75" r="5" fill="#fdfbf7" />
|
||||||
|
<!-- 阳 -->
|
||||||
|
<circle cx="50" cy="50" r="50" fill="#fdfbf7" clip-path="url(#yang)" />
|
||||||
|
<circle cx="50" cy="25" r="5" fill="#2c2c2c" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载文字 -->
|
||||||
|
<div class="mystic-loading-text">
|
||||||
|
<div class="mystic-loading-title">{{ title }}</div>
|
||||||
|
<div class="mystic-loading-subtitle">{{ subtitle }}</div>
|
||||||
|
<div class="mystic-loading-tip">分析时间预计1-2分钟,可点击返回按钮退出,并在我的方案中查看结果</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 进度提示 -->
|
||||||
|
<div class="mystic-loading-dots">
|
||||||
|
<span class="dot"></span>
|
||||||
|
<span class="dot"></span>
|
||||||
|
<span class="dot"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
title: '正在推演命盘',
|
||||||
|
subtitle: '易经数理 · 五行生克 · 三才五格'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 八卦符号
|
||||||
|
const bagua = ['☰', '☱', '☲', '☳', '☴', '☵', '☶', '☷'];
|
||||||
|
|
||||||
|
// 天干
|
||||||
|
const tiangan = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mystic-loading-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-loading-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(circle at center, rgba(139, 35, 35, 0.1) 0%, transparent 70%);
|
||||||
|
animation: pulse 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-loading-content {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 罗盘容器 */
|
||||||
|
.mystic-compass {
|
||||||
|
position: relative;
|
||||||
|
width: 280px;
|
||||||
|
height: 280px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 外圈 - 八卦 */
|
||||||
|
.mystic-compass-outer {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
animation: rotate-clockwise 20s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-compass-bagua {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
filter: drop-shadow(0 0 10px rgba(139, 35, 35, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bagua-text {
|
||||||
|
font-size: 24px;
|
||||||
|
fill: #8b2323;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 中圈 - 天干 */
|
||||||
|
.mystic-compass-middle {
|
||||||
|
position: absolute;
|
||||||
|
width: 70%;
|
||||||
|
height: 70%;
|
||||||
|
animation: rotate-counter-clockwise 15s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-compass-tiangan {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
filter: drop-shadow(0 0 8px rgba(218, 165, 32, 0.4));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiangan-text {
|
||||||
|
font-size: 18px;
|
||||||
|
fill: #daa520;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内圈 - 太极 */
|
||||||
|
.mystic-compass-inner {
|
||||||
|
position: absolute;
|
||||||
|
width: 35%;
|
||||||
|
height: 35%;
|
||||||
|
animation: rotate-clockwise 10s linear infinite;
|
||||||
|
filter: drop-shadow(0 0 15px rgba(255, 255, 255, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-taiji {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate-clockwise {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate-counter-clockwise {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(-360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载文字 */
|
||||||
|
.mystic-loading-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-loading-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fdfbf7;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
letter-spacing: 0.3em;
|
||||||
|
text-shadow: 0 0 20px rgba(139, 35, 35, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-loading-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-loading-tip {
|
||||||
|
margin-top: 10px;
|
||||||
|
max-width: min(520px, 86vw);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: rgba(226, 226, 226, 0.72);
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载点 */
|
||||||
|
.mystic-loading-dots {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #8b2323;
|
||||||
|
animation: dot-pulse 1.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot:nth-child(1) {
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dot-pulse {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
80%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
40% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 过渡动画 */
|
||||||
|
.mystic-loading-enter-active,
|
||||||
|
.mystic-loading-leave-active {
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-loading-enter-from,
|
||||||
|
.mystic-loading-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-loading-enter-active .mystic-compass {
|
||||||
|
animation: compass-enter 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes compass-enter {
|
||||||
|
from {
|
||||||
|
transform: scale(0) rotate(-180deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: scale(1) rotate(0deg);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
270
前端源码/uni-app/components/MysticSelect.vue
Normal file
270
前端源码/uni-app/components/MysticSelect.vue
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="mystic-select">
|
||||||
|
<view v-if="isOpen" class="mystic-select-overlay">
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<view class="mystic-select-backdrop" @click="handleClose"></view>
|
||||||
|
|
||||||
|
<!-- Modal Content -->
|
||||||
|
<view class="mystic-select-modal">
|
||||||
|
<!-- Header -->
|
||||||
|
<view class="mystic-select-header">
|
||||||
|
<view class="mystic-select-close" @click="handleClose">
|
||||||
|
<CloseIcon class="mystic-select-icon" />
|
||||||
|
</view>
|
||||||
|
<text class="mystic-select-title">{{ title }}</text>
|
||||||
|
<view class="mystic-select-confirm" @click="handleConfirm">
|
||||||
|
<CheckIcon class="mystic-select-icon" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- H5 友好:用可滚动列表保证能下滑、能点击选中 -->
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
class="mystic-select-view"
|
||||||
|
:scroll-into-view="`opt-${selectedIndex}`"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
v-for="(item, idx) in options"
|
||||||
|
:key="item.value"
|
||||||
|
:id="`opt-${idx}`"
|
||||||
|
class="mystic-select-item"
|
||||||
|
:class="{ 'mystic-select-item-active': idx === selectedIndex }"
|
||||||
|
@click="selectOption(idx)"
|
||||||
|
>
|
||||||
|
<view class="mystic-select-item-main">
|
||||||
|
<text class="mystic-select-item-label">{{ item.label }}</text>
|
||||||
|
<text v-if="item.desc" class="mystic-select-item-desc">{{ item.desc }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="idx === selectedIndex" class="mystic-select-check">
|
||||||
|
<CheckIcon class="mystic-select-check-icon" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- Footer Tip -->
|
||||||
|
<view v-if="tip" class="mystic-select-footer">
|
||||||
|
<text class="mystic-select-tip">{{ tip }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import CloseIcon from './icons/CloseIcon.vue';
|
||||||
|
import CheckIcon from './icons/CheckIcon.vue';
|
||||||
|
|
||||||
|
export interface SelectOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
desc?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
title?: string;
|
||||||
|
tip?: string;
|
||||||
|
options: SelectOption[];
|
||||||
|
defaultValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: '请选择',
|
||||||
|
tip: '',
|
||||||
|
defaultValue: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
confirm: [option: SelectOption];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selectedIndex = ref(0);
|
||||||
|
|
||||||
|
// 当打开时设置默认值
|
||||||
|
watch(() => props.isOpen, (newVal: boolean) => {
|
||||||
|
if (newVal) {
|
||||||
|
// 延迟设置,确保列表渲染完成
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = props.options.findIndex((o: SelectOption) => o.value === props.defaultValue);
|
||||||
|
selectedIndex.value = idx >= 0 ? idx : 0;
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
const selectedOption = props.options[selectedIndex.value];
|
||||||
|
emit('confirm', selectedOption);
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectOption = (idx: number) => {
|
||||||
|
selectedIndex.value = idx;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mystic-select-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(26, 26, 26, 0.6);
|
||||||
|
backdrop-filter: blur(8rpx);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-modal {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
width: 100%;
|
||||||
|
background: #fcfaf5;
|
||||||
|
border-top-left-radius: 32rpx;
|
||||||
|
border-top-right-radius: 32rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 -16rpx 64rpx rgba(0, 0, 0, 0.25);
|
||||||
|
border-top: 8rpx solid #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 32rpx 40rpx;
|
||||||
|
border-bottom: 2rpx solid #eaddcf;
|
||||||
|
background: #f9f7f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-close,
|
||||||
|
.mystic-select-confirm {
|
||||||
|
padding: 16rpx;
|
||||||
|
color: #5a5a5a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-close:active,
|
||||||
|
.mystic-select-confirm:active {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-confirm {
|
||||||
|
color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-icon {
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c2c2c;
|
||||||
|
letter-spacing: 0.3em;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-view {
|
||||||
|
height: 480rpx;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-item {
|
||||||
|
height: 100rpx;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
border-bottom: 1rpx solid rgba(234, 221, 207, 0.7);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-item-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4rpx;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-item-active {
|
||||||
|
background: rgba(139, 35, 35, 0.06);
|
||||||
|
border-bottom-color: rgba(139, 35, 35, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-item-label {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-item-desc {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #8a8a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-check {
|
||||||
|
width: 56rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-check-icon {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-footer {
|
||||||
|
background: #f9f7f2;
|
||||||
|
padding: 24rpx;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 2rpx solid #eaddcf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-tip {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #8a8a8a;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transition */
|
||||||
|
.mystic-select-enter-active,
|
||||||
|
.mystic-select-leave-active {
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-enter-active .mystic-select-modal,
|
||||||
|
.mystic-select-leave-active .mystic-select-modal {
|
||||||
|
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-enter-from,
|
||||||
|
.mystic-select-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystic-select-enter-from .mystic-select-modal,
|
||||||
|
.mystic-select-leave-to .mystic-select-modal {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
270
前端源码/uni-app/components/NamingPayModal.vue
Normal file
270
前端源码/uni-app/components/NamingPayModal.vue
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
<template>
|
||||||
|
<view v-if="visible" class="pay-modal-overlay" @click="handleClose">
|
||||||
|
<view class="pay-modal" @click.stop>
|
||||||
|
<view class="pay-modal-header">
|
||||||
|
<text class="pay-modal-title">解锁完整报告</text>
|
||||||
|
<view class="pay-modal-close" @click="handleClose">✕</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="pay-modal-content">
|
||||||
|
<view class="pay-modal-preview">
|
||||||
|
<view class="pay-modal-name">{{ data?.name || '吉名' }}</view>
|
||||||
|
<view class="pay-modal-pinyin">{{ data?.pinyin || '' }}</view>
|
||||||
|
<view class="pay-modal-lock-icon">🔒</view>
|
||||||
|
<text class="pay-modal-lock-text">详细解析已加密</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="pay-modal-features">
|
||||||
|
<view class="pay-modal-feature">
|
||||||
|
<text class="pay-modal-feature-icon">✦</text>
|
||||||
|
<text class="pay-modal-feature-text">字义与生肖解析</text>
|
||||||
|
</view>
|
||||||
|
<view class="pay-modal-feature">
|
||||||
|
<text class="pay-modal-feature-icon">✦</text>
|
||||||
|
<text class="pay-modal-feature-text">三才五格数理分析</text>
|
||||||
|
</view>
|
||||||
|
<view class="pay-modal-feature">
|
||||||
|
<text class="pay-modal-feature-icon">✦</text>
|
||||||
|
<text class="pay-modal-feature-text">六维格局与周易卦象</text>
|
||||||
|
</view>
|
||||||
|
<view class="pay-modal-feature">
|
||||||
|
<text class="pay-modal-feature-icon">✦</text>
|
||||||
|
<text class="pay-modal-feature-text">开运锦囊与人生运程</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="pay-modal-price">
|
||||||
|
<text class="pay-modal-price-label">限时特惠</text>
|
||||||
|
<text class="pay-modal-price-value">¥9.9</text>
|
||||||
|
<text class="pay-modal-price-original">¥29.9</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="pay-modal-actions">
|
||||||
|
<button class="pay-modal-btn pay-modal-btn-share" open-type="share" @click="handleShare">
|
||||||
|
<text class="pay-modal-btn-text">分享解锁</text>
|
||||||
|
</button>
|
||||||
|
<button class="pay-modal-btn pay-modal-btn-pay" @click="handlePay">
|
||||||
|
<text class="pay-modal-btn-text">立即解锁</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="pay-modal-tip">
|
||||||
|
<text>分享给好友即可免费解锁完整报告</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { GeneratedName } from "./NamingResult.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
data: GeneratedName | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
pay: [data: GeneratedName];
|
||||||
|
share: [];
|
||||||
|
unlock: [data: GeneratedName];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit("close");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePay = () => {
|
||||||
|
// 支付成功后直接跳转到详情页
|
||||||
|
if (props.data) {
|
||||||
|
emit("pay", props.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShare = () => {
|
||||||
|
emit("share");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pay-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal {
|
||||||
|
width: 85%;
|
||||||
|
max-width: 600rpx;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
border: 1px solid rgba(212, 175, 55, 0.3);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 32rpx;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #d4af37;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-close {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #a0a0a0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-content {
|
||||||
|
padding: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-preview {
|
||||||
|
text-align: center;
|
||||||
|
padding: 32rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-name {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e2e2e2;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-pinyin {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #a0a0a0;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-lock-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-lock-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5a5a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-features {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-feature {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-feature-icon {
|
||||||
|
color: #d4af37;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-feature-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-price {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24rpx;
|
||||||
|
background: rgba(212, 175, 55, 0.1);
|
||||||
|
border-radius: 12rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-price-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #d4af37;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-price-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-price-original {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #5a5a5a;
|
||||||
|
text-decoration: line-through;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 0 32rpx 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-btn {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-btn-share {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: #e2e2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-btn-pay {
|
||||||
|
background: linear-gradient(135deg, #d4af37 0%, #8a6e1e 100%);
|
||||||
|
color: #1a1a2e;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-btn-icon {
|
||||||
|
margin-right: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-btn-text {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-tip {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 32rpx 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-modal-tip text {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #5a5a5a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
292
前端源码/uni-app/components/NamingResult.vue
Normal file
292
前端源码/uni-app/components/NamingResult.vue
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
<template>
|
||||||
|
<view class="naming-result">
|
||||||
|
<view class="naming-result-header">
|
||||||
|
<text class="naming-result-title">甄选吉名</text>
|
||||||
|
<view class="naming-result-reset" @click="$emit('reset')">
|
||||||
|
<text>重测</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="naming-result-list">
|
||||||
|
<view v-for="item in results" :key="item.id" class="naming-result-item">
|
||||||
|
<view class="naming-result-item-bg">✦</view>
|
||||||
|
<view class="naming-result-item-actions">
|
||||||
|
<view class="naming-result-action-btn" @click.stop="checkDup(item.id)">查重</view>
|
||||||
|
<view
|
||||||
|
class="naming-result-action-btn naming-result-action-btn-heart"
|
||||||
|
:class="item.isFavorite ? 'naming-result-action-btn-heart-active' : ''"
|
||||||
|
@click.stop="toggleFav(item.id)"
|
||||||
|
>❤</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="naming-result-item-content" @click="handleItemClick(item)">
|
||||||
|
<text class="naming-result-item-name">{{ item.name }}</text>
|
||||||
|
<text class="naming-result-item-pinyin">{{ item.pinyin }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="naming-result-item-tags" @click="handleItemClick(item)">
|
||||||
|
<text v-for="(t, i) in item.tags" :key="i" class="naming-result-tag">{{ t }}</text>
|
||||||
|
<text v-if="item.duplicateRate" class="naming-result-tag naming-result-tag-gold">
|
||||||
|
重名率: {{ item.duplicateRate }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="naming-result-item-divider"></view>
|
||||||
|
|
||||||
|
<view class="naming-result-item-details" @click="handleItemClick(item)">
|
||||||
|
<view class="naming-result-detail-row">
|
||||||
|
<text class="naming-result-detail-label naming-result-detail-label-primary">寓意</text>
|
||||||
|
<text class="naming-result-detail-text">{{ item.meaning }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="naming-result-detail-row">
|
||||||
|
<text class="naming-result-detail-label">出处</text>
|
||||||
|
<text class="naming-result-detail-text naming-result-detail-text-small">{{ item.source }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
import { userApi } from "@/api";
|
||||||
|
|
||||||
|
declare const uni: any;
|
||||||
|
|
||||||
|
export interface GeneratedName {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
pinyin: string;
|
||||||
|
meaning: string;
|
||||||
|
source: string;
|
||||||
|
tags: string[];
|
||||||
|
isFavorite?: boolean;
|
||||||
|
duplicateRate?: string;
|
||||||
|
score?: number;
|
||||||
|
zodiac?: string;
|
||||||
|
wuxing?: string;
|
||||||
|
constellation?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
data: GeneratedName[];
|
||||||
|
category?: 'personal' | 'company';
|
||||||
|
payBusinessId?: number;
|
||||||
|
payAmount?: number;
|
||||||
|
}>(), {
|
||||||
|
category: 'company',
|
||||||
|
payAmount: 9.9,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
reset: [];
|
||||||
|
showDetail: [data: GeneratedName];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const results = ref<GeneratedName[]>([]);
|
||||||
|
|
||||||
|
watch(() => props.data, (newData: GeneratedName[]) => {
|
||||||
|
if (newData) {
|
||||||
|
results.value = newData.map((item: GeneratedName) => ({ ...item }));
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
const toggleFav = async (id: string) => {
|
||||||
|
const idx = results.value.findIndex((r: GeneratedName) => r.id === id);
|
||||||
|
if (idx < 0) return;
|
||||||
|
|
||||||
|
const solutionId = Number(id);
|
||||||
|
const prev = !!results.value[idx].isFavorite;
|
||||||
|
|
||||||
|
if (!prev) {
|
||||||
|
const res = await userApi.favoriteSolution({ solution_id: solutionId, category: props.category });
|
||||||
|
results.value[idx].isFavorite = true;
|
||||||
|
uni.showToast({ title: res?.msg || '收藏成功', icon: 'success' });
|
||||||
|
} else {
|
||||||
|
const res = await userApi.unfavoriteSolution({ solution_id: solutionId });
|
||||||
|
results.value[idx].isFavorite = false;
|
||||||
|
uni.showToast({ title: res?.msg || '已取消收藏', icon: 'success' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkDup = (id: string) => {
|
||||||
|
const rates = ["低 (<100人)", "中 (~1000人)", "高 (>10000人)"];
|
||||||
|
const rate = rates[Math.floor(Math.random() * rates.length)];
|
||||||
|
results.value = results.value.map((r: GeneratedName) =>
|
||||||
|
r.id === id ? { ...r, duplicateRate: rate } : r
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleItemClick = (item: GeneratedName) => {
|
||||||
|
emit("showDetail", item);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.naming-result {
|
||||||
|
padding-bottom: 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-reset {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5a5a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-item {
|
||||||
|
background-color: #f9f7f2;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
border: 1px solid #dcd3c9;
|
||||||
|
padding: 28rpx 24rpx 24rpx;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-item-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 8rpx;
|
||||||
|
opacity: 0.08;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-item-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 12rpx;
|
||||||
|
right: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-action-btn {
|
||||||
|
border-radius: 999rpx;
|
||||||
|
border: 1px solid #dcd3c9;
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5a5a5a;
|
||||||
|
padding:7rpx 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-action-btn-heart {
|
||||||
|
padding: 6rpx;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-action-btn-heart-active {
|
||||||
|
color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-item-content {
|
||||||
|
padding-right: 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-item-name {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
margin-bottom: 4rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-item-pinyin {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5a5a5a;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-item-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 4rpx 8rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
border: 1px solid rgba(139, 35, 35, 0.2);
|
||||||
|
color: #8b2323;
|
||||||
|
background: rgba(139, 35, 35, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-tag-gold {
|
||||||
|
border-color: #d4af37;
|
||||||
|
color: #d4af37;
|
||||||
|
background: rgba(212, 175, 55, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-item-divider {
|
||||||
|
height: 1px;
|
||||||
|
margin: 12rpx 0;
|
||||||
|
background-color: #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-item-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-detail-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-detail-label {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 4rpx 8rpx;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
border: 1px solid #5a5a5a;
|
||||||
|
color: #5a5a5a;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-detail-label-primary {
|
||||||
|
border-color: #8b2323;
|
||||||
|
color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-detail-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naming-result-detail-text-small {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5a5a5a;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
313
前端源码/uni-app/components/PaymentModal.vue
Normal file
313
前端源码/uni-app/components/PaymentModal.vue
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
<template>
|
||||||
|
<view v-if="visible" class="payment-modal" @click="handleClose">
|
||||||
|
<view class="payment-modal-content" @click.stop>
|
||||||
|
<!-- 关闭按钮 -->
|
||||||
|
<view class="payment-close-btn" @click="handleClose">
|
||||||
|
<text class="payment-close-icon">×</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 商品信息 -->
|
||||||
|
<view class="payment-product">
|
||||||
|
<view class="payment-product-icon">{{ productIcon }}</view>
|
||||||
|
<text class="payment-product-name">{{ productName }}</text>
|
||||||
|
<text class="payment-product-desc">{{ productDesc }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 价格 -->
|
||||||
|
<view class="payment-price">
|
||||||
|
<text class="payment-price-symbol">¥</text>
|
||||||
|
<text class="payment-price-amount">{{ amount }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 支付方式 -->
|
||||||
|
<view class="payment-method">
|
||||||
|
<view class="payment-method-item payment-method-active">
|
||||||
|
<view class="payment-method-left">
|
||||||
|
<text class="payment-method-icon">💳</text>
|
||||||
|
<text class="payment-method-label">微信支付</text>
|
||||||
|
</view>
|
||||||
|
<view class="payment-method-check">✓</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 支付按钮 -->
|
||||||
|
<view class="payment-submit-btn" :class="{ 'payment-submit-disabled': paying }" @click="handlePay">
|
||||||
|
<text class="payment-submit-text">{{ paying ? '支付中...' : '立即支付' }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 提示 -->
|
||||||
|
<view class="payment-tips">
|
||||||
|
<text class="payment-tips-text">支付即代表同意《服务协议》和《隐私政策》</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { wxPay } from "@/utils/payment";
|
||||||
|
import { isWechatBrowser, payWithWechatJsapiH5 } from "@/utils/wechat-h5-jsapi-pay";
|
||||||
|
|
||||||
|
declare const uni: any;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean;
|
||||||
|
productName: string; // 商品名称
|
||||||
|
productDesc?: string; // 商品描述
|
||||||
|
productIcon?: string; // 商品图标
|
||||||
|
amount: number; // 支付金额
|
||||||
|
businessType: string; // 业务类型
|
||||||
|
businessId: number; // 业务ID
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
productDesc: '',
|
||||||
|
productIcon: '📦'
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
success: [outTradeNo: string];
|
||||||
|
fail: [msg: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const paying = ref(false);
|
||||||
|
|
||||||
|
// 处理支付:微信 H5 内与财运月度详批一致走 JSAPI;其它环境走 uni 支付封装
|
||||||
|
const handlePay = async () => {
|
||||||
|
if (paying.value) return;
|
||||||
|
|
||||||
|
paying.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof window !== 'undefined' && isWechatBrowser()) {
|
||||||
|
const r = await payWithWechatJsapiH5({
|
||||||
|
description: props.productDesc || props.productName,
|
||||||
|
totalAmountYuan: props.amount,
|
||||||
|
businessType: props.businessType,
|
||||||
|
businessId: props.businessId,
|
||||||
|
});
|
||||||
|
if (r.redirected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (r.ok) {
|
||||||
|
emit('success', r.outTradeNo || '');
|
||||||
|
handleClose();
|
||||||
|
} else if (r.msg && r.msg !== 'not_wechat') {
|
||||||
|
emit('fail', r.msg || '支付失败');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await wxPay({
|
||||||
|
description: props.productName,
|
||||||
|
total_amount: props.amount,
|
||||||
|
business_type: props.businessType,
|
||||||
|
business_id: props.businessId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
uni.showToast({ title: '支付成功', icon: 'success' });
|
||||||
|
emit('success', result.outTradeNo || '');
|
||||||
|
handleClose();
|
||||||
|
} else {
|
||||||
|
emit('fail', (result as any).msg || '支付失败');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('支付失败:', error);
|
||||||
|
uni.showToast({ title: error.msg || '支付失败', icon: 'none' });
|
||||||
|
emit('fail', error.msg || '支付失败');
|
||||||
|
} finally {
|
||||||
|
paying.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
const handleClose = () => {
|
||||||
|
if (paying.value) {
|
||||||
|
uni.showToast({ title: '支付进行中,请稍候', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.payment-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-modal-content {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 32rpx 32rpx 0 0;
|
||||||
|
padding: 48rpx 32rpx;
|
||||||
|
padding-bottom: calc(48rpx + env(safe-area-inset-bottom, 0px));
|
||||||
|
position: relative;
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 24rpx;
|
||||||
|
right: 24rpx;
|
||||||
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-close-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 商品信息 */
|
||||||
|
.payment-product {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-product-icon {
|
||||||
|
font-size: 80rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-product-name {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-product-desc {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 价格 */
|
||||||
|
.payment-price {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-price-symbol {
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: #8b2323;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-price-amount {
|
||||||
|
font-size: 72rpx;
|
||||||
|
color: #8b2323;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 支付方式 */
|
||||||
|
.payment-method {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
border: 2rpx solid transparent;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-active {
|
||||||
|
background-color: rgba(139, 35, 35, 0.05);
|
||||||
|
border-color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-icon {
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-check {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #8b2323;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 支付按钮 */
|
||||||
|
.payment-submit-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 28rpx 0;
|
||||||
|
background-color: #8b2323;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-submit-btn:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-submit-disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-submit-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f2e6d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提示 */
|
||||||
|
.payment-tips {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-tips-text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
105
前端源码/uni-app/components/RadarChart.vue
Normal file
105
前端源码/uni-app/components/RadarChart.vue
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<view class="radar-chart">
|
||||||
|
<!-- 使用普通容器 + SVG 渲染,避免 getContext 报错 -->
|
||||||
|
<view ref="chartRef" class="radar-chart-inner"></view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
|
// 通过 index.html 全局引入 echarts,运行时从 window 上获取
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
declare const window: any;
|
||||||
|
|
||||||
|
const chartRef = ref<HTMLElement | null>(null);
|
||||||
|
let chart: any = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const el = chartRef.value;
|
||||||
|
if (!el || !window || !window.echarts) return;
|
||||||
|
|
||||||
|
const echarts = window.echarts;
|
||||||
|
// 使用 SVG 渲染器,避免对 canvas.getContext 的依赖
|
||||||
|
chart = echarts.init(el, undefined, { renderer: 'svg' });
|
||||||
|
|
||||||
|
const option: any = {
|
||||||
|
radar: {
|
||||||
|
indicator: [
|
||||||
|
{ name: '事业', max: 100 },
|
||||||
|
{ name: '财运', max: 100 },
|
||||||
|
{ name: '健康', max: 100 },
|
||||||
|
{ name: '家庭', max: 100 },
|
||||||
|
{ name: '社交', max: 100 },
|
||||||
|
{ name: '智慧', max: 100 }
|
||||||
|
],
|
||||||
|
splitNumber: 4,
|
||||||
|
shape: 'polygon',
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(255,255,255,0.12)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
splitArea: {
|
||||||
|
areaStyle: {
|
||||||
|
color: ['rgba(255,255,255,0.02)', 'rgba(255,255,255,0.01)']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(255,255,255,0.12)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisName: {
|
||||||
|
color: '#cfd2dc',
|
||||||
|
fontSize: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'radar',
|
||||||
|
areaStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: 'rgba(255,193,7,0.9)' },
|
||||||
|
{ offset: 1, color: 'rgba(255,87,34,0.7)' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(255,193,7,0.9)'
|
||||||
|
},
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 3,
|
||||||
|
itemStyle: {
|
||||||
|
color: '#ffc107'
|
||||||
|
},
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: [88, 92, 86, 80, 75, 90]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
chart.setOption(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (chart) {
|
||||||
|
chart.dispose();
|
||||||
|
chart = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.radar-chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 260rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radar-chart-inner {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
597
前端源码/uni-app/components/SharePosterModal.vue
Normal file
597
前端源码/uni-app/components/SharePosterModal.vue
Normal file
@@ -0,0 +1,597 @@
|
|||||||
|
<template>
|
||||||
|
<view class="share-poster-modal">
|
||||||
|
<view v-if="visible && !showPosterPreview" class="modal-overlay" @click="handleClose">
|
||||||
|
<view class="modal-box" @click.stop>
|
||||||
|
<!-- 关闭按钮 -->
|
||||||
|
<view class="close-btn" @click="handleClose">
|
||||||
|
<text class="close-icon">×</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 海报预览 -->
|
||||||
|
<view class="poster-preview">
|
||||||
|
<view class="poster-header">
|
||||||
|
<text class="poster-symbol">☯</text>
|
||||||
|
<text class="poster-title">易凡起名</text>
|
||||||
|
<text class="poster-subtitle">传承国学智慧 · 赋予美好寓意</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="poster-card">
|
||||||
|
<view class="service-list">
|
||||||
|
<view class="service-item" v-for="item in services" :key="item">
|
||||||
|
<view class="service-dot"></view>
|
||||||
|
<text class="service-text">{{ item }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="poster-divider"></view>
|
||||||
|
|
||||||
|
<view class="qr-area">
|
||||||
|
<view class="qr-box">
|
||||||
|
<image v-if="qrImageUrl" :src="qrImageUrl" class="qr-image" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
<text class="invite-text">邀请码:{{ userId }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<text class="poster-footer">— 邀您共探姓名玄机 —</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 按钮 -->
|
||||||
|
<view class="btn-row">
|
||||||
|
<button class="btn btn-share" open-type="share">
|
||||||
|
<text class="btn-label">分享好友</text>
|
||||||
|
</button>
|
||||||
|
<view class="btn btn-save" @click="handleSave">
|
||||||
|
<text class="btn-label">{{ isSaving ? '生成中...' : '保存图片' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 海报图片预览(直接展示) -->
|
||||||
|
<view v-if="showPosterPreview" class="poster-save-overlay" @click="handleClose">
|
||||||
|
<view class="poster-save-tip">{{ isSaving ? '海报生成中...' : '海报已生成,可保存到本地' }}</view>
|
||||||
|
<img v-if="posterDataUrl" :src="posterDataUrl" class="poster-save-image" @click.stop />
|
||||||
|
<view v-else class="poster-save-loading">海报生成中...</view>
|
||||||
|
<view class="poster-save-actions" @click.stop>
|
||||||
|
<view class="poster-save-btn" :class="{ 'poster-save-btn-disabled': !posterDataUrl || isSaving }" @click="handleSave">
|
||||||
|
{{ isSaving ? '生成中...' : '保存图片' }}
|
||||||
|
</view>
|
||||||
|
<view class="poster-save-close" @click="handleClose">关闭</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, nextTick } from 'vue';
|
||||||
|
// @ts-ignore
|
||||||
|
import QRCode from '@/utils/qrcode.js';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
userId: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const services = ['个人起名', '公司起名', '专业测名', '择吉日'];
|
||||||
|
const qrModules = ref<number[][]>([]);
|
||||||
|
const qrImageUrl = ref('');
|
||||||
|
const isSaving = ref(false);
|
||||||
|
const posterDataUrl = ref('');
|
||||||
|
const showPosterPreview = ref(false);
|
||||||
|
|
||||||
|
const QR_SIZE = 400;
|
||||||
|
const POSTER_WIDTH = 300;
|
||||||
|
const POSTER_HEIGHT = 500;
|
||||||
|
const POSTER_SCALE = 2;
|
||||||
|
|
||||||
|
const qrUrl = computed(() => `https://yfh5.action-ai.cn?invite_id=${encodeURIComponent(String(props.userId || ''))}`);
|
||||||
|
|
||||||
|
watch(() => props.visible, async (val) => {
|
||||||
|
if (val) {
|
||||||
|
posterDataUrl.value = '';
|
||||||
|
qrImageUrl.value = '';
|
||||||
|
showPosterPreview.value = true;
|
||||||
|
await nextTick();
|
||||||
|
await preparePoster();
|
||||||
|
} else {
|
||||||
|
showPosterPreview.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const createCanvas = (width: number, height: number): HTMLCanvasElement => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
return canvas;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateQrModules = (): number[][] => {
|
||||||
|
const result = QRCode.generate(qrUrl.value, { errorCorrectionLevel: 'M' });
|
||||||
|
const modules = result.modules || [];
|
||||||
|
qrModules.value = modules;
|
||||||
|
return modules;
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawQrModulesToCanvas = (
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
modules: number[][],
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
size: number
|
||||||
|
) => {
|
||||||
|
const count = modules.length;
|
||||||
|
if (!count) return;
|
||||||
|
const cell = size / count;
|
||||||
|
ctx.fillStyle = '#FFFFFF';
|
||||||
|
ctx.fillRect(x, y, size, size);
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
for (let r = 0; r < count; r++) {
|
||||||
|
for (let c = 0; c < count; c++) {
|
||||||
|
if (modules[r][c]) ctx.fillRect(x + c * cell, y + r * cell, Math.ceil(cell), Math.ceil(cell));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateQrImage = async (): Promise<string> => {
|
||||||
|
qrImageUrl.value = '';
|
||||||
|
const modules = generateQrModules();
|
||||||
|
if (!modules.length) {
|
||||||
|
throw new Error('qrcode modules empty');
|
||||||
|
}
|
||||||
|
const canvas = createCanvas(QR_SIZE, QR_SIZE);
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('qrcode canvas context unavailable');
|
||||||
|
}
|
||||||
|
drawQrModulesToCanvas(ctx, modules, 0, 0, QR_SIZE);
|
||||||
|
qrImageUrl.value = canvas.toDataURL('image/png');
|
||||||
|
return qrImageUrl.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
showPosterPreview.value = false;
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
// H5专用loading/toast
|
||||||
|
const showLoading = (title: string) => {
|
||||||
|
if (typeof uni?.showLoading === 'function') {
|
||||||
|
uni.showLoading({ title });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const hideLoading = () => {
|
||||||
|
if (typeof uni?.hideLoading === 'function') {
|
||||||
|
uni.hideLoading();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const showToast = (opts: { title: string; icon?: string }) => {
|
||||||
|
if (typeof uni?.showToast === 'function') {
|
||||||
|
uni.showToast(opts);
|
||||||
|
} else {
|
||||||
|
alert(opts.title);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 绘制圆角矩形
|
||||||
|
const drawRoundRect = (ctx: any, x: number, y: number, w: number, h: number, r: number) => {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x + r, y);
|
||||||
|
ctx.arcTo(x + w, y, x + w, y + h, r);
|
||||||
|
ctx.arcTo(x + w, y + h, x, y + h, r);
|
||||||
|
ctx.arcTo(x, y + h, x, y, r);
|
||||||
|
ctx.arcTo(x, y, x + w, y, r);
|
||||||
|
ctx.closePath();
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawImage = (
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
src: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
w: number,
|
||||||
|
h: number
|
||||||
|
) => {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
ctx.drawImage(img, x, y, w, h);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
img.onerror = () => reject(new Error('image load failed'));
|
||||||
|
img.src = src;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerDownload = (src: string, fileName: string) => {
|
||||||
|
try {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
if (typeof link.download !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
link.href = src;
|
||||||
|
link.download = fileName;
|
||||||
|
link.rel = 'noopener';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildPosterDataUrl = async (qrSrc: string) => {
|
||||||
|
const width = POSTER_WIDTH * POSTER_SCALE;
|
||||||
|
const height = POSTER_HEIGHT * POSTER_SCALE;
|
||||||
|
const posterCanvas = createCanvas(width, height);
|
||||||
|
const ctx = posterCanvas.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('poster canvas context unavailable');
|
||||||
|
}
|
||||||
|
ctx.scale(POSTER_SCALE, POSTER_SCALE);
|
||||||
|
const w = POSTER_WIDTH;
|
||||||
|
const h = POSTER_HEIGHT;
|
||||||
|
|
||||||
|
// 1. 背景
|
||||||
|
ctx.fillStyle = '#2d1515';
|
||||||
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
ctx.fillStyle = '#d4af37';
|
||||||
|
ctx.fillRect(0, 0, w, 5);
|
||||||
|
|
||||||
|
// 2. 标题区
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillStyle = '#d4af37';
|
||||||
|
ctx.font = 'bold 28px serif';
|
||||||
|
ctx.fillText('☯', w / 2, 45);
|
||||||
|
ctx.fillStyle = '#f2e6d8';
|
||||||
|
ctx.font = 'bold 22px serif';
|
||||||
|
ctx.fillText('易凡起名', w / 2, 80);
|
||||||
|
ctx.fillStyle = '#d4af37';
|
||||||
|
ctx.font = '12px sans-serif';
|
||||||
|
ctx.fillText('传承国学智慧 · 赋予美好寓意', w / 2, 100);
|
||||||
|
|
||||||
|
// 3. 卡片
|
||||||
|
const cardX = 20;
|
||||||
|
const cardY = 115;
|
||||||
|
const cardW = w - 40;
|
||||||
|
const cardH = 310;
|
||||||
|
ctx.fillStyle = '#fffdf9';
|
||||||
|
drawRoundRect(ctx, cardX, cardY, cardW, cardH, 8);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// 4. 服务项目
|
||||||
|
ctx.font = '13px sans-serif';
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
services.forEach((name, i) => {
|
||||||
|
const col = i % 2;
|
||||||
|
const row = Math.floor(i / 2);
|
||||||
|
const x = cardX + 25 + col * 115;
|
||||||
|
const y = cardY + 32 + row * 26;
|
||||||
|
ctx.fillStyle = '#8b2323';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x - 8, y - 4, 3, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.fillStyle = '#333';
|
||||||
|
ctx.fillText(name, x, y);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. 分隔线
|
||||||
|
const lineY = cardY + 85;
|
||||||
|
ctx.strokeStyle = '#d4af37';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(cardX + 15, lineY);
|
||||||
|
ctx.lineTo(cardX + cardW - 15, lineY);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// 6. 二维码
|
||||||
|
const qrSize = 120;
|
||||||
|
const qrX = (w - qrSize) / 2;
|
||||||
|
const qrY = lineY + 15;
|
||||||
|
ctx.strokeStyle = '#d4af37';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeRect(qrX - 5, qrY - 5, qrSize + 10, qrSize + 10);
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.fillRect(qrX, qrY, qrSize, qrSize);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await drawImage(ctx, qrSrc, qrX, qrY, qrSize, qrSize);
|
||||||
|
} catch (e) {
|
||||||
|
if (qrModules.value.length) {
|
||||||
|
drawQrModulesToCanvas(ctx, qrModules.value, qrX, qrY, qrSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 邀请码
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillStyle = '#8b2323';
|
||||||
|
ctx.font = '12px sans-serif';
|
||||||
|
ctx.fillText(`邀请码:${props.userId}`, w / 2, qrY + qrSize + 20);
|
||||||
|
ctx.fillStyle = '#999';
|
||||||
|
ctx.font = '11px sans-serif';
|
||||||
|
ctx.fillText('长按识别 · 开启好运', w / 2, qrY + qrSize + 38);
|
||||||
|
|
||||||
|
// 8. 底部
|
||||||
|
ctx.fillStyle = '#d4af37';
|
||||||
|
ctx.font = '11px sans-serif';
|
||||||
|
ctx.fillText('— 邀您共探姓名玄机 —', w / 2, h - 20);
|
||||||
|
ctx.fillRect(0, h - 5, w, 5);
|
||||||
|
|
||||||
|
return posterCanvas.toDataURL('image/png');
|
||||||
|
};
|
||||||
|
|
||||||
|
const preparePoster = async () => {
|
||||||
|
if (isSaving.value) return;
|
||||||
|
isSaving.value = true;
|
||||||
|
showLoading('生成中...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const qrSrc = qrImageUrl.value || await generateQrImage();
|
||||||
|
posterDataUrl.value = await buildPosterDataUrl(qrSrc);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('海报生成失败:', err);
|
||||||
|
showToast({ title: '生成失败', icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
isSaving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (isSaving.value) return;
|
||||||
|
if (!posterDataUrl.value) {
|
||||||
|
await preparePoster();
|
||||||
|
}
|
||||||
|
if (!posterDataUrl.value) return;
|
||||||
|
|
||||||
|
const downloaded = triggerDownload(posterDataUrl.value, `poster-${props.userId || 'share'}.png`);
|
||||||
|
showToast({ title: downloaded ? '已下载海报' : '请长按图片保存', icon: downloaded ? 'success' : 'none' });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-box {
|
||||||
|
width: 85%;
|
||||||
|
max-width: 600rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: -50rpx;
|
||||||
|
right: 0;
|
||||||
|
width: 50rpx;
|
||||||
|
height: 50rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-icon {
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 海报预览 */
|
||||||
|
.poster-preview {
|
||||||
|
background: linear-gradient(180deg, #1a0a0a 0%, #2d1515 50%, #1a0a0a 100%);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 40rpx 32rpx;
|
||||||
|
border: 4rpx solid #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-symbol {
|
||||||
|
font-size: 44rpx;
|
||||||
|
color: #d4af37;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-title {
|
||||||
|
font-size: 44rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #f2e6d8;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-subtitle {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #d4af37;
|
||||||
|
display: block;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-card {
|
||||||
|
background: #fffdf9;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 28rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-item {
|
||||||
|
width: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-dot {
|
||||||
|
width: 10rpx;
|
||||||
|
height: 10rpx;
|
||||||
|
background: #8b2323;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-divider {
|
||||||
|
height: 1rpx;
|
||||||
|
background: #d4af37;
|
||||||
|
margin: 20rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-box {
|
||||||
|
width: 180rpx;
|
||||||
|
height: 180rpx;
|
||||||
|
padding: 8rpx;
|
||||||
|
border: 2rpx solid #d4af37;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #8b2323;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-text {
|
||||||
|
font-size: 18rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-footer {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #d4af37;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮 */
|
||||||
|
.btn-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-top: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 84rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-share {
|
||||||
|
background: linear-gradient(135deg, #8b2323, #6b1a1a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save {
|
||||||
|
background: linear-gradient(135deg, #d4af37, #b8962e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-save-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-save-tip {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 28rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-save-image {
|
||||||
|
max-width: 80%;
|
||||||
|
max-height: 70vh;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-save-loading {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 24rpx;
|
||||||
|
padding: 60rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-save-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-save-btn {
|
||||||
|
background: linear-gradient(135deg, #d4af37, #b8962e);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 26rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 14rpx 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-save-btn-disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-save-close {
|
||||||
|
margin-top: 0;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 28rpx;
|
||||||
|
padding: 16rpx 40rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
245
前端源码/uni-app/components/SixDimensionRadar.vue
Normal file
245
前端源码/uni-app/components/SixDimensionRadar.vue
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
<template>
|
||||||
|
<view class="sixdim-section">
|
||||||
|
<!-- 顶部图标 + 标题 -->
|
||||||
|
<!-- <view class="sixdim-bar">
|
||||||
|
<view class="sixdim-icon-grid">
|
||||||
|
<view v-for="n in 9" :key="n" class="sixdim-icon-dot"></view>
|
||||||
|
</view>
|
||||||
|
<text class="sixdim-bar-title">六维格局</text>
|
||||||
|
</view> -->
|
||||||
|
|
||||||
|
<!-- 卡片主体 -->
|
||||||
|
<view class="sixdim-card">
|
||||||
|
<view class="sixdim-info">
|
||||||
|
<view class="sixdim-info-icon">i</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="sixdim-radar">
|
||||||
|
<!-- SVG 绘制六边形网格线 -->
|
||||||
|
<svg class="sixdim-radar-svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet">
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 中圈六边形 -->
|
||||||
|
<polygon
|
||||||
|
points="50,22 78,36 78,64 50,78 22,64 22,36"
|
||||||
|
fill="none"
|
||||||
|
stroke="rgba(255, 255, 255, 0.4)"
|
||||||
|
stroke-width="0.8"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-for="(label, idx) in labels"
|
||||||
|
:key="idx"
|
||||||
|
class="sixdim-label"
|
||||||
|
:class="'sixdim-label-' + idx"
|
||||||
|
>
|
||||||
|
<text>{{ label }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="sixdim-radar-fill" :style="fillStyle"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="sixdim-remark sixdim-remark-center">
|
||||||
|
<text class="sixdim-remark-label">大师的批注:</text>
|
||||||
|
<text class="sixdim-remark-text">{{ remark }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
labels: string[]; // 六维文字数组,如 ['事业', '财运', '健康', '家庭', '社交', '智慧']
|
||||||
|
values: number[]; // 六维数据数组,范围 0-100
|
||||||
|
remark: string; // 大师批注
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
labels: () => ['事业', '财运', '健康', '家庭', '社交', '智慧'],
|
||||||
|
values: () => [100, 92, 86, 80, 75, 90],
|
||||||
|
remark: '此名天格人格地格配置上佳,主事业运亨通。水木相生,智慧超群,唯需注意社交圆融。'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 根据 values 计算填充区域的 clip-path
|
||||||
|
// 六边形顶点位置(从顶部开始,顺时针):0°(上), 60°(右上), 120°(右下), 180°(下), 240°(左下), 300°(左上)
|
||||||
|
const fillStyle = computed(() => {
|
||||||
|
const maxValue = 100;
|
||||||
|
const centerX = 50; // 中心点 x 百分比
|
||||||
|
const centerY = 50; // 中心点 y 百分比
|
||||||
|
const baseRadius = 44; // 基础半径(从中心到外圈的百分比,约 44%)
|
||||||
|
|
||||||
|
// 计算每个维度的实际半径(0-100 映射到 0-baseRadius)
|
||||||
|
const points = props.values.map((value: number, idx: number) => {
|
||||||
|
const angle = (idx * 60 - 90) * (Math.PI / 180); // 转换为弧度,-90° 使顶部为起点
|
||||||
|
const radius = (value / maxValue) * baseRadius;
|
||||||
|
const x = centerX + radius * Math.cos(angle);
|
||||||
|
const y = centerY + radius * Math.sin(angle);
|
||||||
|
return { x, y };
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成 clip-path polygon 字符串
|
||||||
|
const path = points.map((p: { x: any; y: any; }) => `${p.x}% ${p.y}%`).join(', ');
|
||||||
|
return {
|
||||||
|
clipPath: `polygon(${path})`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sixdim-section {
|
||||||
|
margin-bottom: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding: 0 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-icon-grid {
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
grid-template-rows: repeat(3, 1fr);
|
||||||
|
gap: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-icon-dot {
|
||||||
|
width: 6rpx;
|
||||||
|
height: 6rpx;
|
||||||
|
border-radius: 2rpx;
|
||||||
|
background: #d4af37;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-bar-title {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e2e2e2;
|
||||||
|
letter-spacing: 0.24em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-card {
|
||||||
|
position: relative;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 20rpx;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
padding: 28rpx 24rpx 32rpx;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-info {
|
||||||
|
position: absolute;
|
||||||
|
top: 16rpx;
|
||||||
|
right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-info-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
font-size: 18px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-radar {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 380rpx;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-top: 65%;
|
||||||
|
background: radial-gradient(circle at 50% 50%, rgba(255, 193, 7, 0.04), transparent 70%);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-radar-svg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-radar-fill {
|
||||||
|
position: absolute;
|
||||||
|
inset: 24%;
|
||||||
|
margin: auto;
|
||||||
|
background: linear-gradient(to bottom, rgba(255, 193, 7, 0.75), rgba(255, 87, 34, 0.55));
|
||||||
|
opacity: 0.85;
|
||||||
|
transition: clip-path 0.5s ease-out;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-label {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #cfd2dc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-label-0 {
|
||||||
|
top: 6%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-label-1 {
|
||||||
|
top: 26%;
|
||||||
|
right: 6%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-label-2 {
|
||||||
|
bottom: 28%;
|
||||||
|
right: 4%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-label-3 {
|
||||||
|
bottom: 4%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-label-4 {
|
||||||
|
bottom: 28%;
|
||||||
|
left: 4%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-label-5 {
|
||||||
|
top: 26%;
|
||||||
|
left: 6%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-remark {
|
||||||
|
font-size: 20rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #a0a4b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-remark-center {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-remark-label {
|
||||||
|
color: #fdd835;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-remark-text {
|
||||||
|
color: #c0c3d0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
255
前端源码/uni-app/components/SixDimensionRadarDesktopEchart.vue
Normal file
255
前端源码/uni-app/components/SixDimensionRadarDesktopEchart.vue
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
<template>
|
||||||
|
<view class="sixdim-section">
|
||||||
|
<view class="sixdim-card">
|
||||||
|
<view class="sixdim-info">
|
||||||
|
<view class="sixdim-info-icon">i</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 电脑端雷达图:ECharts 渲染 -->
|
||||||
|
<view class="sixdim-radar">
|
||||||
|
<view ref="chartRef" class="sixdim-echart-inner" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="sixdim-remark sixdim-remark-center">
|
||||||
|
<text class="sixdim-remark-label">大师批注:</text>
|
||||||
|
<text class="sixdim-remark-text">{{ remark }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onUnmounted, ref, watch } from "vue";
|
||||||
|
import * as echarts from "echarts";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
labels: string[];
|
||||||
|
values: number[];
|
||||||
|
remark: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const chartRef = ref<HTMLElement | null>(null);
|
||||||
|
let chart: echarts.ECharts | null = null;
|
||||||
|
|
||||||
|
const getSafeValues = (vals: number[]) => (Array.isArray(vals) ? vals.map((v) => (Number.isFinite(v as any) ? Number(v) : 0)) : []);
|
||||||
|
const getSafeLabels = (labs: string[]) => (Array.isArray(labs) ? labs.map((s) => String(s ?? "")) : []);
|
||||||
|
|
||||||
|
/** 雷达图 indicator 与 value 必须等长且非空,否则 ECharts radarLayout 会报 push undefined */
|
||||||
|
function alignRadarData(rawLabels: string[], rawValues: number[]): { labels: string[]; values: number[] } {
|
||||||
|
let labels = getSafeLabels(rawLabels).map((s) => s.trim());
|
||||||
|
let values = getSafeValues(rawValues);
|
||||||
|
let n = Math.max(labels.length, values.length);
|
||||||
|
|
||||||
|
// 个人测名默认六维:事业、财运、健康、家庭、社交、智慧
|
||||||
|
const defaultSix = ["事业", "财运", "健康", "家庭", "社交", "智慧"];
|
||||||
|
|
||||||
|
if (n === 0) {
|
||||||
|
labels = defaultSix;
|
||||||
|
values = [0, 0, 0, 0, 0, 0];
|
||||||
|
n = 6;
|
||||||
|
} else if (n <= 6) {
|
||||||
|
// 不足 6 维时,补足到 6 维并强制使用默认中文维度,确保「家庭」等字段一定出现
|
||||||
|
n = 6;
|
||||||
|
while (values.length < n) values.push(0);
|
||||||
|
labels = defaultSix;
|
||||||
|
} else {
|
||||||
|
while (labels.length < n) labels.push(`维度${labels.length + 1}`);
|
||||||
|
while (values.length < n) values.push(0);
|
||||||
|
labels = labels.slice(0, n);
|
||||||
|
values = values.slice(0, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再次截断到 n,防御性处理
|
||||||
|
labels = labels.slice(0, n).map((s) => s || "—");
|
||||||
|
values = values.slice(0, n);
|
||||||
|
return { labels, values };
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildOption = () => {
|
||||||
|
const { labels, values } = alignRadarData(props.labels, props.values);
|
||||||
|
|
||||||
|
const radarIndicator = labels.map((name) => ({ name: name || "—", max: 100 }));
|
||||||
|
|
||||||
|
return {
|
||||||
|
animationDuration: 700,
|
||||||
|
animationEasing: "cubicOut" as any,
|
||||||
|
tooltip: {
|
||||||
|
trigger: "item",
|
||||||
|
formatter: (params: any) => {
|
||||||
|
const valueArr: number[] = params?.value ?? [];
|
||||||
|
const parts = labels.map((lab, idx) => `${lab}:${valueArr[idx] ?? 0}`);
|
||||||
|
return parts.join("<br/>");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
radar: {
|
||||||
|
indicator: radarIndicator,
|
||||||
|
splitNumber: 4,
|
||||||
|
shape: "polygon",
|
||||||
|
center: ["50%", "48%"],
|
||||||
|
// 轴文字可能在较小容器内被裁切:适当缩小半径,让 label 落在视窗内
|
||||||
|
radius: "62%",
|
||||||
|
axisName: {
|
||||||
|
color: "rgba(207, 210, 220, 0.88)",
|
||||||
|
fontSize: 9,
|
||||||
|
padding: 6,
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: "rgba(212, 220, 236, 0.32)",
|
||||||
|
width: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: "rgba(255, 255, 255, 0.12)",
|
||||||
|
width: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
splitArea: {
|
||||||
|
areaStyle: {
|
||||||
|
color: ["rgba(255, 255, 255, 0.02)", "rgba(255, 193, 7, 0.015)"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: "radar",
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: [...values],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lineStyle: {
|
||||||
|
color: "rgba(255, 193, 7, 0.92)",
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: "rgba(255, 193, 7, 0.95)",
|
||||||
|
},
|
||||||
|
symbol: "circle",
|
||||||
|
symbolSize: 3,
|
||||||
|
areaStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: "rgba(255, 193, 7, 0.78)" },
|
||||||
|
{ offset: 1, color: "rgba(243, 138, 43, 0.52)" },
|
||||||
|
]),
|
||||||
|
opacity: 0.95,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 包含所有轴文字(避免 label 在容器内被裁切)
|
||||||
|
grid: { containLabel: true },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
if (!chartRef.value) return;
|
||||||
|
if (!chart) {
|
||||||
|
// 使用 svg 渲染器,避免 canvas getContext 在某些环境出错
|
||||||
|
chart = echarts.init(chartRef.value, undefined, { renderer: "svg" });
|
||||||
|
}
|
||||||
|
chart.setOption(buildOption(), { notMerge: true, lazyUpdate: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
render();
|
||||||
|
|
||||||
|
// 适配父容器缩放(桌面布局下尤其必要)
|
||||||
|
const onResize = () => chart?.resize();
|
||||||
|
window.addEventListener("resize", onResize);
|
||||||
|
|
||||||
|
onUnmounted(() => window.removeEventListener("resize", onResize));
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.labels, props.values],
|
||||||
|
() => {
|
||||||
|
render();
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (chart) {
|
||||||
|
chart.dispose();
|
||||||
|
chart = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sixdim-section {
|
||||||
|
margin-bottom: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-card {
|
||||||
|
position: relative;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 20rpx;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
padding: 28rpx 24rpx 32rpx;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-info {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-info-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
font-size: 18px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-radar {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 380rpx;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-top: 0;
|
||||||
|
background: radial-gradient(circle at 50% 50%, rgba(255, 193, 7, 0.04), transparent 70%);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
height: 280px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-echart-inner {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-remark {
|
||||||
|
font-size: 20rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #a0a4b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-remark-center {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-remark-label {
|
||||||
|
color: #fdd835;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sixdim-remark-text {
|
||||||
|
color: #c0c3d0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
367
前端源码/uni-app/components/examples/PaymentExample.vue
Normal file
367
前端源码/uni-app/components/examples/PaymentExample.vue
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
<template>
|
||||||
|
<view class="payment-example">
|
||||||
|
<view class="example-header">
|
||||||
|
<text class="example-title">支付功能示例</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 示例1: 购买报告 -->
|
||||||
|
<view class="example-section">
|
||||||
|
<text class="example-section-title">示例1: 购买命名报告</text>
|
||||||
|
<view class="example-card">
|
||||||
|
<view class="example-card-content">
|
||||||
|
<text class="example-card-name">张三命名报告</text>
|
||||||
|
<text class="example-card-desc">包含六维分析、周易卦象等</text>
|
||||||
|
<text class="example-card-price">¥99</text>
|
||||||
|
</view>
|
||||||
|
<view class="example-card-btn" @click="buyReport">
|
||||||
|
<text class="example-card-btn-text">立即购买</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 示例2: 推广合伙人 -->
|
||||||
|
<view class="example-section">
|
||||||
|
<text class="example-section-title">示例2: 开通推广合伙人</text>
|
||||||
|
<view class="example-card">
|
||||||
|
<view class="example-card-content">
|
||||||
|
<text class="example-card-name">推广合伙人权益</text>
|
||||||
|
<text class="example-card-desc">开启睡后收入之旅</text>
|
||||||
|
<text class="example-card-price">¥99</text>
|
||||||
|
</view>
|
||||||
|
<view class="example-card-btn" @click="buyPartner">
|
||||||
|
<text class="example-card-btn-text">立即开通</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 示例3: 测试支付 -->
|
||||||
|
<view class="example-section">
|
||||||
|
<text class="example-section-title">示例3: 测试支付(0.01元)</text>
|
||||||
|
<view class="example-card">
|
||||||
|
<view class="example-card-content">
|
||||||
|
<text class="example-card-name">测试商品</text>
|
||||||
|
<text class="example-card-desc">用于测试支付流程</text>
|
||||||
|
<text class="example-card-price">¥0.01</text>
|
||||||
|
</view>
|
||||||
|
<view class="example-card-btn" @click="testPay">
|
||||||
|
<text class="example-card-btn-text">测试支付</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 订单列表 -->
|
||||||
|
<view class="example-section">
|
||||||
|
<text class="example-section-title">最近订单</text>
|
||||||
|
<view v-if="orders.length === 0" class="example-empty">
|
||||||
|
<text class="example-empty-text">暂无订单</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="example-orders">
|
||||||
|
<view v-for="order in orders" :key="order.out_trade_no" class="example-order">
|
||||||
|
<view class="example-order-info">
|
||||||
|
<text class="example-order-name">{{ order.business_type }}</text>
|
||||||
|
<text class="example-order-time">{{ order.paid_at || '待支付' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="example-order-right">
|
||||||
|
<text class="example-order-amount">¥{{ order.total_amount }}</text>
|
||||||
|
<text class="example-order-status" :class="`status-${order.status}`">
|
||||||
|
{{ getStatusText(order.status) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 支付弹窗 -->
|
||||||
|
<PaymentModal :visible="showPayment" :product-name="paymentInfo.productName"
|
||||||
|
:product-desc="paymentInfo.productDesc" :product-icon="paymentInfo.productIcon" :amount="paymentInfo.amount"
|
||||||
|
:business-type="paymentInfo.businessType" :business-id="paymentInfo.businessId" @close="showPayment = false"
|
||||||
|
@success="handlePaymentSuccess" @fail="handlePaymentFail" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import PaymentModal from '../PaymentModal.vue';
|
||||||
|
import type { QueryOrderResponse } from '@/api/types';
|
||||||
|
|
||||||
|
declare const uni: any;
|
||||||
|
|
||||||
|
const showPayment = ref(false);
|
||||||
|
const orders = ref<QueryOrderResponse[]>([]);
|
||||||
|
|
||||||
|
const paymentInfo = reactive({
|
||||||
|
productName: '',
|
||||||
|
productDesc: '',
|
||||||
|
productIcon: '📦',
|
||||||
|
amount: 0,
|
||||||
|
businessType: '',
|
||||||
|
businessId: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 购买报告
|
||||||
|
const buyReport = () => {
|
||||||
|
paymentInfo.productName = '张三命名报告';
|
||||||
|
paymentInfo.productDesc = '包含六维分析、周易卦象、开运建议等';
|
||||||
|
paymentInfo.productIcon = '📊';
|
||||||
|
paymentInfo.amount = 99;
|
||||||
|
paymentInfo.businessType = 'naming_report';
|
||||||
|
paymentInfo.businessId = 123;
|
||||||
|
showPayment.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 购买推广合伙人
|
||||||
|
const buyPartner = () => {
|
||||||
|
paymentInfo.productName = '推广合伙人权益';
|
||||||
|
paymentInfo.productDesc = '开启睡后收入之旅';
|
||||||
|
paymentInfo.productIcon = '💼';
|
||||||
|
paymentInfo.amount = 99;
|
||||||
|
paymentInfo.businessType = 'partner_apply';
|
||||||
|
paymentInfo.businessId = 456;
|
||||||
|
showPayment.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 测试支付
|
||||||
|
const testPay = () => {
|
||||||
|
paymentInfo.productName = '测试商品';
|
||||||
|
paymentInfo.productDesc = '用于测试支付流程';
|
||||||
|
paymentInfo.productIcon = '🧪';
|
||||||
|
paymentInfo.amount = 0.01;
|
||||||
|
paymentInfo.businessType = 'test';
|
||||||
|
paymentInfo.businessId = 999;
|
||||||
|
showPayment.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 支付成功回调
|
||||||
|
const handlePaymentSuccess = (outTradeNo: string) => {
|
||||||
|
|
||||||
|
// Web环境使用alert,uni-app环境使用showModal
|
||||||
|
if (typeof uni?.showModal === 'function') {
|
||||||
|
uni.showModal({
|
||||||
|
title: '支付成功',
|
||||||
|
content: `订单号:${outTradeNo}\n感谢您的购买!`,
|
||||||
|
showCancel: false,
|
||||||
|
success: () => {
|
||||||
|
// 刷新订单列表
|
||||||
|
loadOrders();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Web环境使用原生alert
|
||||||
|
alert(`支付成功\n\n订单号:${outTradeNo}\n感谢您的购买!`);
|
||||||
|
loadOrders();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 支付失败回调
|
||||||
|
const handlePaymentFail = (message: string) => {
|
||||||
|
console.log('支付失败:', message);
|
||||||
|
|
||||||
|
// Web环境使用alert,uni-app环境使用showModal
|
||||||
|
if (typeof uni?.showModal === 'function') {
|
||||||
|
uni.showModal({
|
||||||
|
title: '支付失败',
|
||||||
|
content: message || '支付过程中出现问题,请重试',
|
||||||
|
showCancel: false
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Web环境使用原生alert
|
||||||
|
alert(`支付失败\n\n${message || '支付过程中出现问题,请重试'}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载订单列表(示例数据)
|
||||||
|
const loadOrders = () => {
|
||||||
|
// 实际应该调用API获取订单列表
|
||||||
|
orders.value = [
|
||||||
|
{
|
||||||
|
out_trade_no: 'ORDER_001',
|
||||||
|
status: 'paid',
|
||||||
|
total_amount: 99,
|
||||||
|
paid_amount: 99,
|
||||||
|
paid_at: '2026-01-15 10:30:00',
|
||||||
|
business_type: 'naming_report',
|
||||||
|
business_id: 123
|
||||||
|
},
|
||||||
|
{
|
||||||
|
out_trade_no: 'ORDER_002',
|
||||||
|
status: 'pending',
|
||||||
|
total_amount: 99,
|
||||||
|
business_type: 'partner_apply',
|
||||||
|
business_id: 456
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
const statusMap: Record<string, string> = {
|
||||||
|
pending: '待支付',
|
||||||
|
paid: '已支付',
|
||||||
|
cancelled: '已取消',
|
||||||
|
refunded: '已退款'
|
||||||
|
};
|
||||||
|
return statusMap[status] || status;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadOrders();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.payment-example {
|
||||||
|
padding: 32rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-header {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-title {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-section {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-section-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card-name {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card-desc {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card-price {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #8b2323;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
background-color: #8b2323;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card-btn-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 64rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-empty-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-orders {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-order {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-order-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-order-name {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-order-time {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-order-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-order-amount {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-order-status {
|
||||||
|
font-size: 20rpx;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-paid {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending {
|
||||||
|
background-color: #fff3e0;
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cancelled {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-refunded {
|
||||||
|
background-color: #ffebee;
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
31
前端源码/uni-app/components/icons/ActivityIcon.vue
Normal file
31
前端源码/uni-app/components/icons/ActivityIcon.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<image
|
||||||
|
:style="{ width: size + 'px', height: size + 'px' }"
|
||||||
|
:src="iconSrc"
|
||||||
|
mode="aspectFit"
|
||||||
|
:class="className || ''"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
size?: number | string;
|
||||||
|
className?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 24,
|
||||||
|
className: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const iconSrc =
|
||||||
|
"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMjIgMTJoLTIuNDhhMiAyIDAgMCAwLTEuOTMgMS40NmwtMi4zNSA4LjM2YS4yNS4yNSAwIDAgMS0uNDggMEw5LjI0IDIuMThhLjI1LjI1IDAgMCAwLS40OCAwbC0yLjM1IDguMzZBMiAyIDAgMCAxIDQuNDkgMTJIMiIgc3Ryb2tlPSIjOGIyMzIzIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
image {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
30
前端源码/uni-app/components/icons/CalendarIcon.vue
Normal file
30
前端源码/uni-app/components/icons/CalendarIcon.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<svg :width="size" :height="size" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
:class="className || ''">
|
||||||
|
<path d="M8 2v4" stroke="#8b2323" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<path d="M16 2v4" stroke="#8b2323" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<rect width="18" height="18" x="3" y="4" rx="2" stroke="#8b2323" stroke-width="1.5" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<path d="M3 10h18" stroke="#8b2323" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
size?: number | string;
|
||||||
|
className?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 24,
|
||||||
|
className: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
svg {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
26
前端源码/uni-app/components/icons/CheckIcon.vue
Normal file
26
前端源码/uni-app/components/icons/CheckIcon.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<svg :width="size" :height="size" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
:class="className || ''">
|
||||||
|
<path d="M20 6 9 17l-5-5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
size?: number | string;
|
||||||
|
className?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 24,
|
||||||
|
className: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
svg {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
26
前端源码/uni-app/components/icons/ChevronDownIcon.vue
Normal file
26
前端源码/uni-app/components/icons/ChevronDownIcon.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<svg :width="size" :height="size" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
:class="className || ''">
|
||||||
|
<path d="m6 9 6 6 6-6" stroke="#8b2323" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
size?: number | string;
|
||||||
|
className?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 24,
|
||||||
|
className: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
svg {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
27
前端源码/uni-app/components/icons/CloseIcon.vue
Normal file
27
前端源码/uni-app/components/icons/CloseIcon.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<svg :width="size" :height="size" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
:class="className || ''">
|
||||||
|
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
size?: number | string;
|
||||||
|
className?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 24,
|
||||||
|
className: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
svg {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
30
前端源码/uni-app/components/icons/HomeIcon.vue
Normal file
30
前端源码/uni-app/components/icons/HomeIcon.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<svg :width="size" :height="size" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
:class="className || ''">
|
||||||
|
<path d="M3 11L12 3L21 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<path d="M5 11V21H19V11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<path d="M9 21V15H15V21" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<path d="M2 11H22" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
size?: number | string;
|
||||||
|
className?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 24,
|
||||||
|
className: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
svg {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
33
前端源码/uni-app/components/icons/NamingIcon.vue
Normal file
33
前端源码/uni-app/components/icons/NamingIcon.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<svg :width="size" :height="size" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
:class="className || ''">
|
||||||
|
<path d="M12 19L19 12L22 15L15 22L12 19Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<path d="M18 13L16.5 14.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<path d="M2 22L4.5 20" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<path d="M2 22C2 22 4 18 9 13L13 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<path d="M11 5L15 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
size?: number | string;
|
||||||
|
className?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 24,
|
||||||
|
className: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
svg {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
31
前端源码/uni-app/components/icons/PenToolIcon.vue
Normal file
31
前端源码/uni-app/components/icons/PenToolIcon.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<image
|
||||||
|
:style="{ width: size + 'px', height: size + 'px' }"
|
||||||
|
:src="iconSrc"
|
||||||
|
mode="aspectFit"
|
||||||
|
:class="className || ''"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
size?: number | string;
|
||||||
|
className?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 24,
|
||||||
|
className: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const iconSrc =
|
||||||
|
"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTUuNzA3IDIxLjI5M2ExIDEgMCAwIDEtMS40MTQgMGwtMS41ODYtMS41ODZhMSAxIDAgMCAxIDAtMS40MTRsNS41ODYtNS41ODZhMSAxIDAgMCAxIDEuNDE0IDBsMS41ODYgMS41ODZhMSAxIDAgMCAxIDAgMS40MTR6IiBzdHJva2U9IiM4YjIzMjMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJtMTggMTMtMS4zNzUtNi44NzRhMSAxIDAgMCAwLS43NDYtLjc3NkwzLjIzNSAyLjAyOGExIDEgMCAwIDAtMS4yMDcgMS4yMDdMNS4zNSAxNS44NzlhMSAxIDAgMCAwIC43NzYuNzQ2TDEzIDE4IiBzdHJva2U9IiM4YjIzMjMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJtMi4zIDIuMyA3LjI4NiA3LjI4NiIgc3Ryb2tlPSIjOGIyMzIzIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PGNpcmNsZSBjeD0iMTEiIGN5PSIxMSIgcj0iMiIgc3Ryb2tlPSIjOGIyMzIzIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
image {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
29
前端源码/uni-app/components/icons/ProfileIcon.vue
Normal file
29
前端源码/uni-app/components/icons/ProfileIcon.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<svg :width="size" :height="size" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
:class="className || ''">
|
||||||
|
<circle cx="12" cy="7" r="4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<path d="M6 21V19C6 15.6863 8.68629 13 12 13C15.3137 13 18 15.6863 18 19V21" stroke="currentColor"
|
||||||
|
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
size?: number | string;
|
||||||
|
className?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 24,
|
||||||
|
className: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
svg {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
32
前端源码/uni-app/components/icons/RenamingIcon.vue
Normal file
32
前端源码/uni-app/components/icons/RenamingIcon.vue
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<svg :width="size" :height="size" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
:class="className || ''">
|
||||||
|
<path d="M21 12C21 16.9706 16.9706 21 12 21C9.69494 21 7.59227 20.1332 6.00488 18.7121" stroke="currentColor"
|
||||||
|
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<path d="M3 12C3 7.02944 7.02944 3 12 3C14.3051 3 16.4077 3.86676 17.9951 5.28793" stroke="currentColor"
|
||||||
|
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<path d="M18 5V9H14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<path d="M6 19V15H10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
size?: number | string;
|
||||||
|
className?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 24,
|
||||||
|
className: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
svg {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
29
前端源码/uni-app/components/icons/SearchIcon.vue
Normal file
29
前端源码/uni-app/components/icons/SearchIcon.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<svg :width="size" :height="size" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
:class="className || ''">
|
||||||
|
<path d="m21 21-4.34-4.34" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
size?: number | string;
|
||||||
|
className?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 24,
|
||||||
|
className: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
svg {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
31
前端源码/uni-app/components/icons/TestIcon.vue
Normal file
31
前端源码/uni-app/components/icons/TestIcon.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<svg :width="size" :height="size" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
:class="className || ''">
|
||||||
|
<rect x="5" y="3" width="14" height="18" rx="2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<path d="M9 7H15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<path d="M9 11H15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<path d="M9 15H12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<circle cx="15" cy="16" r="1.5" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
size?: number | string;
|
||||||
|
className?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 24,
|
||||||
|
className: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
svg {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1025
前端源码/uni-app/components/screens/Affinity.vue
Normal file
1025
前端源码/uni-app/components/screens/Affinity.vue
Normal file
File diff suppressed because it is too large
Load Diff
1175
前端源码/uni-app/components/screens/AffinityResult.vue
Normal file
1175
前端源码/uni-app/components/screens/AffinityResult.vue
Normal file
File diff suppressed because it is too large
Load Diff
131
前端源码/uni-app/components/screens/Analysis.vue
Normal file
131
前端源码/uni-app/components/screens/Analysis.vue
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<view class="analysis-screen">
|
||||||
|
<!-- Starry Background -->
|
||||||
|
<view class="analysis-bg">
|
||||||
|
<view class="analysis-bg-gradient"></view>
|
||||||
|
<view
|
||||||
|
v-for="s in stars"
|
||||||
|
:key="s.id"
|
||||||
|
class="analysis-star"
|
||||||
|
:style="{top: s.top + '%', left: s.left + '%', width: s.size + 'px', height: s.size + 'px'}">
|
||||||
|
</view>
|
||||||
|
<view class="analysis-bg-blur-1"></view>
|
||||||
|
<view class="analysis-bg-blur-2"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Loading / Analyzing Stage -->
|
||||||
|
<view v-if="stage !== 'result'" class="analysis-loading">
|
||||||
|
<MysticCompass />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Result Stage -->
|
||||||
|
<AnalysisResult v-else @back="$emit('back')" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import MysticCompass from '../MysticCompass.vue';
|
||||||
|
import AnalysisResult from '../AnalysisResult.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
back: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const stage = ref<'loading' | 'analyzing' | 'result'>('loading');
|
||||||
|
|
||||||
|
// Starry background
|
||||||
|
const stars = ref(Array.from({ length: 50 }).map((_, i) => ({
|
||||||
|
id: i,
|
||||||
|
top: Math.random() * 100,
|
||||||
|
left: Math.random() * 100,
|
||||||
|
size: Math.random() * 2 + 1
|
||||||
|
})));
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
stage.value = 'analyzing';
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
stage.value = 'result';
|
||||||
|
}, 3500);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.analysis-screen {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
|
||||||
|
color: #e2e2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Background */
|
||||||
|
.analysis-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-bg-gradient {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(to bottom, #050508, #10101a, #1a1a2e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-star {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: white;
|
||||||
|
opacity: 0.2;
|
||||||
|
animation: twinkle 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes twinkle {
|
||||||
|
0%, 100% { opacity: 0.1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.5; transform: scale(1.2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-bg-blur-1 {
|
||||||
|
position: absolute;
|
||||||
|
top: -10%;
|
||||||
|
left: -10%;
|
||||||
|
width: 50%;
|
||||||
|
height: 50%;
|
||||||
|
background: #2a3d5d;
|
||||||
|
opacity: 0.2;
|
||||||
|
filter: blur(100px);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-bg-blur-2 {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -10%;
|
||||||
|
right: -10%;
|
||||||
|
width: 50%;
|
||||||
|
height: 50%;
|
||||||
|
background: #9c2a2a;
|
||||||
|
opacity: 0.1;
|
||||||
|
filter: blur(100px);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Stage */
|
||||||
|
.analysis-loading {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
12
前端源码/uni-app/components/screens/AnalysisPlaceholder.vue
Normal file
12
前端源码/uni-app/components/screens/AnalysisPlaceholder.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<view class="h-full flex flex-col items-center justify-center bg-[#050508] text-[#d4af37] font-serif relative overflow-hidden">
|
||||||
|
<view class="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-[#1a1a2e] via-[#050508] to-[#000] opacity-80"></view>
|
||||||
|
<view class="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/stardust.png')] opacity-20"></view>
|
||||||
|
<view class="relative z-10 text-center px-8">
|
||||||
|
<view class="text-5xl mb-4">☯</view>
|
||||||
|
<text class="block text-xl font-bold tracking-[0.4em] mb-2">命盘解析生成中</text>
|
||||||
|
<text class="block text-sm text-[#e2e2e2]/80">分析时间预计1-2分钟,可点击返回按钮退出,并在我的方案中查看结果</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
590
前端源码/uni-app/components/screens/AuspiciousForm.vue
Normal file
590
前端源码/uni-app/components/screens/AuspiciousForm.vue
Normal file
@@ -0,0 +1,590 @@
|
|||||||
|
<template>
|
||||||
|
<view class="auspicious-form">
|
||||||
|
<view class="auspicious-texture"></view>
|
||||||
|
|
||||||
|
<!-- 固定Header -->
|
||||||
|
<view class="auspicious-fixed-header">
|
||||||
|
<view class="status-bar-placeholder"></view>
|
||||||
|
<view class="auspicious-header">
|
||||||
|
<view class="auspicious-back" @click="$emit('back')">‹</view>
|
||||||
|
<text class="auspicious-title">精准八字择吉</text>
|
||||||
|
<view class="auspicious-header-spacer"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 头部占位 -->
|
||||||
|
<view class="auspicious-header-placeholder"></view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="auspicious-scroll">
|
||||||
|
<view class="auspicious-container">
|
||||||
|
<view class="auspicious-intro">
|
||||||
|
<text class="auspicious-intro-title">顺天时 · 得地利 · 人和顺</text>
|
||||||
|
<text class="auspicious-intro-sub">根据您的生辰八字,精准测算最佳黄道吉日</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 事项 -->
|
||||||
|
<view class="auspicious-section">
|
||||||
|
<label class="auspicious-label">
|
||||||
|
您要求测的事项
|
||||||
|
</label>
|
||||||
|
<view class="auspicious-grid">
|
||||||
|
<button v-for="type in eventTypes" :key="type.id" @click="form.eventType = type.id"
|
||||||
|
:class="['auspicious-event-card', form.eventType === type.id ? 'is-active' : '']">
|
||||||
|
<text class="auspicious-event-text">{{ type.label }}</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="form.eventType === 'other'" class="auspicious-custom">
|
||||||
|
<input v-model="form.customEvent" type="text" placeholder="请输入您要求测的事项 (如: 签约, 出行, 动土...)"
|
||||||
|
class="auspicious-input" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 福主信息 -->
|
||||||
|
<view class="auspicious-section">
|
||||||
|
<label class="auspicious-label">
|
||||||
|
福主信息
|
||||||
|
</label>
|
||||||
|
<view class="auspicious-card">
|
||||||
|
<view class="auspicious-field">
|
||||||
|
<label class="auspicious-field-label">您的姓名</label>
|
||||||
|
<input v-model="form.name" type="text" placeholder="请输入真实姓名" class="auspicious-field-input" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="auspicious-field">
|
||||||
|
<label class="auspicious-field-label">性别</label>
|
||||||
|
<view class="auspicious-gender">
|
||||||
|
<button :class="['auspicious-gender-btn', form.gender === 'male' ? 'is-active' : '']"
|
||||||
|
@click="form.gender = 'male'">
|
||||||
|
男
|
||||||
|
</button>
|
||||||
|
<button :class="['auspicious-gender-btn', form.gender === 'female' ? 'is-active' : '']"
|
||||||
|
@click="form.gender = 'female'">
|
||||||
|
女
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="auspicious-field">
|
||||||
|
<label class="auspicious-field-label">出生日期时辰</label>
|
||||||
|
<view class="auspicious-date-trigger" @click="showDatePicker = true">
|
||||||
|
<text class="auspicious-date-text" :class="{ 'is-placeholder': !form.birthDateDisplay }">
|
||||||
|
{{ form.birthDateDisplay || '请选择出生日期时辰' }}
|
||||||
|
</text>
|
||||||
|
<text class="auspicious-date-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="auspicious-field">
|
||||||
|
<label class="auspicious-field-label">出生地</label>
|
||||||
|
<input v-model="form.birthPlace" type="text" placeholder="请输入出生地(如:临沂市)" class="auspicious-field-input" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 择吉目的 -->
|
||||||
|
<view class="auspicious-section">
|
||||||
|
<label class="auspicious-label">
|
||||||
|
择吉目的
|
||||||
|
</label>
|
||||||
|
<view class="auspicious-card">
|
||||||
|
<textarea v-model="form.zejiPurpose" placeholder="请描述您的择吉目的(如:选择结婚吉日,希望婚姻美满幸福)" class="auspicious-textarea"
|
||||||
|
maxlength="200" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 期望日期范围 -->
|
||||||
|
<view class="auspicious-section">
|
||||||
|
<label class="auspicious-label">
|
||||||
|
期望日期范围
|
||||||
|
</label>
|
||||||
|
<view class="auspicious-card">
|
||||||
|
<view class="auspicious-field">
|
||||||
|
<label class="auspicious-field-label">开始日期</label>
|
||||||
|
<view class="auspicious-date-trigger" @click="showStartDatePicker = true">
|
||||||
|
<text class="auspicious-date-text" :class="{ 'is-placeholder': !form.dateRangeStartDisplay }">
|
||||||
|
{{ form.dateRangeStartDisplay || '请选择开始日期' }}
|
||||||
|
</text>
|
||||||
|
<text class="auspicious-date-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="auspicious-field">
|
||||||
|
<label class="auspicious-field-label">结束日期</label>
|
||||||
|
<view class="auspicious-date-trigger" @click="showEndDatePicker = true">
|
||||||
|
<text class="auspicious-date-text" :class="{ 'is-placeholder': !form.dateRangeEndDisplay }">
|
||||||
|
{{ form.dateRangeEndDisplay || '请选择结束日期' }}
|
||||||
|
</text>
|
||||||
|
<text class="auspicious-date-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 日期选择器 -->
|
||||||
|
<MysticDatePicker :is-open="showDatePicker" title="请择良辰" :default-value="form.birthDateDisplay"
|
||||||
|
@close="showDatePicker = false" @confirm="handleDateConfirm" />
|
||||||
|
|
||||||
|
<!-- 开始日期:不可晚于今天;年份为过去至今年 -->
|
||||||
|
<MysticDatePicker :is-open="showStartDatePicker" title="选择开始日期" :default-value="form.dateRangeStartDisplay"
|
||||||
|
:min-year="zejiStartMinYear" :max-year="zejiStartMaxYear" cap-at-today
|
||||||
|
footer-tip="开始日期:不可选择今天之后的日期,滑动选择后自动对应农历干支"
|
||||||
|
@close="showStartDatePicker = false" @confirm="handleStartDateConfirm" />
|
||||||
|
|
||||||
|
<!-- 结束日期选择器 -->
|
||||||
|
<MysticDatePicker :is-open="showEndDatePicker" title="选择结束日期" :default-value="form.dateRangeEndDisplay"
|
||||||
|
:min-year="zejiExpectRangeMinYear" :max-year="zejiExpectRangeMaxYear"
|
||||||
|
footer-tip="期望日期区间:支持选择至未来多年,滑动选择后自动对应农历干支"
|
||||||
|
@close="showEndDatePicker = false" @confirm="handleEndDateConfirm" />
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<view class="auspicious-footer">
|
||||||
|
<button class="auspicious-submit" @click="submit">
|
||||||
|
立即测算
|
||||||
|
</button>
|
||||||
|
<text class="auspicious-footer-tip">
|
||||||
|
已有 28,392 人通过壹梵择得良辰吉日
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, reactive, ref } from "vue";
|
||||||
|
import MysticDatePicker from "../MysticDatePicker.vue";
|
||||||
|
import { baziZejiApi, type BaziZejiCalculateRequest } from '../../api';
|
||||||
|
|
||||||
|
declare const uni: any;
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
submit: [data: any];
|
||||||
|
back: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
eventType: "wedding",
|
||||||
|
customEvent: "",
|
||||||
|
name: "",
|
||||||
|
gender: "male",
|
||||||
|
birthDateDisplay: "",
|
||||||
|
birthDateApi: "",
|
||||||
|
birthPlace: "",
|
||||||
|
zejiPurpose: "",
|
||||||
|
dateRangeStartDisplay: "",
|
||||||
|
dateRangeStart: "",
|
||||||
|
dateRangeEndDisplay: "",
|
||||||
|
dateRangeEnd: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const showDatePicker = ref(false);
|
||||||
|
const showStartDatePicker = ref(false);
|
||||||
|
const showEndDatePicker = ref(false);
|
||||||
|
|
||||||
|
/** 精准八字择吉 · 结束日期:从当年起可往后选多年 */
|
||||||
|
const ZEJI_RANGE_FORWARD_YEARS = 50;
|
||||||
|
const zejiExpectRangeMinYear = computed(() => new Date().getFullYear());
|
||||||
|
const zejiExpectRangeMaxYear = computed(() => new Date().getFullYear() + ZEJI_RANGE_FORWARD_YEARS);
|
||||||
|
|
||||||
|
/** 开始日期:至多为今天,年份列与生辰类似(当年往前若干年) */
|
||||||
|
const zejiStartMinYear = computed(() => new Date().getFullYear() - 85);
|
||||||
|
const zejiStartMaxYear = computed(() => new Date().getFullYear());
|
||||||
|
|
||||||
|
const eventTypes = [
|
||||||
|
{ id: "wedding", label: "婚嫁择吉", icon: "💒" },
|
||||||
|
{ id: "business", label: "开业择吉", icon: "🧧" },
|
||||||
|
{ id: "move", label: "搬家择吉", icon: "🏠" },
|
||||||
|
{ id: "travel", label: "出行择吉", icon: "✈️" },
|
||||||
|
{ id: "investment", label: "投资择吉", icon: "💰" },
|
||||||
|
{ id: "surgery", label: "手术择吉", icon: "🏥" },
|
||||||
|
{ id: "contract", label: "签约择吉", icon: "📝" },
|
||||||
|
{ id: "other", label: "其他择吉", icon: "✍️" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleDateConfirm = (displayVal: string, apiVal: string) => {
|
||||||
|
form.birthDateDisplay = displayVal;
|
||||||
|
form.birthDateApi = apiVal;
|
||||||
|
showDatePicker.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartDateConfirm = (displayVal: string, apiVal: string) => {
|
||||||
|
form.dateRangeStartDisplay = displayVal;
|
||||||
|
// 从API格式中提取日期部分 (YYYY-MM-DD)
|
||||||
|
form.dateRangeStart = apiVal.split(' ')[0];
|
||||||
|
showStartDatePicker.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEndDateConfirm = (displayVal: string, apiVal: string) => {
|
||||||
|
form.dateRangeEndDisplay = displayVal;
|
||||||
|
// 从API格式中提取日期部分 (YYYY-MM-DD)
|
||||||
|
form.dateRangeEnd = apiVal.split(' ')[0];
|
||||||
|
showEndDatePicker.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (form.eventType === "other" && !form.customEvent.trim()) {
|
||||||
|
uni.showToast({ title: "请输入您要求测的事项", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!form.name || !form.birthDateDisplay) {
|
||||||
|
uni.showToast({ title: "请填写真实信息以确保准确", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!form.birthPlace) {
|
||||||
|
uni.showToast({ title: "请输入出生地", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!form.zejiPurpose) {
|
||||||
|
uni.showToast({ title: "请输入择吉目的", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!form.dateRangeStart || !form.dateRangeEnd) {
|
||||||
|
uni.showToast({ title: "请选择期望日期范围", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showLoading({ title: '测算中...', mask: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestData: BaziZejiCalculateRequest = {
|
||||||
|
name: form.name,
|
||||||
|
gender: form.gender as 'male' | 'female',
|
||||||
|
birth_date: form.birthDateDisplay,
|
||||||
|
birth_date_api: form.birthDateApi,
|
||||||
|
birth_place: form.birthPlace,
|
||||||
|
zeji_type: form.eventType as any,
|
||||||
|
zeji_purpose: form.eventType === 'other' ? form.customEvent : form.zejiPurpose,
|
||||||
|
date_range_start: form.dateRangeStart,
|
||||||
|
date_range_end: form.dateRangeEnd,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await baziZejiApi.calculateBaziZeji(requestData);
|
||||||
|
|
||||||
|
uni.hideLoading();
|
||||||
|
emit('submit', result);
|
||||||
|
} catch (error: any) {
|
||||||
|
uni.hideLoading();
|
||||||
|
// 如果是认证失败错误,不显示toast(因为API函数中已经处理了跳转)
|
||||||
|
if (error.message !== '认证失败,请登录后再试') {
|
||||||
|
uni.showToast({
|
||||||
|
title: error.message || '测算失败,请稍后重试',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.auspicious-form {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f0efe9;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-texture {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.4;
|
||||||
|
mix-blend-mode: multiply;
|
||||||
|
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 固定头部容器 */
|
||||||
|
.auspicious-fixed-header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: #f0efe9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态栏占位 */
|
||||||
|
.status-bar-placeholder {
|
||||||
|
height: var(--status-bar-height, 0);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部占位 */
|
||||||
|
.auspicious-header-placeholder {
|
||||||
|
height: calc(var(--status-bar-height, 0) + 100rpx);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid #eaddcf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-back {
|
||||||
|
padding: 16rpx;
|
||||||
|
margin-left: -8rpx;
|
||||||
|
color: #5a5a5a;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c2c2c;
|
||||||
|
letter-spacing: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-header-spacer {
|
||||||
|
width: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-scroll {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-container {
|
||||||
|
max-width: 700rpx;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-intro {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-intro-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c2c2c;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-intro-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5a5a5a;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-section {
|
||||||
|
margin-bottom: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c2c2c;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
padding-left: 12rpx;
|
||||||
|
border-left: 4rpx solid #8b2323;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 20rpx;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-event-card {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 24rpx 16rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
background: #fffdf9;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
color: #5a5a5a;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-event-card.is-active {
|
||||||
|
background: #8b2323;
|
||||||
|
border-color: #8b2323;
|
||||||
|
color: #fdfbf7;
|
||||||
|
box-shadow: 0 10rpx 16rpx -4rpx rgba(139, 35, 35, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-event-icon {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-event-text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-custom {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-input {
|
||||||
|
width: 100%;
|
||||||
|
background: #fffdf9;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 14rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2c2c2c;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-input::placeholder {
|
||||||
|
color: #bfbfbf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-card {
|
||||||
|
background: #fffdf9;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
box-shadow: 0 6rpx 12rpx -4rpx rgba(0, 0, 0, 0.08);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-field-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-field-input {
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
padding: 14rpx 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2c2c2c;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-field-input::placeholder {
|
||||||
|
color: #bfbfbf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 120rpx;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
padding: 14rpx 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2c2c2c;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-textarea::placeholder {
|
||||||
|
color: #bfbfbf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-gender {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-gender-btn {
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
background: transparent;
|
||||||
|
color: #5a5a5a;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-gender-btn.is-active {
|
||||||
|
background: #2c2c2c;
|
||||||
|
color: #d4af37;
|
||||||
|
border-color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-date-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
padding: 14rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-date-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-date-text.is-placeholder {
|
||||||
|
color: #bfbfbf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-date-arrow {
|
||||||
|
color: #8b2323;
|
||||||
|
font-size: 24rpx;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-footer {
|
||||||
|
padding: 32rpx;
|
||||||
|
background: #fdfbf7;
|
||||||
|
border-top: 1px solid #e5e5e5;
|
||||||
|
position: relative;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-submit {
|
||||||
|
width: 100%;
|
||||||
|
background: #8b2323;
|
||||||
|
color: #fdfbf7;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 18rpx 0;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
box-shadow: 0 12rpx 18rpx -8rpx rgba(139, 35, 35, 0.35);
|
||||||
|
font-size: 16px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auspicious-footer-tip {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
320
前端源码/uni-app/components/screens/AuspiciousLoading.vue
Normal file
320
前端源码/uni-app/components/screens/AuspiciousLoading.vue
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
<template>
|
||||||
|
<view class="aus-load">
|
||||||
|
<view class="aus-load-bg"></view>
|
||||||
|
<view class="aus-compass-wrapper">
|
||||||
|
<view class="aus-compass">
|
||||||
|
<view class="compass-glow"></view>
|
||||||
|
<view class="compass-svg-wrap">
|
||||||
|
<!-- 外圈虚线圆 -->
|
||||||
|
<view class="circle-outer"></view>
|
||||||
|
<view class="circle-main"></view>
|
||||||
|
|
||||||
|
<!-- 天干地支 -->
|
||||||
|
<view
|
||||||
|
v-for="(char, i) in runes"
|
||||||
|
:key="'rune-' + i"
|
||||||
|
class="rune-char"
|
||||||
|
:class="{ active: i === activeCharIndex }"
|
||||||
|
:style="getRuneStyle(i)"
|
||||||
|
>
|
||||||
|
{{ char }}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 八卦符号 - 旋转圈 -->
|
||||||
|
<view class="bagua-circle" :style="{ transform: 'rotate(' + baGuaRotation + 'deg)' }">
|
||||||
|
<view
|
||||||
|
v-for="(gua, i) in baGua"
|
||||||
|
:key="'gua-' + i"
|
||||||
|
class="bagua-char"
|
||||||
|
:style="getBaGuaStyle(i)"
|
||||||
|
>
|
||||||
|
{{ gua }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 中心罗盘指针 -->
|
||||||
|
<view class="compass-pointer" :style="{ transform: 'rotate(' + pointerRotation + 'deg)' }">
|
||||||
|
<view class="pointer-bar"></view>
|
||||||
|
<view class="pointer-dot"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载文本 -->
|
||||||
|
<view class="loading-text">
|
||||||
|
<view class="loading-title">择吉推演中</view>
|
||||||
|
<view class="loading-subtitle">结合八字 · 排盘择吉 · 避讳冲煞</view>
|
||||||
|
<view class="loading-tip">分析时间预计1-2分钟,可点击返回按钮退出,并在我的方案中查看结果</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
|
|
||||||
|
const runes = [
|
||||||
|
"甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸",
|
||||||
|
"子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"
|
||||||
|
];
|
||||||
|
|
||||||
|
const baGua = ["☰", "☱", "☲", "☳", "☴", "☵", "☶", "☷"];
|
||||||
|
|
||||||
|
const activeCharIndex = ref(-1);
|
||||||
|
const baGuaRotation = ref(0);
|
||||||
|
const pointerRotation = ref(0);
|
||||||
|
|
||||||
|
let runeInterval: number | null = null;
|
||||||
|
let baGuaInterval: number | null = null;
|
||||||
|
let pointerInterval: number | null = null;
|
||||||
|
|
||||||
|
const getRuneStyle = (index: number) => {
|
||||||
|
const angle = (index * 360) / runes.length;
|
||||||
|
const radius = 240;
|
||||||
|
const angleRad = (angle - 90) * (Math.PI / 180);
|
||||||
|
const x = 300 + radius * Math.cos(angleRad);
|
||||||
|
const y = 300 + radius * Math.sin(angleRad);
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: x + "rpx",
|
||||||
|
top: y + "rpx",
|
||||||
|
transform: `translate(-50%, -50%) rotate(${angle}deg)`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBaGuaStyle = (index: number) => {
|
||||||
|
const angle = (index * 360) / 8;
|
||||||
|
const radius = 145;
|
||||||
|
const angleRad = (angle - 90) * (Math.PI / 180);
|
||||||
|
const x = 180 + radius * Math.cos(angleRad);
|
||||||
|
const y = 180 + radius * Math.sin(angleRad);
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: x + "rpx",
|
||||||
|
top: y + "rpx",
|
||||||
|
transform: `translate(-50%, -50%) rotate(${angle}deg)`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
let count = 0;
|
||||||
|
runeInterval = setInterval(() => {
|
||||||
|
activeCharIndex.value = count % runes.length;
|
||||||
|
count++;
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
baGuaInterval = setInterval(() => {
|
||||||
|
baGuaRotation.value += 0.25;
|
||||||
|
}, 30);
|
||||||
|
|
||||||
|
let pointerCount = 0;
|
||||||
|
pointerInterval = setInterval(() => {
|
||||||
|
pointerCount += 1;
|
||||||
|
pointerRotation.value = pointerCount * 1.2;
|
||||||
|
}, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (runeInterval) clearInterval(runeInterval);
|
||||||
|
if (baGuaInterval) clearInterval(baGuaInterval);
|
||||||
|
if (pointerInterval) clearInterval(pointerInterval);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.aus-load {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #1a1a1a;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aus-load-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||||||
|
opacity: 0.08;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aus-compass-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 96rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aus-compass {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 60vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 512rpx;
|
||||||
|
min-height: 512rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-glow {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #d4af37;
|
||||||
|
filter: blur(60rpx);
|
||||||
|
opacity: 0.2;
|
||||||
|
animation: pulse 4s ease-in-out infinite;
|
||||||
|
max-width: 80vw;
|
||||||
|
max-height: 80vw;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.15;
|
||||||
|
transform: translate(-50%, -50%) scale(0.95);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.35;
|
||||||
|
transform: translate(-50%, -50%) scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-svg-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 80vw;
|
||||||
|
height: 80vw;
|
||||||
|
max-width: 600rpx;
|
||||||
|
max-height: 600rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-outer {
|
||||||
|
position: absolute;
|
||||||
|
left: 10rpx;
|
||||||
|
top: 10rpx;
|
||||||
|
width: 580rpx;
|
||||||
|
height: 580rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1rpx dashed #d4af37;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-main {
|
||||||
|
position: absolute;
|
||||||
|
left: 20rpx;
|
||||||
|
top: 20rpx;
|
||||||
|
width: 560rpx;
|
||||||
|
height: 560rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2rpx solid #d4af37;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rune-char {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #888;
|
||||||
|
font-family: SimSun, serif;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rune-char.active {
|
||||||
|
color: #d4af37;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 0 0 10rpx #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bagua-circle {
|
||||||
|
position: absolute;
|
||||||
|
left: 120rpx;
|
||||||
|
top: 120rpx;
|
||||||
|
width: 360rpx;
|
||||||
|
height: 360rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2rpx solid rgba(212, 175, 55, 0.15);
|
||||||
|
transition: transform 0.3s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bagua-char {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #d4af37;
|
||||||
|
opacity: 0.7;
|
||||||
|
font-family: SimSun, serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-pointer {
|
||||||
|
position: absolute;
|
||||||
|
left: 300rpx;
|
||||||
|
top: 300rpx;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
transition: transform 0.05s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointer-bar {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: -160rpx;
|
||||||
|
width: 4rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
background-color: #8b2323;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointer-dot {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 12rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #d4af37;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
animation: fade 2.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade {
|
||||||
|
0%, 100% { opacity: 0.3; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-title {
|
||||||
|
color: #d4af37;
|
||||||
|
letter-spacing: 0.4em;
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 0 0 20rpx rgba(212, 175, 55, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-subtitle {
|
||||||
|
color: #888;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-tip {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
padding: 0 48rpx;
|
||||||
|
color: rgba(226, 226, 226, 0.72);
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1369
前端源码/uni-app/components/screens/AuspiciousResult.vue
Normal file
1369
前端源码/uni-app/components/screens/AuspiciousResult.vue
Normal file
File diff suppressed because it is too large
Load Diff
608
前端源码/uni-app/components/screens/Calendar.vue
Normal file
608
前端源码/uni-app/components/screens/Calendar.vue
Normal file
@@ -0,0 +1,608 @@
|
|||||||
|
<template>
|
||||||
|
<view class="calendar-screen">
|
||||||
|
<view class="calendar-texture"></view>
|
||||||
|
|
||||||
|
<!-- 固定头部 -->
|
||||||
|
<view class="calendar-fixed-header">
|
||||||
|
<!-- 状态栏占位 -->
|
||||||
|
<view class="status-bar-placeholder"></view>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<view class="calendar-header">
|
||||||
|
<view class="calendar-back" @click="$emit('back')">‹</view>
|
||||||
|
<view class="calendar-title">
|
||||||
|
<text class="calendar-year">{{ year }}年 {{ lunarMonths[month] }}</text>
|
||||||
|
<text class="calendar-subtitle">Year of the Dragon</text>
|
||||||
|
</view>
|
||||||
|
<view class="calendar-header-spacer"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 头部占位 -->
|
||||||
|
<view class="calendar-header-placeholder"></view>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<scroll-view scroll-y class="calendar-scroll">
|
||||||
|
<!-- Controls -->
|
||||||
|
<view class="calendar-controls">
|
||||||
|
<button class="calendar-nav-button" @click="prevMonth">‹</button>
|
||||||
|
<text class="calendar-month">
|
||||||
|
{{ month + 1 }} <text class="calendar-month-unit">月</text>
|
||||||
|
</text>
|
||||||
|
<button class="calendar-nav-button" @click="nextMonth">›</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Weekdays -->
|
||||||
|
<view class="calendar-weekdays">
|
||||||
|
<text v-for="(d, i) in weekdays" :key="i" class="calendar-weekday"
|
||||||
|
:class="{ 'calendar-weekday-weekend': i === 0 || i === 6 }">
|
||||||
|
{{ d }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Calendar Grid -->
|
||||||
|
<view class="calendar-grid">
|
||||||
|
<view v-for="cell in calendarCells" :key="cell.key" class="calendar-cell"
|
||||||
|
:class="cell.day ? ['calendar-cell-filled', cellSelected(cell.day) ? 'is-selected' : '', cellToday(cell.day) ? 'is-today' : ''] : 'calendar-cell-empty'"
|
||||||
|
@click="cell.day && selectDay(cell.day)">
|
||||||
|
<template v-if="cell.day">
|
||||||
|
<text class="calendar-cell-day"
|
||||||
|
:class="{ 'is-selected': cellSelected(cell.day), 'is-today': cellToday(cell.day) }">
|
||||||
|
{{ cell.day }}
|
||||||
|
</text>
|
||||||
|
<text class="calendar-cell-lunar" :class="{ 'is-selected': cellSelected(cell.day) }">
|
||||||
|
{{ lunarDay(cell.day) }}
|
||||||
|
</text>
|
||||||
|
<view v-if="cellToday(cell.day) && !cellSelected(cell.day)" class="calendar-today-dot"></view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Detail Card -->
|
||||||
|
<view class="calendar-detail-wrapper">
|
||||||
|
<view class="calendar-detail">
|
||||||
|
<view class="calendar-detail-corner"></view>
|
||||||
|
<view class="calendar-detail-header">
|
||||||
|
<view class="calendar-detail-date">
|
||||||
|
<text class="calendar-detail-day">{{ selected.getDate() }}</text>
|
||||||
|
<view class="calendar-detail-meta">
|
||||||
|
<text class="calendar-detail-lunar">{{ lunarDay(selected.getDate()) }}</text>
|
||||||
|
<text class="calendar-detail-weekday">{{ weekdayText(selected.getDay()) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="calendar-detail-badge">今日运势</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="calendar-detail-body">
|
||||||
|
<view class="calendar-detail-row">
|
||||||
|
<view class="calendar-detail-icon calendar-detail-icon-yi">宜</view>
|
||||||
|
<view class="calendar-detail-tags">
|
||||||
|
<text v-for="(item, i) in yiJi.yi" :key="i" class="calendar-detail-tag">{{ item }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="calendar-detail-row">
|
||||||
|
<view class="calendar-detail-icon calendar-detail-icon-ji">忌</view>
|
||||||
|
<view class="calendar-detail-tags">
|
||||||
|
<text v-for="(item, i) in yiJi.ji" :key="i" class="calendar-detail-tag calendar-detail-tag-ji">{{ item
|
||||||
|
}}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<view class="calendar-cta">
|
||||||
|
<button class="calendar-cta-card" @click="$emit('auspicious')">
|
||||||
|
<view class="calendar-cta-glow"></view>
|
||||||
|
<view class="calendar-cta-content">
|
||||||
|
<view>
|
||||||
|
<text class="calendar-cta-title">精准八字择吉</text>
|
||||||
|
<text class="calendar-cta-subtitle">结婚 · 开业 · 乔迁 · 动土</text>
|
||||||
|
</view>
|
||||||
|
<view class="calendar-cta-arrow">›</view>
|
||||||
|
</view>
|
||||||
|
<view class="calendar-cta-tags">
|
||||||
|
<text class="calendar-cta-tag">个人定制</text>
|
||||||
|
<text class="calendar-cta-tag">避讳冲煞</text>
|
||||||
|
</view>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
onBack?: () => void;
|
||||||
|
onNavigateToAuspicious?: () => void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const weekdays = ["日", "一", "二", "三", "四", "五", "六"];
|
||||||
|
const lunarMonths = ["正月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "冬月", "腊月"];
|
||||||
|
const lunarDays = [
|
||||||
|
"初一", "初二", "初三", "初四", "初五", "初六", "初七", "初八", "初九", "初十",
|
||||||
|
"十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十",
|
||||||
|
"廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九", "三十"
|
||||||
|
];
|
||||||
|
const yiJiData = [
|
||||||
|
{ yi: ["出行", "开市", "交易", "裁衣", "安床"], ji: ["动土", "安葬", "破土", "作灶", "入宅"] },
|
||||||
|
{ yi: ["嫁娶", "订盟", "纳采", "祭祀", "祈福"], ji: ["开仓", "出货", "盖屋", "造桥", "破土"] },
|
||||||
|
{ yi: ["解除", "扫舍", "整手足甲", "沐浴"], ji: ["安门", "分居", "修造", "动土"] },
|
||||||
|
{ yi: ["塑绘", "开光", "进人口", "纳畜"], ji: ["嫁娶", "安葬", "行丧", "伐木"] },
|
||||||
|
{ yi: ["祭祀", "会亲友", "纳财", "捕捉"], ji: ["嫁娶", "开市", "安床", "探病"] }
|
||||||
|
];
|
||||||
|
|
||||||
|
const current = ref(new Date());
|
||||||
|
const selected = ref(new Date());
|
||||||
|
|
||||||
|
const year = computed(() => current.value.getFullYear());
|
||||||
|
const month = computed(() => current.value.getMonth());
|
||||||
|
|
||||||
|
const daysInMonth = computed(() => new Date(year.value, month.value + 1, 0).getDate());
|
||||||
|
const firstDayOfMonth = computed(() => new Date(year.value, month.value, 1).getDay());
|
||||||
|
|
||||||
|
const calendarCells = computed(() => {
|
||||||
|
const cells: { key: string; day?: number }[] = [];
|
||||||
|
for (let i = 0; i < firstDayOfMonth.value; i++) {
|
||||||
|
cells.push({ key: `empty-${i}` });
|
||||||
|
}
|
||||||
|
for (let d = 1; d <= daysInMonth.value; d++) {
|
||||||
|
cells.push({ key: `d-${d}`, day: d });
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
});
|
||||||
|
|
||||||
|
const lunarDay = (d: number) => lunarDays[(d - 1) % 30];
|
||||||
|
const getYiJi = (d: number) => yiJiData[d % yiJiData.length];
|
||||||
|
const yiJi = computed(() => getYiJi(selected.value.getDate()));
|
||||||
|
const weekdayText = (d: number) => ["周日", "周一", "周二", "周三", "周四", "周五", "周六"][d];
|
||||||
|
|
||||||
|
const selectDay = (d: number) => {
|
||||||
|
selected.value = new Date(year.value, month.value, d);
|
||||||
|
};
|
||||||
|
const prevMonth = () => {
|
||||||
|
current.value = new Date(year.value, month.value - 1, 1);
|
||||||
|
if (month.value === selected.value.getMonth()) {
|
||||||
|
selected.value = new Date(year.value, month.value, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const nextMonth = () => {
|
||||||
|
current.value = new Date(year.value, month.value + 1, 1);
|
||||||
|
if (month.value === selected.value.getMonth()) {
|
||||||
|
selected.value = new Date(year.value, month.value + 1, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cellSelected = (d: number) => selected.value.getDate() === d && selected.value.getMonth() === month.value;
|
||||||
|
const cellToday = (d: number) => {
|
||||||
|
const today = new Date();
|
||||||
|
return today.getFullYear() === year.value && today.getMonth() === month.value && today.getDate() === d;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.calendar-screen {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fdfbf7;
|
||||||
|
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 固定头部容器 */
|
||||||
|
.calendar-fixed-header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: #fdfbf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态栏占位 */
|
||||||
|
.status-bar-placeholder {
|
||||||
|
height: var(--status-bar-height, 0);
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部占位,防止内容被固定头部遮挡 */
|
||||||
|
.calendar-header-placeholder {
|
||||||
|
height: calc(var(--status-bar-height, 0) + 120rpx);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-texture {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0.1;
|
||||||
|
pointer-events: none;
|
||||||
|
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 28rpx 32rpx;
|
||||||
|
border-bottom: 1px solid #eaddcf;
|
||||||
|
background: rgba(255, 253, 249, 0.85);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-back {
|
||||||
|
padding: 16rpx;
|
||||||
|
margin-left: -12rpx;
|
||||||
|
color: #5a5a5a;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-year {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c2c2c;
|
||||||
|
letter-spacing: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-subtitle {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-header-spacer {
|
||||||
|
width: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-scroll {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
padding-bottom: 80rpx;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24rpx 48rpx 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-nav-button {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #eaddcf;
|
||||||
|
color: #8b2323;
|
||||||
|
background: #fffdf9;
|
||||||
|
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.04);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 36rpx;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-month {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-month-unit {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-weekdays {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-weekday {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #5a5a5a;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-weekday-weekend {
|
||||||
|
color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
padding: 0 32rpx;
|
||||||
|
gap: 12rpx 0;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cell {
|
||||||
|
height: 112rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cell-filled {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cell-filled.is-selected {
|
||||||
|
background: #8b2323;
|
||||||
|
box-shadow: 0 6rpx 12rpx rgba(139, 35, 35, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cell-filled.is-today:not(.is-selected) {
|
||||||
|
background: rgba(139, 35, 35, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cell-day {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c2c2c;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cell-day.is-selected {
|
||||||
|
color: #fdfbf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cell-day.is-today:not(.is-selected) {
|
||||||
|
color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cell-lunar {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cell-lunar.is-selected {
|
||||||
|
color: rgba(253, 251, 247, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-today-dot {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 6rpx;
|
||||||
|
width: 8rpx;
|
||||||
|
height: 8rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-wrapper {
|
||||||
|
padding: 0 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail {
|
||||||
|
background: #fffdf9;
|
||||||
|
border: 1px solid #eaddcf;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
box-shadow: 0 12rpx 20rpx -8rpx rgba(0, 0, 0, 0.12);
|
||||||
|
padding: 32rpx 32rpx 28rpx;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-corner {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
background: rgba(139, 35, 35, 0.05);
|
||||||
|
border-bottom-left-radius: 160rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
border-bottom: 1px solid #eaddcf;
|
||||||
|
padding-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-date {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-day {
|
||||||
|
font-size: 44px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c2c2c;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-lunar {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-weekday {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6rpx 12rpx;
|
||||||
|
background: #8b2323;
|
||||||
|
color: #fdfbf7;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-icon {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #2c2c2c;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-icon-yi {
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-icon-ji {
|
||||||
|
border-color: #8b2323;
|
||||||
|
color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding-top: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-tag {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #5a5a5a;
|
||||||
|
padding: 6rpx 12rpx;
|
||||||
|
background: #f7f2ea;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-tag-ji {
|
||||||
|
color: #8b2323;
|
||||||
|
background: rgba(139, 35, 35, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cta {
|
||||||
|
padding: 24rpx 32rpx 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cta-card {
|
||||||
|
width: 100%;
|
||||||
|
background: #2c2c2c;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 12rpx 24rpx rgba(0, 0, 0, 0.18);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cta-glow {
|
||||||
|
position: absolute;
|
||||||
|
top: -20rpx;
|
||||||
|
right: -20rpx;
|
||||||
|
width: 180rpx;
|
||||||
|
height: 180rpx;
|
||||||
|
background: #d4af37;
|
||||||
|
opacity: 0.25;
|
||||||
|
filter: blur(40rpx);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cta-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cta-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #d4af37;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cta-subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(242, 230, 216, 0.8);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cta-arrow {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(212, 175, 55, 0.15);
|
||||||
|
color: #d4af37;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cta-tags {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-cta-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
padding: 6rpx 12rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1526
前端源码/uni-app/components/screens/CompanyAnalysis.vue
Normal file
1526
前端源码/uni-app/components/screens/CompanyAnalysis.vue
Normal file
File diff suppressed because it is too large
Load Diff
2116
前端源码/uni-app/components/screens/CompanyBusinessFortune.vue
Normal file
2116
前端源码/uni-app/components/screens/CompanyBusinessFortune.vue
Normal file
File diff suppressed because it is too large
Load Diff
2328
前端源码/uni-app/components/screens/CompanyNamingDetail.vue
Normal file
2328
前端源码/uni-app/components/screens/CompanyNamingDetail.vue
Normal file
File diff suppressed because it is too large
Load Diff
823
前端源码/uni-app/components/screens/CompanyNamingDetailDesktop.vue
Normal file
823
前端源码/uni-app/components/screens/CompanyNamingDetailDesktop.vue
Normal file
@@ -0,0 +1,823 @@
|
|||||||
|
<template>
|
||||||
|
<view class="company-desktop-detail">
|
||||||
|
<view class="detail-header">
|
||||||
|
<view class="detail-header-back" @click="emit('back')">
|
||||||
|
<text class="back-icon">‹</text>
|
||||||
|
<text class="back-text">返回</text>
|
||||||
|
</view>
|
||||||
|
<text class="detail-header-title">公司测名详解</text>
|
||||||
|
<view class="detail-header-placeholder" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="detail-content">
|
||||||
|
<view class="report-body">
|
||||||
|
<!-- header -->
|
||||||
|
<view
|
||||||
|
class="section"
|
||||||
|
:class="{ 'section--click': hasNodes(header.details?.nodes) }"
|
||||||
|
@click="hasNodes(header.details?.nodes) && openDetail(header.details?.title || '总分详解', header.details?.nodes)"
|
||||||
|
>
|
||||||
|
<text class="section-label">header · 总分</text>
|
||||||
|
<view class="score-row">
|
||||||
|
<text class="name">{{ toText(header.name) || '—' }}</text>
|
||||||
|
<text class="score">{{ num(header.score, 0) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="tag-row">
|
||||||
|
<text class="pill">{{ toText(header.tagLeft) }}</text>
|
||||||
|
<text class="pill">{{ toText(header.tagRight) }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="body-text">{{ toText(header.quote) }}</text>
|
||||||
|
<text v-if="hasNodes(header.details?.nodes)" class="section-hint">点击本段查看「{{ toText(header.details?.title) || '详解' }}」</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- characterAnalysis -->
|
||||||
|
<view
|
||||||
|
class="section"
|
||||||
|
:class="{ 'section--click': hasNodes(characterAnalysis.details?.nodes) }"
|
||||||
|
@click="
|
||||||
|
hasNodes(characterAnalysis.details?.nodes) &&
|
||||||
|
openDetail(characterAnalysis.details?.title || '字义数理详解', characterAnalysis.details?.nodes)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<text class="section-label">characterAnalysis · 字义数理</text>
|
||||||
|
<view class="char-grid">
|
||||||
|
<view v-for="(it, idx) in arr(characterAnalysis.characters)" :key="idx" class="char-box">
|
||||||
|
<text class="char-single">{{ toText(it?.char) }}</text>
|
||||||
|
<text class="char-meta">{{ toText(it?.element) }} · {{ num(it?.stroke, 0) }}画</text>
|
||||||
|
<text class="body-text small">{{ toText(it?.meaning) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="body-text">{{ toText(characterAnalysis.analysis) }}</text>
|
||||||
|
<text v-if="hasNodes(characterAnalysis.details?.nodes)" class="section-hint">
|
||||||
|
点击本段查看「{{ toText(characterAnalysis.details?.title) || '详细拆解' }}」
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- businessPattern -->
|
||||||
|
<view class="section">
|
||||||
|
<text class="section-label">businessPattern · 商业格局</text>
|
||||||
|
<SixDimensionRadarDesktopEchart
|
||||||
|
:labels="arr(businessPattern.radar?.labels)"
|
||||||
|
:values="arr(businessPattern.radar?.values)"
|
||||||
|
:remark="summaryText"
|
||||||
|
/>
|
||||||
|
<view v-if="arr(businessPattern.summary).length" class="kv-inline">
|
||||||
|
<view v-for="(s, si) in arr(businessPattern.summary)" :key="si" class="summary-chip">
|
||||||
|
<text class="chip-k">{{ toText(s?.label) }}</text>
|
||||||
|
<text class="chip-v">{{ toText(s?.value) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="hasNodes(businessPattern.details?.nodes)"
|
||||||
|
class="section-link"
|
||||||
|
:class="{ 'section--click': true }"
|
||||||
|
@click="openDetail(businessPattern.details?.title || '商业六维详解', businessPattern.details?.nodes)"
|
||||||
|
>
|
||||||
|
<text>{{ toText(businessPattern.details?.title) || '商业六维 · 解释与建议' }} ›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- gua -->
|
||||||
|
<view
|
||||||
|
class="section"
|
||||||
|
:class="{ 'section--click': hasNodes(gua.details?.nodes) }"
|
||||||
|
@click="hasNodes(gua.details?.nodes) && openDetail(gua.details?.title || '卦象解读', gua.details?.nodes)"
|
||||||
|
>
|
||||||
|
<text class="section-label">gua · 卦象</text>
|
||||||
|
<text v-if="toText(gua.bg)" class="gua-bg">卦字:{{ toText(gua.bg) }}</text>
|
||||||
|
<text class="headline">{{ toText(gua.name) }} · {{ toText(gua.badge) }}</text>
|
||||||
|
<text class="body-text">{{ toText(gua.desc) }}</text>
|
||||||
|
<view v-if="arr(gua.tags).length" class="tag-list">
|
||||||
|
<text v-for="(t, ti) in arr(gua.tags)" :key="ti" class="pill pill--soft">{{ toText(t) }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="body-text">{{ toText(gua.insight) }}</text>
|
||||||
|
<text v-if="hasNodes(gua.details?.nodes)" class="section-hint">点击本段查看卦象详解</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- team -->
|
||||||
|
<view
|
||||||
|
class="section"
|
||||||
|
:class="{ 'section--click': hasNodes(team.details?.nodes) }"
|
||||||
|
@click="hasNodes(team.details?.nodes) && openDetail(team.details?.title || '团队契合', team.details?.nodes)"
|
||||||
|
>
|
||||||
|
<text class="section-label">team · 团队契合</text>
|
||||||
|
<view v-for="(m, mi) in arr(team.members)" :key="mi" class="member-block">
|
||||||
|
<text class="member-line">
|
||||||
|
{{ toText(m?.role) }} · {{ num(m?.score, 0) }}分 · {{ toText(m?.match) }}
|
||||||
|
</text>
|
||||||
|
<text v-if="toText(m?.desc)" class="body-text small">{{ toText(m?.desc) }}</text>
|
||||||
|
</view>
|
||||||
|
<text v-if="toText(team.note)" class="body-text note">{{ toText(team.note) }}</text>
|
||||||
|
<text v-if="hasNodes(team.details?.nodes)" class="section-hint">点击本段查看团队说明</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- years -->
|
||||||
|
<view
|
||||||
|
class="section"
|
||||||
|
:class="{ 'section--click': hasNodes(years.details?.nodes) }"
|
||||||
|
@click="hasNodes(years.details?.nodes) && openDetail(years.details?.title || '流年运势', years.details?.nodes)"
|
||||||
|
>
|
||||||
|
<text class="section-label">years · 流年</text>
|
||||||
|
<view v-for="(y, yi) in arr(years.items)" :key="yi" class="year-row">
|
||||||
|
<text class="year-key">{{ toText(y?.year) }}</text>
|
||||||
|
<text class="year-luck">{{ toText(y?.luck) }}</text>
|
||||||
|
<text class="year-text">{{ toText(y?.text) }}</text>
|
||||||
|
</view>
|
||||||
|
<text v-if="hasNodes(years.details?.nodes)" class="section-hint">点击本段查看流年预警/建议</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- wealthTrend -->
|
||||||
|
<view
|
||||||
|
class="section"
|
||||||
|
:class="{ 'section--click': hasNodes(wealthTrend.details?.nodes) }"
|
||||||
|
@click="hasNodes(wealthTrend.details?.nodes) && openDetail(wealthTrend.details?.title || '财运走势', wealthTrend.details?.nodes)"
|
||||||
|
>
|
||||||
|
<text class="section-label">wealthTrend · 财运走势</text>
|
||||||
|
<view class="bars">
|
||||||
|
<view v-for="(v, vi) in arr(wealthTrend.bars)" :key="vi" class="bar-wrap">
|
||||||
|
<view class="bar" :style="{ height: `${Math.max(8, Math.min(100, num(v, 0)))}%` }" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="body-text">{{ toText(wealthTrend.note) }}</text>
|
||||||
|
<text v-if="hasNodes(wealthTrend.details?.nodes)" class="section-hint">点击本段查看走势建议</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- direction -->
|
||||||
|
<view
|
||||||
|
class="section"
|
||||||
|
:class="{ 'section--click': hasNodes(direction.details?.nodes) }"
|
||||||
|
@click="hasNodes(direction.details?.nodes) && openDetail(direction.details?.title || '吉凶方位', direction.details?.nodes)"
|
||||||
|
>
|
||||||
|
<text class="section-label">direction · 方位</text>
|
||||||
|
<text class="body-text">{{ toText(direction.note) }}</text>
|
||||||
|
<text v-if="direction.goodDot && (direction.goodDot.x != null || direction.goodDot.y != null)" class="body-text small">
|
||||||
|
吉位参考点(相对坐标):x {{ num(direction.goodDot?.x, 0) }},y {{ num(direction.goodDot?.y, 0) }}
|
||||||
|
</text>
|
||||||
|
<text v-if="hasNodes(direction.details?.nodes)" class="section-hint">点击本段查看方位说明</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- layout -->
|
||||||
|
<view
|
||||||
|
class="section"
|
||||||
|
:class="{ 'section--click': hasNodes(layout.details?.nodes) }"
|
||||||
|
@click="hasNodes(layout.details?.nodes) && openDetail(layout.details?.title || '办公布局', layout.details?.nodes)"
|
||||||
|
>
|
||||||
|
<text class="section-label">layout · 办公布局</text>
|
||||||
|
<view v-for="(it, li) in arr(layout.items)" :key="li" class="layout-line">
|
||||||
|
<text class="layout-strong">{{ toText(it?.strong) }}</text>
|
||||||
|
<view class="layout-flow">
|
||||||
|
<text class="body-text small">{{ toText(it?.textBefore) }}</text>
|
||||||
|
<text v-for="(h, hi) in arr(it?.highlights)" :key="hi" class="highlight">「{{ toText(h) }}」</text>
|
||||||
|
<text class="body-text small">{{ toText(it?.textAfter) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text v-if="hasNodes(layout.details?.nodes)" class="section-hint">点击本段查看布局建议</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- execution -->
|
||||||
|
<view
|
||||||
|
class="section section--wide"
|
||||||
|
:class="{ 'section--click': hasNodes(execution.details?.nodes) }"
|
||||||
|
@click="hasNodes(execution.details?.nodes) && openDetail(execution.details?.title || '执行建议', execution.details?.nodes)"
|
||||||
|
>
|
||||||
|
<text class="section-label">execution · 执行建议</text>
|
||||||
|
<text class="body-text">{{ toText(execution.text) }}</text>
|
||||||
|
<text v-if="hasNodes(execution.details?.nodes)" class="section-hint">点击本段查看执行条目</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- liuyao -->
|
||||||
|
<view
|
||||||
|
v-if="hasLiuyao"
|
||||||
|
class="section"
|
||||||
|
:class="{ 'section--click': hasNodes(liuyao.details?.nodes) }"
|
||||||
|
@click="hasNodes(liuyao.details?.nodes) && openDetail('六爻', liuyao.details?.nodes)"
|
||||||
|
>
|
||||||
|
<text class="section-label">liuyao · 六爻</text>
|
||||||
|
<text class="headline">{{ toText(liuyao.hexagram_title) }}</text>
|
||||||
|
<text class="body-text">{{ toText(liuyao.changing_summary) }}</text>
|
||||||
|
<text class="body-text">{{ toText(liuyao.interpretation) }}</text>
|
||||||
|
<text v-for="(yl, yi) in arr(liuyao.yao_lines)" :key="yi" class="body-text small mono">· {{ toText(yl) }}</text>
|
||||||
|
<text v-if="hasNodes(liuyao.details?.nodes)" class="section-hint">点击本段查看六爻详解</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- wuxing_bagua -->
|
||||||
|
<view
|
||||||
|
v-if="hasWuxingBagua"
|
||||||
|
class="section"
|
||||||
|
:class="{ 'section--click': hasNodes(wuxingBagua.details?.nodes) }"
|
||||||
|
@click="hasNodes(wuxingBagua.details?.nodes) && openDetail('五行八卦', wuxingBagua.details?.nodes)"
|
||||||
|
>
|
||||||
|
<text class="section-label">wuxing_bagua · 五行八卦</text>
|
||||||
|
<text class="body-text">{{ toText(wuxingBagua.wuxing_sketch) }}</text>
|
||||||
|
<text class="body-text">{{ toText(wuxingBagua.bagua_profile) }}</text>
|
||||||
|
<text class="body-text">{{ toText(wuxingBagua.mutual_sketch) }}</text>
|
||||||
|
<text class="body-text strong-end">{{ toText(wuxingBagua.summary) }}</text>
|
||||||
|
<text v-if="hasNodes(wuxingBagua.details?.nodes)" class="section-hint">点击本段查看详解</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- zodiac_sign -->
|
||||||
|
<view
|
||||||
|
v-if="hasZodiac"
|
||||||
|
class="section"
|
||||||
|
:class="{ 'section--click': hasNodes(zodiacSign.details?.nodes) }"
|
||||||
|
@click="hasNodes(zodiacSign.details?.nodes) && openDetail('属相', zodiacSign.details?.nodes)"
|
||||||
|
>
|
||||||
|
<text class="section-label">zodiac_sign · 属相</text>
|
||||||
|
<text class="headline">
|
||||||
|
{{ toText(zodiacSign.animal_icon) }} {{ toText(zodiacSign.animal) }}({{ toText(zodiacSign.earthly_branch) }})
|
||||||
|
</text>
|
||||||
|
<text class="body-text">{{ toText(zodiacSign.trait_summary) }}</text>
|
||||||
|
<text class="body-text">{{ toText(zodiacSign.name_harmony) }}</text>
|
||||||
|
<text v-if="hasNodes(zodiacSign.details?.nodes)" class="section-hint">点击本段查看属相详解</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- career_plan -->
|
||||||
|
<view
|
||||||
|
v-if="hasCareerPlan"
|
||||||
|
class="section"
|
||||||
|
:class="{ 'section--click': hasNodes(careerPlan.details?.nodes) }"
|
||||||
|
@click="hasNodes(careerPlan.details?.nodes) && openDetail('事业规划', careerPlan.details?.nodes)"
|
||||||
|
>
|
||||||
|
<text class="section-label">career_plan · 事业规划</text>
|
||||||
|
<text class="body-text">{{ toText(careerPlan.summary) }}</text>
|
||||||
|
<view v-for="(ms, msi) in arr(careerPlan.milestones)" :key="msi" class="milestone">
|
||||||
|
<text class="milestone-title">{{ toText(ms?.phase) }}{{ toText(ms?.period) ? ' · ' + toText(ms.period) : '' }}</text>
|
||||||
|
<text v-if="toText(ms?.focus)" class="body-text small">重点:{{ toText(ms.focus) }}</text>
|
||||||
|
<text v-if="toText(ms?.advice)" class="body-text small">建议:{{ toText(ms.advice) }}</text>
|
||||||
|
</view>
|
||||||
|
<text v-if="hasNodes(careerPlan.details?.nodes)" class="section-hint">点击本段查看规划详解</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- lucky_numbers -->
|
||||||
|
<view
|
||||||
|
v-if="hasLuckyNumbers"
|
||||||
|
class="section"
|
||||||
|
:class="{ 'section--click': hasNodes(luckyNumbers.details?.nodes) }"
|
||||||
|
@click="hasNodes(luckyNumbers.details?.nodes) && openDetail('幸运数字', luckyNumbers.details?.nodes)"
|
||||||
|
>
|
||||||
|
<text class="section-label">lucky_numbers · 幸运数字</text>
|
||||||
|
<text class="headline">首推:{{ toText(luckyNumbers.primary) }}</text>
|
||||||
|
<text class="body-text">{{ arr(luckyNumbers.numbers).join('、') }}</text>
|
||||||
|
<text class="body-text">{{ toText(luckyNumbers.meaning) }}</text>
|
||||||
|
<text v-if="hasNodes(luckyNumbers.details?.nodes)" class="section-hint">点击本段查看数字详解</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- lucky_colors -->
|
||||||
|
<view
|
||||||
|
v-if="hasLuckyColors"
|
||||||
|
class="section"
|
||||||
|
:class="{ 'section--click': hasNodes(luckyColors.details?.nodes) }"
|
||||||
|
@click="hasNodes(luckyColors.details?.nodes) && openDetail('幸运色', luckyColors.details?.nodes)"
|
||||||
|
>
|
||||||
|
<text class="section-label">lucky_colors · 幸运色</text>
|
||||||
|
<text class="headline">主推:{{ toText(luckyColors.primary) }}</text>
|
||||||
|
<view class="color-row">
|
||||||
|
<view v-for="(c, ci) in arr(luckyColors.colors)" :key="ci" class="color-item">
|
||||||
|
<view class="color-swatch" :style="{ backgroundColor: toText(c?.hex) || '#333' }" />
|
||||||
|
<text class="body-text small">{{ toText(c?.name) }}{{ toText(c?.note) ? ' · ' + toText(c.note) : '' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="body-text">{{ toText(luckyColors.meaning) }}</text>
|
||||||
|
<text v-if="hasNodes(luckyColors.details?.nodes)" class="section-hint">点击本段查看用色详解</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view v-if="props.showBusinessFortune !== false" class="footer-action">
|
||||||
|
<button class="fortune-btn" type="button" @click="emit('businessFortune', detailData)">查看商业运势</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="showModal" class="modal-mask" @click="closeModal">
|
||||||
|
<view class="detail-modal" @click.stop>
|
||||||
|
<view class="detail-modal-card">
|
||||||
|
<text class="modal-title">{{ modalTitle }}</text>
|
||||||
|
<text class="close" @click="closeModal">×</text>
|
||||||
|
</view>
|
||||||
|
<scroll-view scroll-y class="detail-modal-body">
|
||||||
|
<view v-for="(node, idx) in modalNodes" :key="idx" class="node">
|
||||||
|
<text v-if="node?.type === 'text'" class="line">{{ toText(node.text) }}</text>
|
||||||
|
<view v-else-if="node?.type === 'list'">
|
||||||
|
<text v-for="(item, j) in arr(node.items)" :key="j" class="line">- {{ toText(item) }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="node?.type === 'kv'">
|
||||||
|
<text v-for="(item, j) in arr(node.items)" :key="j" class="line">{{ toText(item?.label) }}:{{ toText(item?.value) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import SixDimensionRadarDesktopEchart from "../SixDimensionRadarDesktopEchart.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: any;
|
||||||
|
showBusinessFortune?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
back: [];
|
||||||
|
businessFortune: [any];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const parseMaybeJson = (value: any) => {
|
||||||
|
if (value && typeof value === "object") return value;
|
||||||
|
if (typeof value !== "string") return {};
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const toText = (v: any) => String(v ?? "").trim();
|
||||||
|
const num = (v: any, fallback = 0) => {
|
||||||
|
const n = Number(v);
|
||||||
|
return Number.isFinite(n) ? n : fallback;
|
||||||
|
};
|
||||||
|
const arr = (v: any) => (Array.isArray(v) ? v : []);
|
||||||
|
const hasNodes = (nodes: any) => arr(nodes).length > 0;
|
||||||
|
|
||||||
|
const detailData = computed(() => parseMaybeJson(props.data));
|
||||||
|
const header = computed(() => detailData.value?.header || {});
|
||||||
|
const characterAnalysis = computed(() => detailData.value?.characterAnalysis || {});
|
||||||
|
const businessPattern = computed(() => detailData.value?.businessPattern || {});
|
||||||
|
const gua = computed(() => detailData.value?.gua || {});
|
||||||
|
const team = computed(() => detailData.value?.team || {});
|
||||||
|
const years = computed(() => detailData.value?.years || {});
|
||||||
|
const wealthTrend = computed(() => detailData.value?.wealthTrend || {});
|
||||||
|
const direction = computed(() => detailData.value?.direction || {});
|
||||||
|
const layout = computed(() => detailData.value?.layout || {});
|
||||||
|
const execution = computed(() => detailData.value?.execution || {});
|
||||||
|
|
||||||
|
const liuyao = computed(() => detailData.value?.liuyao || {});
|
||||||
|
const wuxingBagua = computed(() => detailData.value?.wuxing_bagua || {});
|
||||||
|
const zodiacSign = computed(() => detailData.value?.zodiac_sign || {});
|
||||||
|
const careerPlan = computed(() => detailData.value?.career_plan || {});
|
||||||
|
const luckyNumbers = computed(() => detailData.value?.lucky_numbers || {});
|
||||||
|
const luckyColors = computed(() => detailData.value?.lucky_colors || {});
|
||||||
|
|
||||||
|
const summaryText = computed(() =>
|
||||||
|
arr(businessPattern.value?.summary)
|
||||||
|
.map((x: any) => `${toText(x?.label)} ${toText(x?.value)}`)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" · "),
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasLiuyao = computed(
|
||||||
|
() =>
|
||||||
|
!!(
|
||||||
|
toText(liuyao.value?.hexagram_title) ||
|
||||||
|
toText(liuyao.value?.changing_summary) ||
|
||||||
|
toText(liuyao.value?.interpretation) ||
|
||||||
|
arr(liuyao.value?.yao_lines).length ||
|
||||||
|
hasNodes(liuyao.value?.details?.nodes)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const hasWuxingBagua = computed(
|
||||||
|
() =>
|
||||||
|
!!(
|
||||||
|
toText(wuxingBagua.value?.wuxing_sketch) ||
|
||||||
|
toText(wuxingBagua.value?.bagua_profile) ||
|
||||||
|
toText(wuxingBagua.value?.mutual_sketch) ||
|
||||||
|
toText(wuxingBagua.value?.summary) ||
|
||||||
|
hasNodes(wuxingBagua.value?.details?.nodes)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const hasZodiac = computed(
|
||||||
|
() =>
|
||||||
|
!!(
|
||||||
|
toText(zodiacSign.value?.animal) ||
|
||||||
|
toText(zodiacSign.value?.trait_summary) ||
|
||||||
|
toText(zodiacSign.value?.name_harmony) ||
|
||||||
|
hasNodes(zodiacSign.value?.details?.nodes)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const hasCareerPlan = computed(
|
||||||
|
() =>
|
||||||
|
!!(
|
||||||
|
toText(careerPlan.value?.summary) ||
|
||||||
|
arr(careerPlan.value?.milestones).length ||
|
||||||
|
hasNodes(careerPlan.value?.details?.nodes)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const hasLuckyNumbers = computed(
|
||||||
|
() =>
|
||||||
|
!!(toText(luckyNumbers.value?.primary) || arr(luckyNumbers.value?.numbers).length || toText(luckyNumbers.value?.meaning)),
|
||||||
|
);
|
||||||
|
const hasLuckyColors = computed(
|
||||||
|
() =>
|
||||||
|
!!(
|
||||||
|
toText(luckyColors.value?.primary) ||
|
||||||
|
arr(luckyColors.value?.colors).length ||
|
||||||
|
toText(luckyColors.value?.meaning)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const showModal = ref(false);
|
||||||
|
const modalTitle = ref("");
|
||||||
|
const modalNodes = ref<any[]>([]);
|
||||||
|
const openDetail = (title: string, nodes: any[]) => {
|
||||||
|
const list = arr(nodes);
|
||||||
|
if (!list.length) return;
|
||||||
|
modalTitle.value = toText(title) || "详情";
|
||||||
|
modalNodes.value = list;
|
||||||
|
showModal.value = true;
|
||||||
|
};
|
||||||
|
const closeModal = () => {
|
||||||
|
showModal.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.company-desktop-detail {
|
||||||
|
min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: linear-gradient(165deg, #070a12 0%, #12102a 42%, #0a1628 100%);
|
||||||
|
color: #e8e4dc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: rgba(15, 23, 42, 0.72);
|
||||||
|
border-bottom: 1px solid rgba(212, 175, 55, 0.2);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.detail-header-back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
.detail-header-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f2e6d8;
|
||||||
|
}
|
||||||
|
.detail-header-placeholder {
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-body {
|
||||||
|
max-width: 820px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 16px 18px 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(15, 23, 42, 0.52);
|
||||||
|
border: 1px solid rgba(212, 175, 55, 0.14);
|
||||||
|
border-left: 3px solid rgba(212, 175, 55, 0.45);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
.section--wide {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.section--click {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.section--click:active {
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: rgba(212, 175, 55, 0.75);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 商业六维内嵌 ECharts 组件自带外边距,报告式布局里收紧 */
|
||||||
|
:deep(.sixdim-section) {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #f2e6d8;
|
||||||
|
}
|
||||||
|
.score {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-row,
|
||||||
|
.tag-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.pill {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(212, 175, 55, 0.12);
|
||||||
|
border: 1px solid rgba(212, 175, 55, 0.28);
|
||||||
|
color: #ede6d8;
|
||||||
|
}
|
||||||
|
.pill--soft {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border-color: rgba(148, 163, 184, 0.28);
|
||||||
|
color: rgba(232, 228, 220, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: rgba(232, 228, 220, 0.92);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.body-text.small {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(232, 228, 220, 0.82);
|
||||||
|
}
|
||||||
|
.body-text.note {
|
||||||
|
font-style: italic;
|
||||||
|
color: rgba(212, 175, 55, 0.65);
|
||||||
|
}
|
||||||
|
.mono {
|
||||||
|
font-family: ui-monospace, monospace;
|
||||||
|
}
|
||||||
|
.strong-end {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0dba9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headline {
|
||||||
|
display: block;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f4e5c4;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.gua-bg {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(212, 175, 55, 0.8);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(148, 163, 184, 0.85);
|
||||||
|
}
|
||||||
|
.section-link {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid rgba(212, 175, 55, 0.12);
|
||||||
|
font-size: 12px;
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.char-box {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(2, 6, 23, 0.4);
|
||||||
|
border: 1px solid rgba(212, 175, 55, 0.12);
|
||||||
|
}
|
||||||
|
.char-single {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
.char-meta {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(203, 213, 225, 0.8);
|
||||||
|
margin: 4px 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kv-inline {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.summary-chip {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(2, 6, 23, 0.45);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
|
}
|
||||||
|
.chip-k {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(203, 213, 225, 0.85);
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.chip-v {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f0dba9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-block {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
.member-block:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.member-line {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f2e6d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.year-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 56px 52px 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: start;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.year-key {
|
||||||
|
color: #d4af37;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.year-luck {
|
||||||
|
color: #a7f3d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-line {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.layout-strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f0dba9;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.highlight {
|
||||||
|
color: #fde68a;
|
||||||
|
margin: 0 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.layout-flow {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bars {
|
||||||
|
height: 96px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
.bar-wrap {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
.bar {
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(180deg, rgba(255, 205, 96, 0.9), rgba(230, 129, 38, 0.78));
|
||||||
|
}
|
||||||
|
|
||||||
|
.milestone {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(2, 6, 23, 0.35);
|
||||||
|
border: 1px solid rgba(212, 175, 55, 0.1);
|
||||||
|
}
|
||||||
|
.milestone-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f4e5c4;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.color-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.color-swatch {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-action {
|
||||||
|
padding: 10px 18px 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
max-width: 820px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.fortune-btn {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: linear-gradient(135deg, rgba(139, 35, 35, 0.95), rgba(90, 20, 20, 0.98));
|
||||||
|
color: #fdfbf7;
|
||||||
|
border: 1px solid rgba(212, 175, 55, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-mask {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 3200;
|
||||||
|
background: rgba(2, 6, 23, 0.72);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.detail-modal {
|
||||||
|
width: min(640px, 100%);
|
||||||
|
max-height: min(82vh, 760px);
|
||||||
|
background: rgba(10, 12, 20, 0.96);
|
||||||
|
border: 1px solid rgba(212, 175, 55, 0.2);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.detail-modal-card {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid rgba(212, 175, 55, 0.16);
|
||||||
|
}
|
||||||
|
.modal-title {
|
||||||
|
color: #d4af37;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.close {
|
||||||
|
color: #d4af37;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.detail-modal-body {
|
||||||
|
max-height: calc(min(82vh, 760px) - 48px);
|
||||||
|
padding: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.node {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.line {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: rgba(232, 228, 220, 0.9);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1703
前端源码/uni-app/components/screens/Home.vue
Normal file
1703
前端源码/uni-app/components/screens/Home.vue
Normal file
File diff suppressed because it is too large
Load Diff
661
前端源码/uni-app/components/screens/Login.vue
Normal file
661
前端源码/uni-app/components/screens/Login.vue
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
<template>
|
||||||
|
<view class="login-screen">
|
||||||
|
<!-- 背景装饰 -->
|
||||||
|
<view class="login-bg">
|
||||||
|
<view class="login-bg-pattern"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 主要内容 -->
|
||||||
|
<view class="login-content">
|
||||||
|
<!-- Logo/标题区域 -->
|
||||||
|
<view class="login-header">
|
||||||
|
<view class="login-logo">
|
||||||
|
<text class="login-logo-text">壹梵</text>
|
||||||
|
</view>
|
||||||
|
<text class="login-title">壹梵起名</text>
|
||||||
|
<text class="login-subtitle">传承千年文化 · 赋予美好寓意</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 登录/注册表单 -->
|
||||||
|
<view class="login-form-wrapper">
|
||||||
|
<!-- 标签切换 -->
|
||||||
|
<view class="login-tabs">
|
||||||
|
<view class="login-tab" :class="{ active: currentTab === 'login' }" @click="currentTab = 'login'">
|
||||||
|
<text class="login-tab-text">登录</text>
|
||||||
|
</view>
|
||||||
|
<view class="login-tab" :class="{ active: currentTab === 'register' }" @click="currentTab = 'register'">
|
||||||
|
<text class="login-tab-text">注册</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 登录表单 -->
|
||||||
|
<view v-if="currentTab === 'login'" class="login-form">
|
||||||
|
<view class="login-form-item">
|
||||||
|
<text class="login-form-label">手机号</text>
|
||||||
|
<input v-model="loginForm.mobile" type="tel" class="login-form-input" placeholder="请输入手机号" maxlength="11" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="login-form-item">
|
||||||
|
<text class="login-form-label">密码</text>
|
||||||
|
<input v-model="loginForm.password" :type="showPassword ? 'text' : 'password'" class="login-form-input"
|
||||||
|
placeholder="请输入密码" />
|
||||||
|
<view class="login-form-eye" @click="showPassword = !showPassword">
|
||||||
|
<text>{{ showPassword ? '👁️' : '👁️🗨️' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="login-forgot" @click="currentTab = 'forgot'">
|
||||||
|
<text class="login-forgot-text">忘记密码?</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<button class="login-btn login-btn-primary" :disabled="!canLogin || loading" @click="handleLogin">
|
||||||
|
<text class="login-btn-text">{{ loading ? '登录中...' : '登录' }}</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 注册表单 -->
|
||||||
|
<view v-if="currentTab === 'register'" class="login-form">
|
||||||
|
<view class="login-form-item">
|
||||||
|
<text class="login-form-label">手机号</text>
|
||||||
|
<input v-model="registerForm.mobile" type="tel" class="login-form-input" placeholder="请输入手机号"
|
||||||
|
maxlength="11" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="login-form-item">
|
||||||
|
<text class="login-form-label">验证码</text>
|
||||||
|
<view class="login-form-code">
|
||||||
|
<input v-model="registerForm.code" type="tel" class="login-form-input" placeholder="请输入验证码"
|
||||||
|
maxlength="6" />
|
||||||
|
<button class="login-code-btn" :disabled="!canSendCode || countdown > 0"
|
||||||
|
@click="handleSendCode('register')">
|
||||||
|
<text class="login-code-text">
|
||||||
|
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
||||||
|
</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="login-form-item">
|
||||||
|
<text class="login-form-label">密码</text>
|
||||||
|
<input v-model="registerForm.password" :type="showPassword ? 'text' : 'password'" class="login-form-input"
|
||||||
|
placeholder="请输入密码(6-20位)" />
|
||||||
|
<view class="login-form-eye" @click="showPassword = !showPassword">
|
||||||
|
<text>{{ showPassword ? '👁️' : '👁️🗨️' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="login-form-item">
|
||||||
|
<text class="login-form-label">确认密码</text>
|
||||||
|
<input v-model="registerForm.repassword" :type="showPassword ? 'text' : 'password'" class="login-form-input"
|
||||||
|
placeholder="请再次输入密码" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<button class="login-btn login-btn-primary" :disabled="loading" @click="handleRegister">
|
||||||
|
<text class="login-btn-text">{{ loading ? '注册中...' : '注册' }}</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 忘记密码表单 -->
|
||||||
|
<view v-if="currentTab === 'forgot'" class="login-form">
|
||||||
|
<view class="login-form-item">
|
||||||
|
<text class="login-form-label">手机号</text>
|
||||||
|
<input v-model="forgotForm.mobile" type="tel" class="login-form-input" placeholder="请输入手机号"
|
||||||
|
maxlength="11" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="login-form-item">
|
||||||
|
<text class="login-form-label">验证码</text>
|
||||||
|
<view class="login-form-code">
|
||||||
|
<input v-model="forgotForm.code" type="tel" class="login-form-input" placeholder="请输入验证码" maxlength="6" />
|
||||||
|
<button class="login-code-btn" :disabled="!canSendCodeForgot || countdown > 0"
|
||||||
|
@click="handleSendCode('forgot')">
|
||||||
|
<text class="login-code-text">
|
||||||
|
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
||||||
|
</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="login-form-item">
|
||||||
|
<text class="login-form-label">新密码</text>
|
||||||
|
<input v-model="forgotForm.password" :type="showPassword ? 'text' : 'password'" class="login-form-input"
|
||||||
|
placeholder="请输入新密码(6-20位)" />
|
||||||
|
<view class="login-form-eye" @click="showPassword = !showPassword">
|
||||||
|
<text>{{ showPassword ? '👁️' : '👁️🗨️' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="login-form-item">
|
||||||
|
<text class="login-form-label">确认密码</text>
|
||||||
|
<input v-model="forgotForm.repassword" :type="showPassword ? 'text' : 'password'" class="login-form-input"
|
||||||
|
placeholder="请再次输入新密码" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<button class="login-btn login-btn-primary" :disabled="!canResetPassword || loading"
|
||||||
|
@click="handleResetPassword">
|
||||||
|
<text class="login-btn-text">{{ loading ? '重置中...' : '重置密码' }}</text>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<view class="login-back" @click="currentTab = 'login'">
|
||||||
|
<text class="login-back-text">返回登录</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 协议 -->
|
||||||
|
<view class="login-agreement">
|
||||||
|
<text class="login-agreement-text">
|
||||||
|
登录即表示同意
|
||||||
|
<text class="login-agreement-link" @click="handleNavigateToAgreement">《用户协议》</text>
|
||||||
|
和
|
||||||
|
<text class="login-agreement-link" @click="handleNavigateToPrivacy">《隐私政策》</text>
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { userApi } from '@/api';
|
||||||
|
import type { MobileLoginResponse, MobileRegisterResponse, ForgotPasswordResponse } from '@/api/types';
|
||||||
|
import { showToast } from '@/utils/uni-compat';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
success: [data: MobileLoginResponse | MobileRegisterResponse | ForgotPasswordResponse];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const currentTab = ref<'login' | 'register' | 'forgot'>('login');
|
||||||
|
const showPassword = ref(false);
|
||||||
|
const countdown = ref(0);
|
||||||
|
let countdownTimer: number | null = null;
|
||||||
|
|
||||||
|
// 登录表单
|
||||||
|
const loginForm = reactive({
|
||||||
|
mobile: '',
|
||||||
|
password: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册表单
|
||||||
|
const registerForm = reactive({
|
||||||
|
mobile: '',
|
||||||
|
code: '',
|
||||||
|
password: '',
|
||||||
|
repassword: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 忘记密码表单
|
||||||
|
const forgotForm = reactive({
|
||||||
|
mobile: '',
|
||||||
|
code: '',
|
||||||
|
password: '',
|
||||||
|
repassword: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证手机号
|
||||||
|
const isValidMobile = (mobile: string) => {
|
||||||
|
return /^1[3-9]\d{9}$/.test(mobile);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
const isValidPassword = (password: string) => {
|
||||||
|
return password.length >= 6 && password.length <= 20;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 是否可以登录(仅根据 loading 控制按钮,去掉其他前置校验)
|
||||||
|
const canLogin = computed(() => {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 是否可以注册
|
||||||
|
const canRegister = computed(() => {
|
||||||
|
return (
|
||||||
|
isValidMobile(registerForm.mobile) &&
|
||||||
|
registerForm.code.length === 6 &&
|
||||||
|
isValidPassword(registerForm.password) &&
|
||||||
|
registerForm.password === registerForm.repassword
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 是否可以发送验证码(注册)
|
||||||
|
const canSendCode = computed(() => {
|
||||||
|
return isValidMobile(registerForm.mobile);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 是否可以发送验证码(忘记密码)
|
||||||
|
const canSendCodeForgot = computed(() => {
|
||||||
|
return isValidMobile(forgotForm.mobile);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 是否可以重置密码
|
||||||
|
const canResetPassword = computed(() => {
|
||||||
|
return (
|
||||||
|
isValidMobile(forgotForm.mobile) &&
|
||||||
|
forgotForm.code.length === 6 &&
|
||||||
|
isValidPassword(forgotForm.password) &&
|
||||||
|
forgotForm.password === forgotForm.repassword
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开始倒计时
|
||||||
|
const startCountdown = () => {
|
||||||
|
countdown.value = 60;
|
||||||
|
if (countdownTimer) {
|
||||||
|
clearInterval(countdownTimer);
|
||||||
|
}
|
||||||
|
countdownTimer = window.setInterval(() => {
|
||||||
|
countdown.value--;
|
||||||
|
if (countdown.value <= 0) {
|
||||||
|
if (countdownTimer) {
|
||||||
|
clearInterval(countdownTimer);
|
||||||
|
countdownTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
const handleSendCode = async (type: 'register' | 'forgot') => {
|
||||||
|
const mobile = type === 'register' ? registerForm.mobile : forgotForm.mobile;
|
||||||
|
|
||||||
|
if (!isValidMobile(mobile)) {
|
||||||
|
showToast({ title: '请输入正确的手机号', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await userApi.sendSmsCode(mobile);
|
||||||
|
showToast({ title: '验证码已发送', icon: 'success' });
|
||||||
|
startCountdown();
|
||||||
|
} catch (error: any) {
|
||||||
|
showToast({ title: error.msg || '发送失败,请重试', icon: 'none' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
const handleLogin = async () => {
|
||||||
|
if (!canLogin.value || loading.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const result = await userApi.mobileLogin({
|
||||||
|
mobile: loginForm.mobile,
|
||||||
|
password: loginForm.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
showToast({ title: '登录成功', icon: 'success' });
|
||||||
|
emit('success', result);
|
||||||
|
} catch (error: any) {
|
||||||
|
showToast({ title: error.msg || '登录失败,请重试', icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 注册
|
||||||
|
const handleRegister = async () => {
|
||||||
|
if (!canRegister.value || loading.value) return;
|
||||||
|
|
||||||
|
if (registerForm.password !== registerForm.repassword) {
|
||||||
|
showToast({ title: '两次密码输入不一致', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const result = await userApi.mobileRegister({
|
||||||
|
mobile: registerForm.mobile,
|
||||||
|
password: registerForm.password,
|
||||||
|
repassword: registerForm.repassword,
|
||||||
|
verification_code: registerForm.code,
|
||||||
|
});
|
||||||
|
|
||||||
|
showToast({ title: '注册成功', icon: 'success' });
|
||||||
|
emit('success', result);
|
||||||
|
} catch (error: any) {
|
||||||
|
showToast({ title: error.msg || '注册失败,请重试', icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置密码
|
||||||
|
const handleResetPassword = async () => {
|
||||||
|
if (!canResetPassword.value || loading.value) return;
|
||||||
|
|
||||||
|
if (forgotForm.password !== forgotForm.repassword) {
|
||||||
|
showToast({ title: '两次密码输入不一致', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const result = await userApi.forgotPassword({
|
||||||
|
mobile: forgotForm.mobile,
|
||||||
|
password: forgotForm.password,
|
||||||
|
repassword: forgotForm.repassword,
|
||||||
|
verification_code: forgotForm.code,
|
||||||
|
});
|
||||||
|
|
||||||
|
showToast({ title: '密码重置成功', icon: 'success' });
|
||||||
|
emit('success', result);
|
||||||
|
} catch (error: any) {
|
||||||
|
showToast({ title: error.msg || '重置失败,请重试', icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导航到用户协议
|
||||||
|
const handleNavigateToAgreement = () => {
|
||||||
|
router.push('/user-agreement');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导航到隐私政策
|
||||||
|
const handleNavigateToPrivacy = () => {
|
||||||
|
router.push('/privacy-policy');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-screen {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #fdfbf7 url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-bg-pattern {
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: radial-gradient(circle at 30% 30%, rgba(139, 35, 35, 0.03) 0%, transparent 50%);
|
||||||
|
animation: rotate 60s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40px 30px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #8b2323 0%, #9c2a2a 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 35, 35, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo-text {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fdfbf7;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #2c2c2c;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
border: 1px solid #eaddcf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tabs {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-bottom: 2px solid #eaddcf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 0;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tab.active {
|
||||||
|
color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tab.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tab-text {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 16px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dcd3c9;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-input:focus {
|
||||||
|
border-color: #8b2323;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-eye {
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
bottom: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-code {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-code .login-form-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-code-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 16px;
|
||||||
|
background: #8b2323;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-code-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-code-text {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-forgot {
|
||||||
|
text-align: right;
|
||||||
|
margin-top: -10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-forgot-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8b2323;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-back {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-back-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #8b2323;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 24px;
|
||||||
|
border: none;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn-primary {
|
||||||
|
background: linear-gradient(135deg, #8b2323 0%, #9c2a2a 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn-primary:active {
|
||||||
|
background: linear-gradient(135deg, #701c1c 0%, #8b2323 100%);
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn-text {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-agreement {
|
||||||
|
margin-top: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-agreement-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-agreement-link {
|
||||||
|
color: #8b2323;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-agreement-link:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
916
前端源码/uni-app/components/screens/MyNamingPlansScreen.vue
Normal file
916
前端源码/uni-app/components/screens/MyNamingPlansScreen.vue
Normal file
@@ -0,0 +1,916 @@
|
|||||||
|
<template>
|
||||||
|
<view class="plans-screen">
|
||||||
|
<view class="plans-bg"></view>
|
||||||
|
<view class="status-bar-placeholder"></view>
|
||||||
|
|
||||||
|
<!-- 固定导航栏 -->
|
||||||
|
<view class="plans-header">
|
||||||
|
<view class="plans-back-btn" @click="handleBack">
|
||||||
|
<text class="plans-back-icon">‹</text>
|
||||||
|
</view>
|
||||||
|
<text class="plans-title">我的方案</text>
|
||||||
|
<view class="plans-refresh-btn" @click="handleRefresh">
|
||||||
|
<text class="plans-refresh-icon" :class="{ rotating: loading }">↻</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 固定Tab切换 -->
|
||||||
|
<view class="plans-tabs">
|
||||||
|
<view v-for="tab in tabs" :key="tab.value"
|
||||||
|
:class="['plans-tab', { 'plans-tab-active': currentTab === tab.value }]"
|
||||||
|
@click="switchTab(tab.value as 'naming' | 'affinity' | 'zeji')">
|
||||||
|
<text class="plans-tab-text">{{ tab.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 滚动列表内容 -->
|
||||||
|
<scroll-view class="plans-scroll" scroll-y="true">
|
||||||
|
<view class="plans-content">
|
||||||
|
<!-- 初始加载状态 -->
|
||||||
|
<view v-if="loading && list.length === 0" class="initial-loading">
|
||||||
|
<text class="loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<template v-else-if="list.length > 0">
|
||||||
|
<!-- 名字方案列表 -->
|
||||||
|
<template v-if="currentTab === 'naming'">
|
||||||
|
<view v-for="item in list" :key="item.id" class="list-item" @click="viewNamingDetail(item)">
|
||||||
|
<view :class="['item-icon', item.category === 'company' ? 'icon-gold' : 'icon-red']">
|
||||||
|
<text class="icon-text">{{ item.category === 'company' ? '企' : '名' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="item-content">
|
||||||
|
<view class="item-header">
|
||||||
|
<text class="item-title">{{ item.title || '未命名方案' }}</text>
|
||||||
|
<text class="item-date">{{ item.relative_time }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="item-subtitle">{{ item.subtitle }}</text>
|
||||||
|
<view class="item-tags">
|
||||||
|
<text :class="['item-tag', item.category === 'company' ? 'tag-gold' : 'tag-red']">
|
||||||
|
{{ item.category === 'company' ? '公司' : '个人' }}
|
||||||
|
</text>
|
||||||
|
<text class="item-status">
|
||||||
|
<text class="status-icon">{{ item.has_solutions ? '✓' : '⏱' }}</text>
|
||||||
|
{{ item.has_solutions ? '已生成方案' : '生成中' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="item-arrow">
|
||||||
|
<text class="arrow-icon">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 缘分合盘列表 -->
|
||||||
|
<template v-else-if="currentTab === 'affinity'">
|
||||||
|
<view v-for="item in list" :key="item.id" class="list-item" @click="viewAffinityDetail(item)">
|
||||||
|
<view class="item-icon icon-pink">
|
||||||
|
<text class="icon-text">缘</text>
|
||||||
|
</view>
|
||||||
|
<view class="item-content">
|
||||||
|
<view class="item-header">
|
||||||
|
<text class="item-title">{{ item.person1_name }} & {{ item.person2_name }}</text>
|
||||||
|
<text class="item-date">{{ formatDate(item.created_time) }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="item-subtitle">
|
||||||
|
{{ getRelationshipLabel(item.relationship) }}
|
||||||
|
<template v-if="item.score">· 缘分指数 {{ item.score }}分</template>
|
||||||
|
</text>
|
||||||
|
<view class="item-tags">
|
||||||
|
<text v-if="item.score_badge" class="item-tag tag-pink">{{ item.score_badge }}</text>
|
||||||
|
<text class="item-status">
|
||||||
|
<text class="status-icon">{{ getTaskStatusIcon(item.task_status) }}</text>
|
||||||
|
{{ getTaskStatusText(item.task_status) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="item-arrow">
|
||||||
|
<text class="arrow-icon">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 八字择吉列表 -->
|
||||||
|
<template v-else-if="currentTab === 'zeji'">
|
||||||
|
<view v-for="item in list" :key="item.id" class="list-item" @click="viewZejiDetail(item)">
|
||||||
|
<view class="item-icon icon-purple">
|
||||||
|
<text class="icon-text">吉</text>
|
||||||
|
</view>
|
||||||
|
<view class="item-content">
|
||||||
|
<view class="item-header">
|
||||||
|
<text class="item-title">{{ item.event_name || '择吉事项' }}</text>
|
||||||
|
<text class="item-date">{{ formatDate(item.created_time) }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="item-subtitle">
|
||||||
|
{{ item.event_type_label }}
|
||||||
|
<template v-if="item.selected_date">· {{ item.selected_date }}</template>
|
||||||
|
</text>
|
||||||
|
<view class="item-tags">
|
||||||
|
<text v-if="item.score_badge" class="item-tag tag-purple">{{ item.score_badge }}</text>
|
||||||
|
<text class="item-status">
|
||||||
|
<text class="status-icon">{{ getTaskStatusIcon(item.task_status) }}</text>
|
||||||
|
{{ getTaskStatusText(item.task_status) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="item-arrow">
|
||||||
|
<text class="arrow-icon">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 加载更多按钮 -->
|
||||||
|
<view class="load-more-wrapper">
|
||||||
|
<view v-if="loading" class="loading-more">
|
||||||
|
<text class="loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="hasMore" class="load-more-btn" @click="loadNextPage">
|
||||||
|
<text class="load-more-text">点击加载更多</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="no-more">
|
||||||
|
<text class="no-more-text">— 已加载全部 —</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<template v-else>
|
||||||
|
<view class="empty-state">
|
||||||
|
<text class="empty-icon">📋</text>
|
||||||
|
<text class="empty-text">{{ getEmptyText() }}</text>
|
||||||
|
<view class="empty-btn" @click="handleEmptyAction">
|
||||||
|
<text class="empty-btn-text">{{ getEmptyButtonText() }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, watch } from "vue";
|
||||||
|
import { userApi, namingApi, affinityApi, baziZejiApi } from "@/api";
|
||||||
|
import type { MyReportItem } from "@/api/types";
|
||||||
|
import type { AffinityListItem } from "@/api";
|
||||||
|
import type { BaziZejiListItem } from "@/api";
|
||||||
|
|
||||||
|
declare const uni: any;
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
/** 从详情页返回时父级传入,用于选中「名字 / 缘分合盘 / 八字择吉」子 tab */
|
||||||
|
focusListTab?: 'naming' | 'affinity' | 'zeji' | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
back: [];
|
||||||
|
navigate: [screen: string];
|
||||||
|
showDetail: [data: any, category?: string, serviceType?: string];
|
||||||
|
showNamingSolutionsList: [payload: { reportId: number; solutions: any[]; category?: string; serviceType?: string }];
|
||||||
|
showAffinityResult: [data: any, fromMyPlans?: boolean];
|
||||||
|
showAuspiciousResult: [data: any, fromMyPlans?: boolean];
|
||||||
|
focusListTabConsumed: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// Tab配置
|
||||||
|
const tabs = [
|
||||||
|
{ label: '名字', value: 'naming' },
|
||||||
|
{ label: '缘分合盘', value: 'affinity' },
|
||||||
|
{ label: '八字择吉', value: 'zeji' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const currentTab = ref<'naming' | 'affinity' | 'zeji'>('naming');
|
||||||
|
const loading = ref(false);
|
||||||
|
const list = ref<any[]>([]);
|
||||||
|
const pageNo = ref(1);
|
||||||
|
const pageSize = 10;
|
||||||
|
const total = ref(0);
|
||||||
|
const hasMore = ref(true);
|
||||||
|
|
||||||
|
// 重置分页状态(须在 switchTab / watch 之前声明,避免暂时性死区)
|
||||||
|
const resetPagination = () => {
|
||||||
|
pageNo.value = 1;
|
||||||
|
list.value = [];
|
||||||
|
hasMore.value = true;
|
||||||
|
total.value = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载列表数据
|
||||||
|
const loadList = async (refresh = false) => {
|
||||||
|
if (loading.value) return;
|
||||||
|
if (!refresh && !hasMore.value) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let res: any;
|
||||||
|
|
||||||
|
if (currentTab.value === 'naming') {
|
||||||
|
res = await userApi.getMyReports({
|
||||||
|
page_no: pageNo.value,
|
||||||
|
page_size: pageSize,
|
||||||
|
});
|
||||||
|
const items = res?.items || (Array.isArray(res) ? res : []);
|
||||||
|
|
||||||
|
if (refresh) {
|
||||||
|
list.value = items;
|
||||||
|
} else {
|
||||||
|
list.value = [...list.value, ...items];
|
||||||
|
}
|
||||||
|
|
||||||
|
total.value = res?.total || items.length;
|
||||||
|
hasMore.value = list.value.length < total.value;
|
||||||
|
|
||||||
|
} else if (currentTab.value === 'affinity') {
|
||||||
|
res = await affinityApi.getAffinityList({
|
||||||
|
page_no: pageNo.value,
|
||||||
|
page_size: pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res && res.data) {
|
||||||
|
const items = Array.isArray(res.data.data) ? res.data.data : [];
|
||||||
|
|
||||||
|
if (refresh) {
|
||||||
|
list.value = items;
|
||||||
|
} else {
|
||||||
|
list.value = [...list.value, ...items];
|
||||||
|
}
|
||||||
|
|
||||||
|
total.value = res.data.total || 0;
|
||||||
|
hasMore.value = res.data.has_next || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (currentTab.value === 'zeji') {
|
||||||
|
res = await baziZejiApi.getBaziZejiList({
|
||||||
|
page_no: pageNo.value,
|
||||||
|
page_size: pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res && res.data) {
|
||||||
|
const items = Array.isArray(res.data.data) ? res.data.data : [];
|
||||||
|
|
||||||
|
if (refresh) {
|
||||||
|
list.value = items;
|
||||||
|
} else {
|
||||||
|
list.value = [...list.value, ...items];
|
||||||
|
}
|
||||||
|
|
||||||
|
total.value = res.data.total || 0;
|
||||||
|
hasMore.value = res.data.has_next || false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pageNo.value++;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载失败:', error);
|
||||||
|
uni.showToast({ title: '加载失败,请重试', icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点击加载下一页
|
||||||
|
const loadNextPage = () => {
|
||||||
|
if (!loading.value && hasMore.value) {
|
||||||
|
loadList();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 刷新
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
if (loading.value) return;
|
||||||
|
resetPagination();
|
||||||
|
await loadList(true);
|
||||||
|
uni.showToast({ title: '刷新成功', icon: 'success', duration: 1000 });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换Tab(依赖 resetPagination、loadList,须放在二者之后)
|
||||||
|
const switchTab = (tab: 'naming' | 'affinity' | 'zeji') => {
|
||||||
|
if (currentTab.value === tab) return;
|
||||||
|
currentTab.value = tab;
|
||||||
|
resetPagination();
|
||||||
|
loadList(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.focusListTab,
|
||||||
|
(v) => {
|
||||||
|
if (!v) return;
|
||||||
|
if (v === currentTab.value) {
|
||||||
|
emit('focusListTabConsumed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switchTab(v);
|
||||||
|
emit('focusListTabConsumed');
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 查看名字方案详情
|
||||||
|
const viewNamingDetail = async (item: MyReportItem) => {
|
||||||
|
|
||||||
|
|
||||||
|
if (!item.has_solutions) {
|
||||||
|
uni.showToast({ title: "方案生成中,请稍后查看", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseMaybeJson = (value: any) => {
|
||||||
|
if (value == null) return value;
|
||||||
|
if (typeof value === "object") return value;
|
||||||
|
if (typeof value !== "string") return value;
|
||||||
|
const raw = value.trim();
|
||||||
|
if (!raw) return value;
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
uni.showLoading({ title: "加载中..." });
|
||||||
|
|
||||||
|
const solutionsResult = await namingApi.getSolutionsByReportId(item.id);
|
||||||
|
|
||||||
|
const solutions = solutionsResult?.solutions || solutionsResult?.items || solutionsResult;
|
||||||
|
|
||||||
|
// 新规则:接口返回 solutions 数组且有数据 -> 跳转到新的方案列表页
|
||||||
|
if (Array.isArray(solutions) && solutions.length > 0) {
|
||||||
|
uni.hideLoading();
|
||||||
|
emit("showNamingSolutionsList", {
|
||||||
|
reportId: item.id,
|
||||||
|
solutions,
|
||||||
|
category: item.category,
|
||||||
|
serviceType: String(item.service_type || ''),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则:回退到旧版详情页(接口可能直接返回详情对象)
|
||||||
|
const parsedLegacy = parseMaybeJson(solutionsResult);
|
||||||
|
uni.hideLoading();
|
||||||
|
if (parsedLegacy && typeof parsedLegacy === "object") {
|
||||||
|
emit("showDetail", parsedLegacy as any, item.category, String(item.service_type || ''));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showToast({ title: "暂无方案数据", icon: "none" });
|
||||||
|
return;
|
||||||
|
|
||||||
|
} catch (e: any) {
|
||||||
|
uni.hideLoading();
|
||||||
|
console.error('Error in viewNamingDetail:', e);
|
||||||
|
uni.showToast({ title: e.msg || e.message || "获取详情失败", icon: "none" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看缘分合盘详情
|
||||||
|
const viewAffinityDetail = async (item: AffinityListItem) => {
|
||||||
|
if (item.task_status !== 5) {
|
||||||
|
uni.showToast({ title: "生成中,请稍后查看", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showLoading({ title: "加载中..." });
|
||||||
|
const detailData = await affinityApi.getAffinityDetail(item.id);
|
||||||
|
uni.hideLoading();
|
||||||
|
|
||||||
|
if (!detailData) {
|
||||||
|
uni.showToast({ title: "获取详情失败", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformedData = {
|
||||||
|
relationship: detailData.relationship,
|
||||||
|
relationshipLabel: getRelationshipLabel(detailData.relationship),
|
||||||
|
person1: {
|
||||||
|
name: detailData.person1_name,
|
||||||
|
gender: detailData.person1_gender,
|
||||||
|
birthDate: detailData.person1_birth_date,
|
||||||
|
},
|
||||||
|
person2: {
|
||||||
|
name: detailData.person2_name,
|
||||||
|
gender: detailData.person2_gender,
|
||||||
|
birthDate: detailData.person2_birth_date,
|
||||||
|
},
|
||||||
|
score: detailData.score,
|
||||||
|
scoreBadge: detailData.score_badge,
|
||||||
|
sixDimension: detailData.six_dimension ? JSON.parse(detailData.six_dimension) : null,
|
||||||
|
radarDesc: detailData.radar_desc,
|
||||||
|
analysisCards: detailData.analysis_cards ? JSON.parse(detailData.analysis_cards).map((card: any, index: number) => ({
|
||||||
|
id: `card-${index}`,
|
||||||
|
icon: ['💗', '💬', '🛡️', '⚡', '🎯', '🏠'][index] || '✨',
|
||||||
|
iconBg: ['rgba(236, 72, 153, 0.2)', 'rgba(59, 130, 246, 0.2)', 'rgba(34, 197, 94, 0.2)', 'rgba(234, 179, 8, 0.2)', 'rgba(168, 85, 247, 0.2)', 'rgba(249, 115, 22, 0.2)'][index] || 'rgba(156, 163, 175, 0.2)',
|
||||||
|
title: card.title,
|
||||||
|
score: String(card.score),
|
||||||
|
summary: card.content.substring(0, 50) + '...',
|
||||||
|
content: card.content,
|
||||||
|
})) : [],
|
||||||
|
unlocked: detailData.unlocked_content ? JSON.parse(detailData.unlocked_content) : null,
|
||||||
|
isUnlocked: detailData.is_unlocked === 1,
|
||||||
|
unlockPrice: detailData.unlock_price,
|
||||||
|
unlockStats: {
|
||||||
|
unlockCount: 12392,
|
||||||
|
accuracy: '98%',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
emit('showAffinityResult', transformedData, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看八字择吉详情
|
||||||
|
const viewZejiDetail = async (item: BaziZejiListItem) => {
|
||||||
|
uni.showLoading({ title: "加载中..." });
|
||||||
|
const detailData = await baziZejiApi.getBaziZejiDetail(item.id);
|
||||||
|
uni.hideLoading();
|
||||||
|
|
||||||
|
if (!detailData) {
|
||||||
|
uni.showToast({ title: "获取详情失败", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('showAuspiciousResult', detailData, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 工具函数:相对时间(刚刚 / N分钟前 / N小时前 / …)
|
||||||
|
const formatDate = (dateStr: string) => {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
if (Number.isNaN(date.getTime())) return String(dateStr).trim();
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const diff = now - date.getTime();
|
||||||
|
if (diff < 0) {
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const d = String(date.getDate()).padStart(2, '0');
|
||||||
|
const h = String(date.getHours()).padStart(2, '0');
|
||||||
|
const min = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
return `${y}-${m}-${d} ${h}:${min}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minutes = Math.floor(diff / (1000 * 60));
|
||||||
|
if (minutes < 1) return '刚刚';
|
||||||
|
if (minutes < 60) return `${minutes}分钟前`;
|
||||||
|
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
if (hours < 24) return `${hours}小时前`;
|
||||||
|
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
if (days === 1) return '昨天';
|
||||||
|
if (days < 7) return `${days}天前`;
|
||||||
|
if (days < 30) return `${Math.floor(days / 7)}周前`;
|
||||||
|
if (days < 365) return `${Math.floor(days / 30)}月前`;
|
||||||
|
return `${Math.floor(days / 365)}年前`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRelationshipLabel = (relationship: string) => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
'couple': '恋人',
|
||||||
|
'married': '夫妻',
|
||||||
|
'crush': '暗恋',
|
||||||
|
'partner': '合作伙伴',
|
||||||
|
'friend': '朋友',
|
||||||
|
'family': '家人',
|
||||||
|
};
|
||||||
|
return labels[relationship] || relationship;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTaskStatusIcon = (status: number) => {
|
||||||
|
if (status === 5) return '✓';
|
||||||
|
if (status === 0) return '⏱';
|
||||||
|
return '⏱';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTaskStatusText = (status: number) => {
|
||||||
|
if (status === 5) return '已生成';
|
||||||
|
if (status === 0) return '生成中';
|
||||||
|
return '生成中';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEmptyText = () => {
|
||||||
|
if (currentTab.value === 'naming') return '暂无起名方案';
|
||||||
|
if (currentTab.value === 'affinity') return '暂无缘分合盘记录';
|
||||||
|
if (currentTab.value === 'zeji') return '暂无八字择吉记录';
|
||||||
|
return '暂无数据';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEmptyButtonText = () => {
|
||||||
|
if (currentTab.value === 'naming') return '去起名';
|
||||||
|
if (currentTab.value === 'affinity') return '去测算';
|
||||||
|
if (currentTab.value === 'zeji') return '去择吉';
|
||||||
|
return '去测算';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEmptyAction = () => {
|
||||||
|
if (currentTab.value === 'naming') {
|
||||||
|
emit('navigate', 'naming');
|
||||||
|
} else if (currentTab.value === 'affinity') {
|
||||||
|
emit('navigate', 'affinity');
|
||||||
|
} else if (currentTab.value === 'zeji') {
|
||||||
|
emit('navigate', 'calendar');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
emit('back');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
loadList(true);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.plans-screen {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f0efe9;
|
||||||
|
position: relative;
|
||||||
|
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar-placeholder {
|
||||||
|
height: var(--status-bar-height, 0);
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.3;
|
||||||
|
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-header {
|
||||||
|
position: sticky;
|
||||||
|
top: var(--status-bar-height, 0);
|
||||||
|
z-index: 100;
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
border-bottom: 1rpx solid #dcd3c9;
|
||||||
|
background-color: rgba(253, 251, 247, 0.95);
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-back-btn {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: -16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-back-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #5a5a5a;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-refresh-btn {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-right: -16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-refresh-icon {
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: #5a5a5a;
|
||||||
|
font-weight: 300;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-refresh-icon.rotating {
|
||||||
|
animation: rotate 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-tabs {
|
||||||
|
position: sticky;
|
||||||
|
top: calc(var(--status-bar-height, 0) + 88rpx);
|
||||||
|
z-index: 99;
|
||||||
|
display: flex;
|
||||||
|
background-color: rgba(253, 251, 247, 0.95);
|
||||||
|
border-bottom: 1rpx solid #dcd3c9;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-tab-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-tab-active .plans-tab-text {
|
||||||
|
color: #8b2323;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-tab-active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 48rpx;
|
||||||
|
height: 4rpx;
|
||||||
|
background-color: #8b2323;
|
||||||
|
border-radius: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-scroll {
|
||||||
|
flex: 1;
|
||||||
|
height: calc(100vh - var(--status-bar-height, 0) - 88rpx - 76rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-content {
|
||||||
|
padding: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initial-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 400rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
background-color: #fffdf9;
|
||||||
|
padding: 32rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
background-color: #f8f6f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-icon {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-red {
|
||||||
|
background-color: rgba(139, 35, 35, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-gold {
|
||||||
|
background-color: rgba(212, 175, 55, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-pink {
|
||||||
|
background-color: rgba(236, 72, 153, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-purple {
|
||||||
|
background-color: rgba(147, 51, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-text {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-red .icon-text {
|
||||||
|
color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-gold .icon-text {
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-pink .icon-text {
|
||||||
|
color: #ec4899;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-purple .icon-text {
|
||||||
|
color: #9333ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-date {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #999;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-subtitle {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #5a5a5a;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-tags {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-tag {
|
||||||
|
font-size: 20rpx;
|
||||||
|
padding: 2rpx 12rpx;
|
||||||
|
border-width: 1rpx;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-red {
|
||||||
|
border-color: rgba(139, 35, 35, 0.3);
|
||||||
|
color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-gold {
|
||||||
|
border-color: rgba(212, 175, 55, 0.3);
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-pink {
|
||||||
|
border-color: rgba(236, 72, 153, 0.3);
|
||||||
|
color: #ec4899;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-purple {
|
||||||
|
border-color: rgba(147, 51, 234, 0.3);
|
||||||
|
color: #9333ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-status {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #ccc;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
font-size: 20rpx;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-arrow {
|
||||||
|
align-self: center;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-wrapper {
|
||||||
|
padding: 32rpx 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-btn {
|
||||||
|
background: #8b2323;
|
||||||
|
color: white;
|
||||||
|
padding: 20rpx 40rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-more {
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-more-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 500rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 96rpx;
|
||||||
|
opacity: 0.3;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-btn {
|
||||||
|
background-color: #8b2323;
|
||||||
|
padding: 16rpx 64rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-btn-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
1116
前端源码/uni-app/components/screens/Naming.vue
Normal file
1116
前端源码/uni-app/components/screens/Naming.vue
Normal file
File diff suppressed because it is too large
Load Diff
2949
前端源码/uni-app/components/screens/NamingDetail.vue
Normal file
2949
前端源码/uni-app/components/screens/NamingDetail.vue
Normal file
File diff suppressed because it is too large
Load Diff
11
前端源码/uni-app/components/screens/NamingPlaceholder.vue
Normal file
11
前端源码/uni-app/components/screens/NamingPlaceholder.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<view class="h-full flex flex-col items-center justify-center bg-[#fdfbf7] text-[#2c2c2c] font-serif relative overflow-hidden">
|
||||||
|
<view class="absolute inset-0 opacity-10 pointer-events-none bg-[url('https://www.transparenttextures.com/patterns/rice-paper.png')]"></view>
|
||||||
|
<view class="relative z-10 text-center px-8">
|
||||||
|
<text class="block text-4xl mb-3 text-[#8b2323]">名</text>
|
||||||
|
<text class="block text-xl font-bold tracking-[0.3em] mb-2">智能起名模块</text>
|
||||||
|
<text class="block text-sm text-[#5a5a5a]">稍后将迁移完整表单与生成结果</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
391
前端源码/uni-app/components/screens/NamingSolutionsList.vue
Normal file
391
前端源码/uni-app/components/screens/NamingSolutionsList.vue
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
<template>
|
||||||
|
<view class="solutions-screen">
|
||||||
|
<view class="solutions-bg"></view>
|
||||||
|
<view class="status-bar-placeholder"></view>
|
||||||
|
|
||||||
|
<view class="solutions-header">
|
||||||
|
<view class="solutions-back-btn" @click="$emit('back')">
|
||||||
|
<text class="solutions-back-icon">‹</text>
|
||||||
|
</view>
|
||||||
|
<view class="solutions-header-center">
|
||||||
|
<text class="solutions-title">起名方案列表</text>
|
||||||
|
</view>
|
||||||
|
<view class="solutions-header-placeholder"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="solutions-scroll">
|
||||||
|
<view class="solutions-content">
|
||||||
|
<view v-if="!solutions.length" class="solutions-empty">
|
||||||
|
<text class="solutions-empty-icon">✦</text>
|
||||||
|
<text class="solutions-empty-text">暂无方案</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-for="(it, idx) in solutions"
|
||||||
|
:key="String(it?.id || it?.solution_id || idx)"
|
||||||
|
class="solutions-item"
|
||||||
|
:style="{ animationDelay: (idx * 0.04) + 's' }"
|
||||||
|
@click="open(it)"
|
||||||
|
>
|
||||||
|
<view class="solutions-item-main">
|
||||||
|
<view class="solutions-item-header">
|
||||||
|
<view>
|
||||||
|
<text class="solutions-item-name">{{ titleOf(it) }}</text>
|
||||||
|
<text class="solutions-item-pinyin">{{ pinyinOf(it) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="solutions-item-actions">
|
||||||
|
<view class="solutions-item-view-btn" @click.stop="open(it)">
|
||||||
|
<text class="solutions-item-view-icon">⌕</text>
|
||||||
|
<text class="solutions-item-view-text">查看</text>
|
||||||
|
</view>
|
||||||
|
<view class="solutions-item-badge">
|
||||||
|
<text class="solutions-item-badge-text">{{ idx + 1 }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="solutions-item-meta">
|
||||||
|
<text class="solutions-tag">{{ tagAOf(it) }}</text>
|
||||||
|
<text class="solutions-tag">{{ tagBOf(it) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="solutions-item-meta-sub">
|
||||||
|
<text v-if="scoreOf(it)" class="solutions-chip">评分 {{ scoreOf(it) }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="poetryOf(it)" class="solutions-item-poetry">
|
||||||
|
<text class="solutions-item-poetry-label">出处</text>
|
||||||
|
<text class="solutions-item-poetry-text">{{ poetryOf(it) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { namingApi } from "@/api/naming";
|
||||||
|
import { parseMaybeJson } from "@/utils/poll-test-solution-detail";
|
||||||
|
|
||||||
|
declare const uni: any;
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
reportId: number;
|
||||||
|
solutions: any[];
|
||||||
|
category?: string;
|
||||||
|
serviceType?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
back: [];
|
||||||
|
showDetail: [data: any, category?: string, serviceType?: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const solutions = computed(() => (Array.isArray(props.solutions) ? props.solutions : []));
|
||||||
|
|
||||||
|
const titleOf = (it: any) => {
|
||||||
|
const name = String(it?.name || it?.solution_name || it?.title || it?.label || "").trim();
|
||||||
|
if (name) return name;
|
||||||
|
const fallback = String(it?.given_name || it?.full_name || it?.company_name || "").trim();
|
||||||
|
return fallback || "方案";
|
||||||
|
};
|
||||||
|
const pinyinOf = (it: any) => String(it?.pinyin || it?.name_pinyin || "").trim() || "Pīn Yīn";
|
||||||
|
const tagAOf = (it: any) => String(it?.wuxing || it?.wuxing_tag || it?.element_tag || "五行均衡");
|
||||||
|
const tagBOf = (it: any) => String(it?.style || it?.style_tag || it?.feature_tag || "温文尔雅");
|
||||||
|
const poetryOf = (it: any) => String(it?.poetry_source || it?.poetry || it?.source || "").trim();
|
||||||
|
const scoreOf = (it: any) => {
|
||||||
|
const v = it?.total_score ?? it?.score;
|
||||||
|
if (v === null || v === undefined || v === "") return "";
|
||||||
|
return String(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
const open = async (it: any) => {
|
||||||
|
const id = it?.id || it?.solution_id;
|
||||||
|
if (!id) {
|
||||||
|
uni.showToast({ title: "方案ID不存在", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
uni.showLoading({ title: "加载中..." });
|
||||||
|
const detailRaw: any = await namingApi.getSolutionDetail(id);
|
||||||
|
uni.hideLoading();
|
||||||
|
const parsed = parseMaybeJson(detailRaw);
|
||||||
|
if (!parsed || typeof parsed !== "object") {
|
||||||
|
uni.showToast({ title: "详情数据格式异常", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit("showDetail", parsed, props.category, String(props.serviceType || ""));
|
||||||
|
} catch (e: any) {
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showToast({ title: e?.msg || e?.message || "加载失败", icon: "none" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.solutions-screen{
|
||||||
|
min-height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.solutions-bg{
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
/* 纸张底色 */
|
||||||
|
radial-gradient(1200px 900px at 30% 20%, rgba(255,255,255,.92), rgba(245,241,232,.88) 45%, rgba(235,229,214,.92) 100%),
|
||||||
|
/* 纸纹颗粒 */
|
||||||
|
radial-gradient(2px 2px at 12% 18%, rgba(0,0,0,.06), transparent 55%),
|
||||||
|
radial-gradient(2px 2px at 48% 62%, rgba(0,0,0,.05), transparent 55%),
|
||||||
|
radial-gradient(2px 2px at 78% 34%, rgba(0,0,0,.04), transparent 55%),
|
||||||
|
radial-gradient(2px 2px at 32% 84%, rgba(0,0,0,.04), transparent 55%),
|
||||||
|
/* 墨韵晕染 */
|
||||||
|
radial-gradient(900px 520px at 12% 8%, rgba(25,28,33,.10), transparent 60%),
|
||||||
|
radial-gradient(760px 520px at 92% 22%, rgba(120,75,40,.08), transparent 62%),
|
||||||
|
radial-gradient(900px 640px at 40% 92%, rgba(90,35,35,.08), transparent 62%),
|
||||||
|
linear-gradient(180deg, #f6f2e8 0%, #efe7d6 60%, #f6f2e8 100%);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
.status-bar-placeholder{
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
.solutions-header{
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:space-between;
|
||||||
|
padding: 10px 14px 12px;
|
||||||
|
}
|
||||||
|
.solutions-back-btn{
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(120,90,40,.26);
|
||||||
|
background: linear-gradient(180deg, rgba(255,255,255,.70), rgba(244,236,220,.65));
|
||||||
|
box-shadow: 0 6px 16px rgba(40,30,20,.10);
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
}
|
||||||
|
.solutions-back-icon{
|
||||||
|
font-size: 22px;
|
||||||
|
color: rgba(82,60,28,.92);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.solutions-header-center{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
align-items:center;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
.solutions-title{
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: rgba(28,24,20,.92);
|
||||||
|
letter-spacing: .08em;
|
||||||
|
font-family: "STSong","Songti SC","SimSun","STSong","Noto Serif SC",serif;
|
||||||
|
}
|
||||||
|
.solutions-subtitle{
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(70,58,44,.72);
|
||||||
|
font-family: "STSong","Songti SC","SimSun","STSong","Noto Serif SC",serif;
|
||||||
|
}
|
||||||
|
.solutions-header-placeholder{
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
.solutions-scroll{
|
||||||
|
height: calc(100vh - 24px - 58px);
|
||||||
|
}
|
||||||
|
.solutions-content{
|
||||||
|
padding: 2px 14px 24px;
|
||||||
|
box-sizing:border-box;
|
||||||
|
}
|
||||||
|
.solutions-empty{
|
||||||
|
padding: 26px 10px;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid rgba(120,90,40,.18);
|
||||||
|
background: linear-gradient(180deg, rgba(255,255,255,.76), rgba(247,239,224,.66));
|
||||||
|
box-shadow: 0 10px 26px rgba(40,30,20,.10);
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
.solutions-empty-icon{
|
||||||
|
display:block;
|
||||||
|
font-size: 18px;
|
||||||
|
color: rgba(132,98,52,.85);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.solutions-empty-text{
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(50,40,28,.78);
|
||||||
|
font-family: "STSong","Songti SC","SimSun","STSong","Noto Serif SC",serif;
|
||||||
|
}
|
||||||
|
.solutions-item{
|
||||||
|
position: relative;
|
||||||
|
display:flex;
|
||||||
|
align-items:stretch;
|
||||||
|
justify-content:space-between;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid rgba(178,120,120,.45);
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255,255,255,.84), rgba(249,243,232,.78));
|
||||||
|
overflow:hidden;
|
||||||
|
padding: 14px 14px 13px 14px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
animation: solFadeUp .22s ease both;
|
||||||
|
box-shadow:
|
||||||
|
0 14px 30px rgba(40,30,20,.14),
|
||||||
|
inset 0 1px 0 rgba(255,255,255,.55);
|
||||||
|
}
|
||||||
|
.solutions-item-main{
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.solutions-item-header{
|
||||||
|
display:flex;
|
||||||
|
align-items:flex-start;
|
||||||
|
justify-content:space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.solutions-item-name{
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #20252e;
|
||||||
|
letter-spacing: .04em;
|
||||||
|
font-family: "STSong","Songti SC","SimSun","STSong","Noto Serif SC",serif;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.solutions-item-pinyin{
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(32,37,46,.72);
|
||||||
|
letter-spacing: .08em;
|
||||||
|
}
|
||||||
|
.solutions-item-badge{
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(202,166,96,.62);
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255,245,220,.90), rgba(248,229,184,.82));
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
flex: 0 0 24px;
|
||||||
|
}
|
||||||
|
.solutions-item-badge-text{
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: rgba(128,96,42,.92);
|
||||||
|
}
|
||||||
|
.solutions-item-actions{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.solutions-item-view-btn{
|
||||||
|
height: 26px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(188,170,136,.78);
|
||||||
|
background: rgba(255,255,255,.70);
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap: 4px;
|
||||||
|
box-shadow: 0 2px 6px rgba(40,30,20,.08);
|
||||||
|
}
|
||||||
|
.solutions-item-view-icon{
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(55,60,68,.75);
|
||||||
|
}
|
||||||
|
.solutions-item-view-text{
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgba(55,60,68,.85);
|
||||||
|
}
|
||||||
|
.solutions-item-meta{
|
||||||
|
display:flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.solutions-tag{
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 7px;
|
||||||
|
border: 1px solid rgba(188,114,114,.52);
|
||||||
|
color: rgba(130,58,58,.90);
|
||||||
|
background: rgba(255,250,248,.78);
|
||||||
|
font-family: "STSong","Songti SC","SimSun","STSong","Noto Serif SC",serif;
|
||||||
|
}
|
||||||
|
.solutions-item-meta-sub{
|
||||||
|
display:flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 9px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.solutions-chip{
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(196,168,106,.42);
|
||||||
|
color: rgba(128,96,42,.88);
|
||||||
|
background: rgba(250,239,211,.65);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
}
|
||||||
|
.solutions-item-poetry{
|
||||||
|
margin-top: 8px;
|
||||||
|
display:flex;
|
||||||
|
align-items:flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.solutions-item-poetry-label{
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: rgba(130,58,58,.90);
|
||||||
|
border: 1px solid rgba(188,114,114,.50);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
background: rgba(255,250,248,.78);
|
||||||
|
}
|
||||||
|
.solutions-item-poetry-text{
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: rgba(40,34,26,.82);
|
||||||
|
font-family: "STSong","Songti SC","SimSun","STSong","Noto Serif SC",serif;
|
||||||
|
}
|
||||||
|
@keyframes solFadeUp{
|
||||||
|
from{opacity:0;transform:translateY(6px)}
|
||||||
|
to{opacity:1;transform:translateY(0)}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 细节:内侧金线与“卷轴边” */
|
||||||
|
.solutions-item::before{
|
||||||
|
content:"";
|
||||||
|
position:absolute;
|
||||||
|
inset: 9px 9px 9px 9px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(188,170,136,.28);
|
||||||
|
pointer-events:none;
|
||||||
|
}
|
||||||
|
.solutions-item::after{
|
||||||
|
content:"";
|
||||||
|
position:absolute;
|
||||||
|
top:-40px;
|
||||||
|
right:-60px;
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
background: radial-gradient(closest-side, rgba(188,114,114,.08), transparent 70%);
|
||||||
|
transform: rotate(18deg);
|
||||||
|
pointer-events:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 轻微按压反馈 */
|
||||||
|
.solutions-item:active{
|
||||||
|
transform: translateY(1px);
|
||||||
|
box-shadow:
|
||||||
|
0 10px 22px rgba(40,30,20,.12),
|
||||||
|
inset 0 1px 0 rgba(255,255,255,.55);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
4885
前端源码/uni-app/components/screens/PersonalWealthAnalysis.vue
Normal file
4885
前端源码/uni-app/components/screens/PersonalWealthAnalysis.vue
Normal file
File diff suppressed because it is too large
Load Diff
351
前端源码/uni-app/components/screens/PrivacyPolicy.vue
Normal file
351
前端源码/uni-app/components/screens/PrivacyPolicy.vue
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
<template>
|
||||||
|
<view class="privacy-screen">
|
||||||
|
<view class="privacy-header">
|
||||||
|
<view class="privacy-back" @click="handleBack">
|
||||||
|
<text class="privacy-back-icon">←</text>
|
||||||
|
</view>
|
||||||
|
<text class="privacy-title">隐私政策</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading" class="privacy-loading">
|
||||||
|
<text class="privacy-loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="error" class="privacy-error">
|
||||||
|
<text class="privacy-error-text">{{ error }}</text>
|
||||||
|
<button class="privacy-retry-btn" @click="loadPrivacyPolicy">
|
||||||
|
<text class="privacy-retry-text">重试</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else class="privacy-content">
|
||||||
|
<view v-if="policy" class="privacy-info">
|
||||||
|
<text class="privacy-version">版本:{{ policy.version }}</text>
|
||||||
|
<text class="privacy-date">生效日期:{{ policy.effective_date }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="policy" class="privacy-body" v-html="policy.content"></view>
|
||||||
|
|
||||||
|
<view v-if="!policy" class="privacy-default">
|
||||||
|
<view class="privacy-section">
|
||||||
|
<text class="privacy-section-title">引言</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
壹梵起名(以下简称"我们")非常重视用户的隐私保护。本隐私政策旨在向您说明我们如何收集、使用、存储和保护您的个人信息。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="privacy-section">
|
||||||
|
<text class="privacy-section-title">一、我们收集的信息</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
1. 账号信息:手机号码、密码等注册信息;
|
||||||
|
</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
2. 服务信息:您在使用起名、测名等服务时提供的姓名、出生日期、性别等信息;
|
||||||
|
</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
3. 设备信息:设备型号、操作系统版本、设备标识符等;
|
||||||
|
</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
4. 日志信息:IP地址、访问时间、浏览记录等。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="privacy-section">
|
||||||
|
<text class="privacy-section-title">二、信息的使用</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
我们收集的信息将用于:
|
||||||
|
</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
1. 提供、维护和改进我们的服务;
|
||||||
|
</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
2. 处理您的订单和支付;
|
||||||
|
</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
3. 向您发送服务通知和更新;
|
||||||
|
</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
4. 保护服务安全,防止欺诈;
|
||||||
|
</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
5. 遵守法律法规要求。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="privacy-section">
|
||||||
|
<text class="privacy-section-title">三、信息的共享</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
我们不会向第三方出售、出租或以其他方式披露您的个人信息,除非:
|
||||||
|
</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
1. 获得您的明确同意;
|
||||||
|
</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
2. 法律法规要求;
|
||||||
|
</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
3. 为提供服务所必需(如支付服务提供商);
|
||||||
|
</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
4. 保护我们或他人的合法权益。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="privacy-section">
|
||||||
|
<text class="privacy-section-title">四、信息的存储</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
您的个人信息将存储在中华人民共和国境内的服务器上。我们将采取合理的安全措施保护您的信息,包括加密存储、访问控制等。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="privacy-section">
|
||||||
|
<text class="privacy-section-title">五、您的权利</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
您有权:
|
||||||
|
</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
1. 访问、更正或删除您的个人信息;
|
||||||
|
</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
2. 撤回您的同意;
|
||||||
|
</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
3. 注销您的账号;
|
||||||
|
</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
4. 投诉或举报。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="privacy-section">
|
||||||
|
<text class="privacy-section-title">六、未成年人保护</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
我们非常重视未成年人的个人信息保护。如果您是未成年人,请在监护人的陪同下阅读本政策,并在监护人同意的情况下使用我们的服务。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="privacy-section">
|
||||||
|
<text class="privacy-section-title">七、政策更新</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
我们可能会不时更新本隐私政策。更新后的政策将在平台上公布,并在您继续使用服务时生效。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="privacy-section">
|
||||||
|
<text class="privacy-section-title">八、联系我们</text>
|
||||||
|
<text class="privacy-text">
|
||||||
|
如果您对本隐私政策有任何疑问或建议,请通过平台内的反馈功能联系我们。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="privacy-footer">
|
||||||
|
<text class="privacy-footer-text">壹梵起名</text>
|
||||||
|
<text class="privacy-footer-text">生效日期:2024年1月1日</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { userApi } from '@/api';
|
||||||
|
import type { PrivacyPolicyResponse } from '@/api/types';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const loading = ref(true);
|
||||||
|
const error = ref('');
|
||||||
|
const policy = ref<PrivacyPolicyResponse | null>(null);
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPrivacyPolicy = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await userApi.getPrivacyPolicy();
|
||||||
|
policy.value = result;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('加载隐私政策失败:', err);
|
||||||
|
error.value = err.msg || '加载失败,请重试';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadPrivacyPolicy();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.privacy-screen {
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
background: #fdfbf7 url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: rgba(253, 251, 247, 0.95);
|
||||||
|
border-bottom: 1px solid #eaddcf;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-back {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-back-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #8b2323;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-loading,
|
||||||
|
.privacy-error {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-loading-text,
|
||||||
|
.privacy-error-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-retry-btn {
|
||||||
|
padding: 8px 24px;
|
||||||
|
background: #8b2323;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px 20px 40px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: rgba(139, 35, 35, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-version,
|
||||||
|
.privacy-date {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8b2323;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-body {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #4a4a4a;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-default {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-section-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #8b2323;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #4a4a4a;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-footer {
|
||||||
|
margin-top: 40px;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid #eaddcf;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-footer-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1134
前端源码/uni-app/components/screens/Profile.vue
Normal file
1134
前端源码/uni-app/components/screens/Profile.vue
Normal file
File diff suppressed because it is too large
Load Diff
416
前端源码/uni-app/components/screens/ProfileFAQ.vue
Normal file
416
前端源码/uni-app/components/screens/ProfileFAQ.vue
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
<template>
|
||||||
|
<view class="faq-screen">
|
||||||
|
<view class="faq-bg"></view>
|
||||||
|
<!-- 状态栏占位 -->
|
||||||
|
<view class="status-bar-placeholder"></view>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<view class="faq-header">
|
||||||
|
<view class="faq-back-btn" @click="handleBack">
|
||||||
|
<text class="faq-back-icon">‹</text>
|
||||||
|
</view>
|
||||||
|
<text class="faq-title">常见问题</text>
|
||||||
|
<view class="faq-header-placeholder"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<scroll-view scroll-y class="faq-content">
|
||||||
|
<view class="faq-content-inner">
|
||||||
|
|
||||||
|
<view v-if="loading" class="faq-loading">
|
||||||
|
<text class="faq-loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-else-if="groups.length > 0">
|
||||||
|
<view v-for="(group, groupIndex) in groups" :key="groupIndex" class="faq-group">
|
||||||
|
<view class="faq-group-header">
|
||||||
|
<text class="faq-group-title">{{ group.category_name }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="faq-list">
|
||||||
|
<view v-for="(item, itemIndex) in group.items" :key="item.id" class="faq-item"
|
||||||
|
:class="{ 'faq-item-expanded': expandedItems[item.id] }" @click="toggleItem(item.id)">
|
||||||
|
<view class="faq-question">
|
||||||
|
<text class="faq-question-icon">Q</text>
|
||||||
|
<view class="faq-question-content">
|
||||||
|
<text class="faq-question-text">{{ item.question }}</text>
|
||||||
|
<text v-if="item.is_hot === 1" class="faq-hot-badge">HOT</text>
|
||||||
|
</view>
|
||||||
|
<text class="faq-question-arrow"
|
||||||
|
:class="{ 'faq-question-arrow-expanded': expandedItems[item.id] }">›</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="expandedItems[item.id]" class="faq-answer">
|
||||||
|
<text class="faq-answer-icon">A</text>
|
||||||
|
<text class="faq-answer-text">{{ item.answer }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view v-else class="faq-empty">
|
||||||
|
<text class="faq-empty-icon">📋</text>
|
||||||
|
<text class="faq-empty-text">暂无常见问题</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 联系客服 -->
|
||||||
|
<view class="faq-contact">
|
||||||
|
<text class="faq-contact-title">没有找到答案?</text>
|
||||||
|
<button class="faq-contact-btn" @click="handleContact">
|
||||||
|
<text class="faq-contact-btn-text">联系客服</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from "vue";
|
||||||
|
import { userApi } from "@/api";
|
||||||
|
import type { FAQGroup } from "@/api/types";
|
||||||
|
|
||||||
|
declare const uni: any;
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
back: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const groups = ref<FAQGroup[]>([]);
|
||||||
|
const expandedItems = reactive<Record<number, boolean>>({});
|
||||||
|
|
||||||
|
const loadFAQ = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await userApi.getFAQ();
|
||||||
|
console.log('getFAQ response:', res);
|
||||||
|
// API返回的是data数组,不是groups
|
||||||
|
groups.value = res?.data || (Array.isArray(res) ? res : []);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('loadFAQ error:', e);
|
||||||
|
uni.showToast({ title: e.msg || "加载失败", icon: "none" });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleItem = (id: number) => {
|
||||||
|
expandedItems[id] = !expandedItems[id];
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContact = () => {
|
||||||
|
// Web环境使用alert,uni-app环境使用showModal
|
||||||
|
if (typeof uni?.showModal === 'function') {
|
||||||
|
uni.showModal({
|
||||||
|
title: '联系客服',
|
||||||
|
content: '客服微信:yifan_service\n工作时间:9:00-18:00',
|
||||||
|
showCancel: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Web环境使用原生alert
|
||||||
|
alert('联系客服\n\n客服微信:yifan_service\n工作时间:9:00-18:00');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
emit('back');
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => loadFAQ());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.faq-screen {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f0efe9;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar-placeholder {
|
||||||
|
height: var(--status-bar-height, 0);
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.3;
|
||||||
|
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.faq-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
border-bottom: 1rpx solid #dcd3c9;
|
||||||
|
background-color: rgba(253, 251, 247, 0.8);
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-back-btn {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: -16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-back-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #5a5a5a;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-header-placeholder {
|
||||||
|
width: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content */
|
||||||
|
.faq-content {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-content-inner {
|
||||||
|
padding: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading */
|
||||||
|
.faq-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 400rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-loading-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Group */
|
||||||
|
.faq-group {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-group-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
padding: 0 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-group-icon {
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-group-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List */
|
||||||
|
.faq-list {
|
||||||
|
background-color: #fffdf9;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item {
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item:active {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item-expanded {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Question */
|
||||||
|
.faq-question {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-question-icon {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #8b2323;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-question-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-question-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-hot-badge {
|
||||||
|
font-size: 18rpx;
|
||||||
|
background: linear-gradient(135deg, #ff6b6b 0%, #ff4757 100%);
|
||||||
|
color: #fff;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-question-arrow {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #ccc;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-question-arrow-expanded {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Answer */
|
||||||
|
.faq-answer {
|
||||||
|
display: flex;
|
||||||
|
padding: 0 32rpx 24rpx 32rpx;
|
||||||
|
gap: 16rpx;
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-answer-icon {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #d4af37;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-answer-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #5a5a5a;
|
||||||
|
line-height: 1.8;
|
||||||
|
padding-top: 8rpx;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty */
|
||||||
|
.faq-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 400rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-empty-icon {
|
||||||
|
font-size: 96rpx;
|
||||||
|
opacity: 0.3;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-empty-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contact */
|
||||||
|
.faq-contact {
|
||||||
|
margin-top: 32rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
background-color: #fffdf9;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-contact-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2c2c2c;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-contact-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
background-color: #8b2323;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-contact-btn-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
556
前端源码/uni-app/components/screens/ProfileFavorites.vue
Normal file
556
前端源码/uni-app/components/screens/ProfileFavorites.vue
Normal file
@@ -0,0 +1,556 @@
|
|||||||
|
<template>
|
||||||
|
<view class="favorites-screen">
|
||||||
|
<view class="favorites-bg"></view>
|
||||||
|
<!-- 状态栏占位 -->
|
||||||
|
<view class="status-bar-placeholder"></view>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<view class="favorites-header">
|
||||||
|
<view class="favorites-back-btn" @click="handleBack">
|
||||||
|
<text class="favorites-back-icon">‹</text>
|
||||||
|
</view>
|
||||||
|
<text class="favorites-title">我的收藏</text>
|
||||||
|
<view class="favorites-header-placeholder"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<view class="favorites-tabs">
|
||||||
|
<view class="favorites-tabs-container">
|
||||||
|
<view v-for="tab in tabs" :key="tab.value" class="favorites-tab-btn"
|
||||||
|
:class="{ 'favorites-tab-btn-active': activeTab === tab.value }" @click="switchTab(tab.value)">
|
||||||
|
<text class="favorites-tab-text" :class="{ 'favorites-tab-text-active': activeTab === tab.value }">
|
||||||
|
{{ tab.label }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- List -->
|
||||||
|
<scroll-view scroll-y class="favorites-list" @scrolltolower="loadMore">
|
||||||
|
<view class="favorites-list-inner">
|
||||||
|
<view v-if="loading && filteredList.length === 0" class="favorites-loading">
|
||||||
|
<text class="favorites-loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-else-if="filteredList.length > 0">
|
||||||
|
<view v-for="(item, index) in filteredList" :key="item.id" class="favorites-item"
|
||||||
|
:style="{ animationDelay: (index * 0.05) + 's' }" @click="viewDetail(item)">
|
||||||
|
<!-- Left Border Accent -->
|
||||||
|
<view class="favorites-item-border"
|
||||||
|
:class="item.category === 'company' ? 'favorites-item-border-gold' : 'favorites-item-border-red'"></view>
|
||||||
|
|
||||||
|
<view class="favorites-item-main">
|
||||||
|
<view class="favorites-item-header">
|
||||||
|
<text class="favorites-item-name">{{ item.name || '未命名' }}</text>
|
||||||
|
<view class="favorites-item-type-icon">
|
||||||
|
<text class="favorites-item-type-text">{{ item.category === 'company' ? '企' : '名' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text v-if="item.pinyin" class="favorites-item-pinyin">{{ item.pinyin }}</text>
|
||||||
|
<view class="favorites-item-bottom">
|
||||||
|
<view class="favorites-item-tags">
|
||||||
|
<text v-for="(tag, idx) in item.tags" :key="idx" class="favorites-item-tag"
|
||||||
|
:class="item.category === 'company' ? 'favorites-item-tag-gold' : 'favorites-item-tag-red'">
|
||||||
|
{{ tag }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<text class="favorites-item-date-inline">{{ formatDate(item.created_time) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="favorites-item-right">
|
||||||
|
<view class="favorites-item-heart">
|
||||||
|
<text class="favorites-item-heart-icon">❤</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="hasMore" class="favorites-load-more">
|
||||||
|
<text class="favorites-load-more-text">{{ loading ? '加载中...' : '上拉加载更多' }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="favorites-no-more">
|
||||||
|
<text class="favorites-no-more-text">— 已加载全部 —</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<view class="favorites-empty">
|
||||||
|
<text class="favorites-empty-icon">⭐</text>
|
||||||
|
<text class="favorites-empty-text">暂无收藏</text>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
import { userApi } from "@/api";
|
||||||
|
import type { MyFavoriteItem } from "@/api/types";
|
||||||
|
|
||||||
|
declare const uni: any;
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
back: [];
|
||||||
|
navigate: [screen: string];
|
||||||
|
showDetail: [data: any, category?: string, serviceType?: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const list = ref<MyFavoriteItem[]>([]);
|
||||||
|
const pageNo = ref(1);
|
||||||
|
const pageSize = 10;
|
||||||
|
const total = ref(0);
|
||||||
|
const hasMore = ref(true);
|
||||||
|
const activeTab = ref<'all' | 'personal' | 'company'>('all');
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ value: 'all' as const, label: '全部' },
|
||||||
|
{ value: 'personal' as const, label: '个人名' },
|
||||||
|
{ value: 'company' as const, label: '商号' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 根据tab过滤列表
|
||||||
|
const filteredList = computed(() => {
|
||||||
|
if (activeTab.value === 'all') return list.value;
|
||||||
|
return list.value.filter(item => item.category === activeTab.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadList = async (refresh = false) => {
|
||||||
|
if (loading.value) return;
|
||||||
|
if (!refresh && !hasMore.value) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
if (refresh) {
|
||||||
|
pageNo.value = 1;
|
||||||
|
list.value = [];
|
||||||
|
}
|
||||||
|
const res = await userApi.getMyFavorites({
|
||||||
|
page_no: pageNo.value,
|
||||||
|
page_size: pageSize,
|
||||||
|
category: activeTab.value === 'all' ? undefined : activeTab.value,
|
||||||
|
});
|
||||||
|
console.log('getMyFavorites response:', res);
|
||||||
|
const items = res?.items || (Array.isArray(res) ? res : []);
|
||||||
|
list.value = refresh ? items : [...list.value, ...items];
|
||||||
|
total.value = res?.total || items.length;
|
||||||
|
hasMore.value = list.value.length < total.value;
|
||||||
|
pageNo.value++;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('loadList error:', e);
|
||||||
|
uni.showToast({ title: e.msg || "加载失败", icon: "none" });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchTab = (tab: 'all' | 'personal' | 'company') => {
|
||||||
|
activeTab.value = tab;
|
||||||
|
loadList(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadMore = () => {
|
||||||
|
if (!loading.value && hasMore.value) loadList();
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseMaybeJson = (value: any): any => {
|
||||||
|
if (value == null) return value;
|
||||||
|
if (typeof value === 'object') return value;
|
||||||
|
if (typeof value !== 'string') return value;
|
||||||
|
const raw = value.trim();
|
||||||
|
if (!raw) return value;
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewDetail = async (item: MyFavoriteItem) => {
|
||||||
|
if (!item.solution_id) {
|
||||||
|
uni.showToast({ title: "方案ID不存在", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const detailData = await userApi.getSolutionDetail(item.solution_id);
|
||||||
|
const parsedData = parseMaybeJson(detailData);
|
||||||
|
const serviceType = String((item as any)?.service_type || '');
|
||||||
|
emit('showDetail', parsedData, item.category, serviceType);
|
||||||
|
} catch (e: any) {
|
||||||
|
uni.showToast({ title: e.msg || "加载详情失败", icon: "none" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
emit('back');
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateStr: string) => {
|
||||||
|
if (!dateStr) return "";
|
||||||
|
// 处理 created_time 格式 "2026-01-13"
|
||||||
|
return dateStr.replace(/-/g, ".");
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => loadList(true));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.favorites-screen {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f0efe9;
|
||||||
|
position: relative;
|
||||||
|
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 强制所有容器全宽 */
|
||||||
|
:deep(*) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.shell) {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar-placeholder {
|
||||||
|
height: var(--status-bar-height, 0);
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.3;
|
||||||
|
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.favorites-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-bottom: 1px solid #dcd3c9;
|
||||||
|
background-color: rgba(253, 251, 247, 0.8);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-back-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-back-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #5a5a5a;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-header-placeholder {
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.favorites-tabs {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background-color: #fdfbf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-tabs-container {
|
||||||
|
display: flex;
|
||||||
|
background-color: #f0efe9;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-tab-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 0;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-tab-btn-active {
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-tab-text {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-tab-text-active {
|
||||||
|
color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List */
|
||||||
|
.favorites-list {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-list-inner {
|
||||||
|
padding: 16px;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #fffdf9;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
animation: fadeInUp 0.3s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-border {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-border-red {
|
||||||
|
background-color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-border-gold {
|
||||||
|
background-color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-family: "SimSun", "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-type-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-type-text {
|
||||||
|
font-size: 9px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-pinyin {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: block;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-bottom {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-category {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-tag {
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #5a5a5a;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-tag-red {
|
||||||
|
background-color: rgba(139, 35, 35, 0.05);
|
||||||
|
color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-tag-gold {
|
||||||
|
background-color: rgba(212, 175, 55, 0.05);
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-date-inline {
|
||||||
|
font-size: 9px;
|
||||||
|
color: #ccc;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-date {
|
||||||
|
font-size: 9px;
|
||||||
|
color: #ccc;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-heart {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-item-heart-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-loading-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-load-more,
|
||||||
|
.favorites-no-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-load-more-text,
|
||||||
|
.favorites-no-more-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-empty-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
opacity: 0.3;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-empty-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-empty-btn {
|
||||||
|
background-color: #8b2323;
|
||||||
|
padding: 10px 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-empty-btn-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #d4af37;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
477
前端源码/uni-app/components/screens/ProfileFeedbackScreen.vue
Normal file
477
前端源码/uni-app/components/screens/ProfileFeedbackScreen.vue
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
<template>
|
||||||
|
<view class="feedback-screen">
|
||||||
|
<view class="feedback-bg"></view>
|
||||||
|
<!-- 状态栏占位 -->
|
||||||
|
<view class="status-bar-placeholder"></view>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<view class="feedback-header">
|
||||||
|
<view class="feedback-back-btn" @click="handleBack">
|
||||||
|
<text class="feedback-back-icon">‹</text>
|
||||||
|
</view>
|
||||||
|
<text class="feedback-title">意见反馈</text>
|
||||||
|
<view class="feedback-header-placeholder"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<scroll-view scroll-y class="feedback-content">
|
||||||
|
<view class="feedback-content-inner">
|
||||||
|
|
||||||
|
<!-- 反馈类型 -->
|
||||||
|
<view class="feedback-section">
|
||||||
|
<text class="feedback-section-title">反馈类型</text>
|
||||||
|
<view class="feedback-type-list">
|
||||||
|
<view v-for="type in feedbackTypes" :key="type.value" class="feedback-type-item"
|
||||||
|
:class="{ 'feedback-type-item-active': selectedType === type.value }"
|
||||||
|
@click="selectType(type.value)">
|
||||||
|
<text class="feedback-type-label">{{ type.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 反馈内容 -->
|
||||||
|
<view class="feedback-section">
|
||||||
|
<text class="feedback-section-title">反馈内容</text>
|
||||||
|
<textarea class="feedback-textarea" v-model="feedbackContent"
|
||||||
|
placeholder="请详细描述您遇到的问题或建议,我们会认真对待每一条反馈..." :maxlength="500"
|
||||||
|
placeholder-class="feedback-textarea-placeholder" />
|
||||||
|
<view class="feedback-textarea-counter">
|
||||||
|
<text class="feedback-textarea-counter-text">{{ feedbackContent.length }}/500</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 上传图片 -->
|
||||||
|
<view class="feedback-section">
|
||||||
|
<text class="feedback-section-title">上传图片(选填)</text>
|
||||||
|
<view class="feedback-images">
|
||||||
|
<view v-for="(img, index) in uploadedImages" :key="index" class="feedback-image-item">
|
||||||
|
<image :src="img" class="feedback-image" mode="aspectFill" />
|
||||||
|
<view class="feedback-image-delete" @click="deleteImage(index)">
|
||||||
|
<text class="feedback-image-delete-icon">×</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="uploadedImages.length < 3" class="feedback-image-upload" @click="chooseImage">
|
||||||
|
<text class="feedback-image-upload-icon">+</text>
|
||||||
|
<text class="feedback-image-upload-text">添加图片</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="feedback-images-tip">最多上传3张图片,每张不超过5MB</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 联系方式 -->
|
||||||
|
<view class="feedback-section">
|
||||||
|
<text class="feedback-section-title">联系方式(选填)</text>
|
||||||
|
<input class="feedback-input" v-model="contactInfo" placeholder="请输入手机号或微信号,方便我们联系您"
|
||||||
|
placeholder-class="feedback-input-placeholder" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<view class="feedback-submit-btn" @click="submitFeedback">
|
||||||
|
<text class="feedback-submit-btn-text">提交反馈</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 温馨提示 -->
|
||||||
|
<view class="feedback-tips">
|
||||||
|
<text class="feedback-tips-title">温馨提示</text>
|
||||||
|
<text class="feedback-tips-text">• 我们会在1-3个工作日内处理您的反馈</text>
|
||||||
|
<text class="feedback-tips-text">• 如需回复,请留下您的联系方式</text>
|
||||||
|
<text class="feedback-tips-text">• 感谢您对壹梵起名的支持与建议</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { userApi } from "@/api";
|
||||||
|
|
||||||
|
declare const uni: any;
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
back: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 反馈类型
|
||||||
|
const feedbackTypes = [
|
||||||
|
{ value: 'suggestion', label: '功能建议', icon: '💡' },
|
||||||
|
{ value: 'bug', label: '问题反馈', icon: '🐛' },
|
||||||
|
{ value: 'complaint', label: '投诉建议', icon: '📢' },
|
||||||
|
{ value: 'other', label: '其他', icon: '💬' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedType = ref<'suggestion' | 'bug' | 'complaint' | 'other'>('suggestion');
|
||||||
|
const feedbackContent = ref("");
|
||||||
|
const uploadedImages = ref<string[]>([]);
|
||||||
|
const contactInfo = ref("");
|
||||||
|
|
||||||
|
const selectType = (type: 'suggestion' | 'bug' | 'complaint' | 'other') => {
|
||||||
|
selectedType.value = type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const chooseImage = async () => {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 3 - uploadedImages.value.length,
|
||||||
|
sizeType: ['compressed'],
|
||||||
|
sourceType: ['album', 'camera'],
|
||||||
|
success: async (res: any) => {
|
||||||
|
const tempFilePaths = res.tempFilePaths;
|
||||||
|
|
||||||
|
uni.showLoading({ title: '上传中...' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 逐个上传图片到服务器
|
||||||
|
for (const filePath of tempFilePaths) {
|
||||||
|
const result = await userApi.uploadImage(filePath);
|
||||||
|
// 使用服务器返回的file_url
|
||||||
|
uploadedImages.value.push(result.file_url);
|
||||||
|
}
|
||||||
|
uni.hideLoading();
|
||||||
|
} catch (error: any) {
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showToast({
|
||||||
|
title: error.msg || '图片上传失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteImage = (index: number) => {
|
||||||
|
uploadedImages.value.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitFeedback = async () => {
|
||||||
|
if (!feedbackContent.value.trim()) {
|
||||||
|
uni.showToast({ title: "请输入反馈内容", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await userApi.submitFeedback({
|
||||||
|
content: feedbackContent.value.trim(),
|
||||||
|
images: uploadedImages.value.join(','),
|
||||||
|
contact: contactInfo.value.trim(),
|
||||||
|
feedback_type: selectedType.value
|
||||||
|
});
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: "提交成功,感谢您的反馈!",
|
||||||
|
icon: "success",
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
|
||||||
|
// 延迟返回,让用户看到成功提示
|
||||||
|
setTimeout(() => {
|
||||||
|
handleBack();
|
||||||
|
}, 2000);
|
||||||
|
} catch (error: any) {
|
||||||
|
uni.showToast({
|
||||||
|
title: error.msg || "提交失败,请稍后重试",
|
||||||
|
icon: "none"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
emit('back');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.feedback-screen {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f0efe9;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar-placeholder {
|
||||||
|
height: var(--status-bar-height, 0);
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.3;
|
||||||
|
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.feedback-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
border-bottom: 1rpx solid #dcd3c9;
|
||||||
|
background-color: rgba(253, 251, 247, 0.8);
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-back-btn {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: -16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-back-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #5a5a5a;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-header-placeholder {
|
||||||
|
width: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content */
|
||||||
|
.feedback-content {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-content-inner {
|
||||||
|
padding: 32rpx;
|
||||||
|
padding-bottom: calc(32rpx + env(safe-area-inset-bottom, 0px));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section */
|
||||||
|
.feedback-section {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-section-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Type List */
|
||||||
|
.feedback-type-list {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-type-item {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 150rpx;
|
||||||
|
padding: 20rpx 16rpx;
|
||||||
|
background-color: #fffdf9;
|
||||||
|
border: 2rpx solid #e5e5e5;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-type-item-active {
|
||||||
|
background-color: #8b2323;
|
||||||
|
border-color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-type-icon {
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-type-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-type-item-active .feedback-type-label {
|
||||||
|
color: #f2e6d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Textarea */
|
||||||
|
.feedback-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 240rpx;
|
||||||
|
background-color: #fffdf9;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2c2c2c;
|
||||||
|
box-sizing: border-box;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-textarea-placeholder {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-textarea-counter {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-textarea-counter-text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Images */
|
||||||
|
.feedback-images {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-image-item {
|
||||||
|
position: relative;
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-image-delete {
|
||||||
|
position: absolute;
|
||||||
|
top: -8rpx;
|
||||||
|
right: -8rpx;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background-color: #8b2323;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-image-delete-icon {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-image-upload {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
background-color: #fffdf9;
|
||||||
|
border: 2rpx dashed #e5e5e5;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-image-upload-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #ccc;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-image-upload-text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-images-tip {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #999;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input */
|
||||||
|
.feedback-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
background-color: #fffdf9;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2c2c2c;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-input-placeholder {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Submit Button */
|
||||||
|
.feedback-submit-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 28rpx 0;
|
||||||
|
background-color: #8b2323;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-submit-btn:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-submit-btn-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f2e6d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tips */
|
||||||
|
.feedback-tips {
|
||||||
|
background-color: rgba(255, 253, 249, 0.6);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-tips-title {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #8b2323;
|
||||||
|
margin-bottom: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-tips-text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
470
前端源码/uni-app/components/screens/ProfileOrderDetailScreen.vue
Normal file
470
前端源码/uni-app/components/screens/ProfileOrderDetailScreen.vue
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
<template>
|
||||||
|
<view class="order-detail-screen">
|
||||||
|
<view class="order-detail-bg"></view>
|
||||||
|
<view class="status-bar-placeholder"></view>
|
||||||
|
|
||||||
|
<view class="order-detail-header">
|
||||||
|
<view class="order-detail-back-btn" @click="emit('back')">
|
||||||
|
<text class="order-detail-back-icon">‹</text>
|
||||||
|
</view>
|
||||||
|
<text class="order-detail-title">订单详情</text>
|
||||||
|
<view class="order-detail-header-placeholder"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="order-detail-content">
|
||||||
|
<view class="order-detail-inner">
|
||||||
|
<view class="detail-card detail-card-highlight">
|
||||||
|
<view class="detail-row">
|
||||||
|
<text class="detail-label">订单状态</text>
|
||||||
|
<text class="detail-status" :class="`status-${order.status}`">{{ getStatusText(order.status) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-price-wrap">
|
||||||
|
<text class="detail-price-symbol">¥</text>
|
||||||
|
<text class="detail-price">{{ displayAmount }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="detail-desc">{{ order.description || getBusinessTypeName(order.business_type) }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="detail-card">
|
||||||
|
<view class="section-title">订单信息</view>
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-item-label">订单号</text>
|
||||||
|
<text class="detail-item-value detail-item-mono">{{ order.out_trade_no || '-' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-item-label">微信订单号</text>
|
||||||
|
<text class="detail-item-value detail-item-mono">{{ order.transaction_id || '-' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-item-label">业务类型</text>
|
||||||
|
<text class="detail-item-value">{{ getBusinessTypeName(order.business_type) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-item-label">业务ID</text>
|
||||||
|
<text class="detail-item-value">{{ order.business_id ?? '-' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="detail-card">
|
||||||
|
<view class="section-title">支付信息</view>
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-item-label">应付金额</text>
|
||||||
|
<text class="detail-item-value">¥{{ order.total_amount ?? '-' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-item-label">实付金额</text>
|
||||||
|
<text class="detail-item-value">¥{{ order.paid_amount ?? order.total_amount ?? '-' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-item-label">支付时间</text>
|
||||||
|
<text class="detail-item-value">{{ order.paid_at || '待支付' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="order-action-bar" v-if="showActionBar">
|
||||||
|
<view v-if="isPending" class="order-action-btn order-action-cancel" @click="handleCancelOrder">
|
||||||
|
<text class="order-action-text">取消订单</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="isPending" class="order-action-btn order-action-pay" @click="handlePayOrder">
|
||||||
|
<text class="order-action-text order-action-text-light">{{ actionLoading ? '处理中...' : '继续支付' }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="isPending" class="order-action-btn order-action-plain" @click="refreshOrderStatus()">
|
||||||
|
<text class="order-action-text">刷新状态</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-else-if="order.status === 'paid'">
|
||||||
|
<view class="order-action-btn order-action-pay" @click="handleOpenBusiness">
|
||||||
|
<text class="order-action-text order-action-text-light">查看对应业务</text>
|
||||||
|
</view>
|
||||||
|
<view class="order-action-btn order-action-plain" @click="emit('back')">
|
||||||
|
<text class="order-action-text">返回订单列表</text>
|
||||||
|
</view>
|
||||||
|
<view class="order-action-btn order-action-pay" @click="refreshOrderStatus()">
|
||||||
|
<text class="order-action-text order-action-text-light">刷新状态</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view v-else class="order-action-btn order-action-plain" @click="emit('back')">
|
||||||
|
<text class="order-action-text">返回订单列表</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch, onMounted } from 'vue';
|
||||||
|
import type { QueryOrderResponse } from '@/api/types';
|
||||||
|
import { closeOrder, wxPay } from '@/utils/payment';
|
||||||
|
import { paymentApi } from '@/api/payment';
|
||||||
|
|
||||||
|
declare const uni: any;
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data?: QueryOrderResponse | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
back: [];
|
||||||
|
openBusiness: [order: QueryOrderResponse];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const buildFallbackOrder = (): QueryOrderResponse => ({
|
||||||
|
out_trade_no: '',
|
||||||
|
status: 'pending',
|
||||||
|
total_amount: 0,
|
||||||
|
business_type: '',
|
||||||
|
business_id: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const localOrder = ref<QueryOrderResponse>(props.data || buildFallbackOrder());
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
(next) => {
|
||||||
|
localOrder.value = next || buildFallbackOrder();
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const order = computed(() => localOrder.value);
|
||||||
|
|
||||||
|
const displayAmount = computed(() => order.value.paid_amount ?? order.value.total_amount ?? 0);
|
||||||
|
const isPending = computed(() => order.value.status === 'pending');
|
||||||
|
const showActionBar = computed(() => Boolean(order.value.out_trade_no));
|
||||||
|
const actionLoading = ref(false);
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
const statusMap: Record<string, string> = {
|
||||||
|
pending: '待支付',
|
||||||
|
paid: '已支付',
|
||||||
|
cancelled: '已关闭',
|
||||||
|
refunded: '已退款',
|
||||||
|
};
|
||||||
|
return statusMap[status] || status;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBusinessTypeName = (type: string) => {
|
||||||
|
const typeMap: Record<string, string> = {
|
||||||
|
naming_report: '命名报告',
|
||||||
|
partner_apply: '推广合伙人',
|
||||||
|
test_report: '测名报告',
|
||||||
|
fortune_report: '财运报告',
|
||||||
|
test: '测试商品',
|
||||||
|
};
|
||||||
|
return typeMap[type] || type || '-';
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshOrderStatus = async (silent = false) => {
|
||||||
|
if (!order.value.out_trade_no || actionLoading.value) return;
|
||||||
|
|
||||||
|
actionLoading.value = true;
|
||||||
|
try {
|
||||||
|
const latest = await paymentApi.queryOrder(order.value.out_trade_no);
|
||||||
|
if (latest) {
|
||||||
|
localOrder.value = latest;
|
||||||
|
if (!silent) {
|
||||||
|
uni.showToast({ title: `状态已更新:${getStatusText(latest.status)}`, icon: 'none' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
if (!silent) {
|
||||||
|
uni.showToast({ title: e?.msg || '刷新失败', icon: 'none' });
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
actionLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelOrder = () => {
|
||||||
|
if (!order.value.out_trade_no || actionLoading.value) return;
|
||||||
|
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定要取消该订单吗?',
|
||||||
|
success: async (res: any) => {
|
||||||
|
if (!res.confirm) return;
|
||||||
|
actionLoading.value = true;
|
||||||
|
try {
|
||||||
|
const success = await closeOrder(order.value.out_trade_no);
|
||||||
|
if (success) {
|
||||||
|
uni.showToast({ title: '订单已取消', icon: 'success' });
|
||||||
|
await refreshOrderStatus(true);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
actionLoading.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePayOrder = async () => {
|
||||||
|
if (actionLoading.value) return;
|
||||||
|
|
||||||
|
actionLoading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await wxPay({
|
||||||
|
description: order.value.description || getBusinessTypeName(order.value.business_type),
|
||||||
|
total_amount: order.value.total_amount,
|
||||||
|
business_type: order.value.business_type,
|
||||||
|
business_id: order.value.business_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
uni.showToast({ title: '支付成功', icon: 'success' });
|
||||||
|
await refreshOrderStatus(true);
|
||||||
|
if (localOrder.value.status === 'paid') {
|
||||||
|
handleOpenBusiness();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showToast({ title: result.msg || '支付失败', icon: 'none' });
|
||||||
|
} catch (e: any) {
|
||||||
|
uni.showToast({ title: e?.msg || '支付失败', icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
actionLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenBusiness = () => {
|
||||||
|
emit('openBusiness', order.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (order.value.out_trade_no && order.value.status === 'pending') {
|
||||||
|
refreshOrderStatus(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.order-detail-screen {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f0efe9;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar-placeholder {
|
||||||
|
height: var(--status-bar-height, 0);
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.3;
|
||||||
|
background-image: url('https://www.transparenttextures.com/patterns/rice-paper.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
border-bottom: 1rpx solid #dcd3c9;
|
||||||
|
background-color: rgba(253, 251, 247, 0.8);
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail-back-btn {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: -16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail-back-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #5a5a5a;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail-header-placeholder {
|
||||||
|
width: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail-content {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail-inner {
|
||||||
|
padding: 32rpx;
|
||||||
|
padding-bottom: calc(180rpx + env(safe-area-inset-bottom, 0px));
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card {
|
||||||
|
background-color: #fffdf9;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
padding: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card-highlight {
|
||||||
|
border-color: #e5d6c4;
|
||||||
|
background: linear-gradient(135deg, #fffdf9 0%, #f9f4ee 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-status {
|
||||||
|
font-size: 24rpx;
|
||||||
|
padding: 4rpx 16rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-paid {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending {
|
||||||
|
background-color: #fff3e0;
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cancelled {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-refunded {
|
||||||
|
background-color: #ffebee;
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-price-wrap {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-price-symbol {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #8b2323;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-price {
|
||||||
|
font-size: 56rpx;
|
||||||
|
color: #8b2323;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-desc {
|
||||||
|
display: block;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 16rpx 0;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item-value {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #333;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item-mono {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-bar {
|
||||||
|
position: relative;
|
||||||
|
z-index: 12;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 20rpx 24rpx calc(20rpx + env(safe-area-inset-bottom, 0px));
|
||||||
|
border-top: 1rpx solid #e5e5e5;
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
backdrop-filter: blur(8rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-btn {
|
||||||
|
flex: 1 1 220rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-text-light {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-cancel,
|
||||||
|
.order-action-plain {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-pay {
|
||||||
|
background-color: #8b2323;
|
||||||
|
border-color: #8b2323;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
576
前端源码/uni-app/components/screens/ProfileOrdersScreen.vue
Normal file
576
前端源码/uni-app/components/screens/ProfileOrdersScreen.vue
Normal file
@@ -0,0 +1,576 @@
|
|||||||
|
<template>
|
||||||
|
<view class="orders-screen">
|
||||||
|
<view class="orders-bg"></view>
|
||||||
|
<!-- 状态栏占位 -->
|
||||||
|
<view class="status-bar-placeholder"></view>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<view class="orders-header">
|
||||||
|
<view class="orders-back-btn" @click="handleBack">
|
||||||
|
<text class="orders-back-icon">‹</text>
|
||||||
|
</view>
|
||||||
|
<text class="orders-title">我的订单</text>
|
||||||
|
<view class="orders-header-placeholder"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<view class="orders-tabs">
|
||||||
|
<view v-for="tab in tabs" :key="tab.value" class="orders-tab"
|
||||||
|
:class="{ 'orders-tab-active': currentTab === tab.value }" @click="switchTab(tab.value)">
|
||||||
|
<text class="orders-tab-text">{{ tab.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<scroll-view scroll-y class="orders-content" refresher-enabled :refresher-triggered="refreshing"
|
||||||
|
@refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
|
||||||
|
<view class="orders-content-inner">
|
||||||
|
<!-- Loading -->
|
||||||
|
<view v-if="loading && orders.length === 0" class="orders-loading">
|
||||||
|
<text class="orders-loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Empty -->
|
||||||
|
<view v-else-if="orders.length === 0" class="orders-empty">
|
||||||
|
<text class="orders-empty-icon">📦</text>
|
||||||
|
<text class="orders-empty-text">暂无订单</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- List -->
|
||||||
|
<view v-else class="orders-list">
|
||||||
|
<view v-for="order in orders" :key="order.out_trade_no" class="order-item">
|
||||||
|
<!-- Header -->
|
||||||
|
<view class="order-header">
|
||||||
|
<text class="order-time">{{ order.paid_at || '待支付' }}</text>
|
||||||
|
<text class="order-status" :class="`status-${order.status}`">
|
||||||
|
{{ getStatusText(order.status) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<view class="order-content">
|
||||||
|
<view class="order-info">
|
||||||
|
<text class="order-name">{{ order.description ||
|
||||||
|
getBusinessTypeName(order.business_type) }}</text>
|
||||||
|
<text class="order-no">订单号:{{ order.out_trade_no }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="order-price">
|
||||||
|
<text class="order-price-symbol">¥</text>
|
||||||
|
<text class="order-price-amount">{{ order.total_amount }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<view class="order-actions">
|
||||||
|
<view v-if="order.status === 'pending'" class="order-action-btn order-action-cancel"
|
||||||
|
@click="handleCancelOrder(order.out_trade_no)">
|
||||||
|
<text class="order-action-text">取消订单</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="order.status === 'pending'" class="order-action-btn order-action-pay"
|
||||||
|
@click="handlePayOrder(order)">
|
||||||
|
<text class="order-action-text">继续支付</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="order.status === 'paid'" class="order-action-btn order-action-view"
|
||||||
|
@click="handleViewOrder(order)">
|
||||||
|
<text class="order-action-text">查看详情</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Load More -->
|
||||||
|
<view v-if="hasMore && !loading" class="orders-loadmore">
|
||||||
|
<text class="orders-loadmore-text">加载更多...</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { closeOrder, wxPay } from "@/utils/payment";
|
||||||
|
import type { QueryOrderResponse } from "@/api/types";
|
||||||
|
import { paymentApi } from "@/api/payment";
|
||||||
|
|
||||||
|
declare const uni: any;
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
back: [];
|
||||||
|
showOrderDetail: [order: QueryOrderResponse];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ label: '全部', value: 'all' },
|
||||||
|
{ label: '待支付', value: 'pending' },
|
||||||
|
{ label: '已支付', value: 'paid' },
|
||||||
|
{ label: '已关闭', value: 'cancelled' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const currentTab = ref('all');
|
||||||
|
const loading = ref(false);
|
||||||
|
const refreshing = ref(false);
|
||||||
|
const orders = ref<QueryOrderResponse[]>([]);
|
||||||
|
const page = ref(1);
|
||||||
|
const pageSize = 10;
|
||||||
|
const hasMore = ref(true);
|
||||||
|
|
||||||
|
const mapTradeStateToStatus = (tradeState: string): QueryOrderResponse['status'] => {
|
||||||
|
const state = (tradeState || '').toUpperCase();
|
||||||
|
if (state === 'SUCCESS') return 'paid';
|
||||||
|
if (state === 'NOTPAY') return 'pending';
|
||||||
|
if (state === 'CLOSED') return 'cancelled';
|
||||||
|
if (state === 'REFUND') return 'refunded';
|
||||||
|
return 'pending';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换标签
|
||||||
|
const switchTab = (tab: string) => {
|
||||||
|
currentTab.value = tab;
|
||||||
|
page.value = 1;
|
||||||
|
orders.value = [];
|
||||||
|
hasMore.value = true;
|
||||||
|
loadOrders();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载订单列表
|
||||||
|
const loadOrders = async () => {
|
||||||
|
if (loading.value) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await paymentApi.listOrders({
|
||||||
|
page_no: page.value,
|
||||||
|
page_size: pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapped: QueryOrderResponse[] = (res?.items || []).map((item: any) => ({
|
||||||
|
out_trade_no: item.out_trade_no,
|
||||||
|
transaction_id: item.transaction_id,
|
||||||
|
status: mapTradeStateToStatus(item.trade_state),
|
||||||
|
total_amount: item.total_amount,
|
||||||
|
paid_amount: item.total_amount,
|
||||||
|
paid_at: item.success_time,
|
||||||
|
business_type: item.business_type,
|
||||||
|
business_id: item.id,
|
||||||
|
description: item.description,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const filtered = currentTab.value === 'all'
|
||||||
|
? mapped
|
||||||
|
: mapped.filter(o => o.status === currentTab.value);
|
||||||
|
|
||||||
|
if (page.value === 1) {
|
||||||
|
orders.value = filtered;
|
||||||
|
} else {
|
||||||
|
orders.value.push(...filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMore.value = (res?.items || []).length >= pageSize;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('加载订单失败:', error);
|
||||||
|
uni.showToast({ title: '加载失败', icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
refreshing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 下拉刷新
|
||||||
|
const onRefresh = () => {
|
||||||
|
refreshing.value = true;
|
||||||
|
page.value = 1;
|
||||||
|
orders.value = [];
|
||||||
|
hasMore.value = true;
|
||||||
|
loadOrders();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载更多
|
||||||
|
const onLoadMore = () => {
|
||||||
|
if (!hasMore.value || loading.value) return;
|
||||||
|
page.value++;
|
||||||
|
loadOrders();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消订单
|
||||||
|
const handleCancelOrder = (outTradeNo: string) => {
|
||||||
|
// Web环境使用confirm,uni-app环境使用showModal
|
||||||
|
if (typeof uni?.showModal === 'function') {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定要取消该订单吗?',
|
||||||
|
success: async (res: any) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
const success = await closeOrder(outTradeNo);
|
||||||
|
if (success) {
|
||||||
|
// 刷新列表
|
||||||
|
onRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Web环境使用原生confirm
|
||||||
|
const confirmed = confirm('确定要取消该订单吗?');
|
||||||
|
if (confirmed) {
|
||||||
|
(async () => {
|
||||||
|
const success = await closeOrder(outTradeNo);
|
||||||
|
if (success) {
|
||||||
|
// 刷新列表
|
||||||
|
onRefresh();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 继续支付
|
||||||
|
const handlePayOrder = (order: QueryOrderResponse) => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const result = await wxPay({
|
||||||
|
description: order.description || getBusinessTypeName(order.business_type),
|
||||||
|
total_amount: order.total_amount,
|
||||||
|
business_type: order.business_type as any,
|
||||||
|
business_id: order.business_id,
|
||||||
|
pay_type: 'jsapi',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
uni.showToast({ title: '支付成功', icon: 'success' });
|
||||||
|
onRefresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showToast({ title: result.msg || '支付失败', icon: 'none' });
|
||||||
|
} catch (e: any) {
|
||||||
|
uni.showToast({ title: e?.msg || '支付失败', icon: 'none' });
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看订单详情
|
||||||
|
const handleViewOrder = (order: QueryOrderResponse) => {
|
||||||
|
emit('showOrderDetail', order);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
const statusMap: Record<string, string> = {
|
||||||
|
pending: '待支付',
|
||||||
|
paid: '已支付',
|
||||||
|
cancelled: '已关闭',
|
||||||
|
refunded: '已退款'
|
||||||
|
};
|
||||||
|
return statusMap[status] || status;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取业务类型名称
|
||||||
|
const getBusinessTypeName = (type: string) => {
|
||||||
|
const typeMap: Record<string, string> = {
|
||||||
|
naming_report: '命名报告',
|
||||||
|
partner_apply: '推广合伙人',
|
||||||
|
test_report: '测名报告',
|
||||||
|
fortune_report: '财运报告',
|
||||||
|
test: '测试商品'
|
||||||
|
};
|
||||||
|
return typeMap[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回
|
||||||
|
const handleBack = () => {
|
||||||
|
emit('back');
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadOrders();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.orders-screen {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f0efe9;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar-placeholder {
|
||||||
|
height: var(--status-bar-height, 0);
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.3;
|
||||||
|
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.orders-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
border-bottom: 1rpx solid #dcd3c9;
|
||||||
|
background-color: rgba(253, 251, 247, 0.8);
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-back-btn {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: -16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-back-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #5a5a5a;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-header-placeholder {
|
||||||
|
width: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.orders-tabs {
|
||||||
|
display: flex;
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1rpx solid #e5e5e5;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-tab-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-tab-active .orders-tab-text {
|
||||||
|
color: #8b2323;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-tab-active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 60rpx;
|
||||||
|
height: 4rpx;
|
||||||
|
background-color: #8b2323;
|
||||||
|
border-radius: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content */
|
||||||
|
.orders-content {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-content-inner {
|
||||||
|
padding: 32rpx;
|
||||||
|
padding-bottom: calc(32rpx + env(safe-area-inset-bottom, 0px));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading & Empty */
|
||||||
|
.orders-loading,
|
||||||
|
.orders-empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 120rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-loading-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-empty-icon {
|
||||||
|
font-size: 120rpx;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-empty-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Order List */
|
||||||
|
.orders-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-item {
|
||||||
|
background-color: #fffdf9;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-time {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status {
|
||||||
|
font-size: 24rpx;
|
||||||
|
padding: 4rpx 16rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-paid {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending {
|
||||||
|
background-color: #fff3e0;
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cancelled {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-refunded {
|
||||||
|
background-color: #ffebee;
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-name {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-no {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-price {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-price-symbol {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #8b2323;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-price-amount {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #8b2323;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
|
.order-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 0 24rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-btn {
|
||||||
|
padding: 12rpx 32rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-cancel {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-pay {
|
||||||
|
background-color: #8b2323;
|
||||||
|
border-color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-pay .order-action-text {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-action-view {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Load More */
|
||||||
|
.orders-loadmore {
|
||||||
|
text-align: center;
|
||||||
|
padding: 32rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-loadmore-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
6
前端源码/uni-app/components/screens/ProfilePlaceholder.vue
Normal file
6
前端源码/uni-app/components/screens/ProfilePlaceholder.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<view class="h-full flex items-center justify-center text-[#5a5a5a]">
|
||||||
|
<text>已替换为 ProfileScreen.vue</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
284
前端源码/uni-app/components/screens/ProfilePrivacy.vue
Normal file
284
前端源码/uni-app/components/screens/ProfilePrivacy.vue
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
<template>
|
||||||
|
<view class="privacy-screen">
|
||||||
|
<view class="privacy-bg"></view>
|
||||||
|
<!-- 状态栏占位 -->
|
||||||
|
<view class="status-bar-placeholder"></view>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<view class="privacy-header">
|
||||||
|
<view class="privacy-back-btn" @click="handleBack">
|
||||||
|
<text class="privacy-back-icon">‹</text>
|
||||||
|
</view>
|
||||||
|
<text class="privacy-title">隐私政策</text>
|
||||||
|
<view class="privacy-header-placeholder"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<scroll-view scroll-y class="privacy-content">
|
||||||
|
<view class="privacy-content-inner">
|
||||||
|
|
||||||
|
<view v-if="loading" class="privacy-loading">
|
||||||
|
<text class="privacy-loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-else-if="policy">
|
||||||
|
<!-- 标题和版本信息 -->
|
||||||
|
<view class="privacy-header-info">
|
||||||
|
<text class="privacy-doc-title">{{ policy.title }}</text>
|
||||||
|
<view class="privacy-meta">
|
||||||
|
<text class="privacy-meta-item">版本:{{ policy.version }}</text>
|
||||||
|
<text class="privacy-meta-item">生效日期:{{ formatDate(policy.effective_date) }}</text>
|
||||||
|
<text class="privacy-meta-item">更新时间:{{ formatDate(policy.updated_at) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 内容 -->
|
||||||
|
<view class="privacy-body">
|
||||||
|
<rich-text :nodes="formattedContent" class="privacy-rich-text"></rich-text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view v-else class="privacy-empty">
|
||||||
|
<text class="privacy-empty-icon">📄</text>
|
||||||
|
<text class="privacy-empty-text">暂无隐私政策</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部提示 -->
|
||||||
|
<view v-if="policy" class="privacy-footer">
|
||||||
|
<text class="privacy-footer-text">如有疑问,请联系客服</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
import { userApi } from "@/api";
|
||||||
|
|
||||||
|
declare const uni: any;
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
back: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const policy = ref<any>(null);
|
||||||
|
|
||||||
|
// 格式化内容(将换行符转换为HTML)
|
||||||
|
const formattedContent = computed(() => {
|
||||||
|
if (!policy.value?.content) return '';
|
||||||
|
|
||||||
|
// 简单的文本格式化:将换行符转换为<br>,段落添加样式
|
||||||
|
let content = policy.value.content;
|
||||||
|
|
||||||
|
// 如果内容包含HTML标签,直接返回
|
||||||
|
if (content.includes('<')) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则进行简单格式化
|
||||||
|
content = content
|
||||||
|
.replace(/\n\n/g, '</p><p>')
|
||||||
|
.replace(/\n/g, '<br>')
|
||||||
|
.replace(/^(.+)$/, '<p>$1</p>');
|
||||||
|
|
||||||
|
return content;
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadPrivacyPolicy = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await userApi.getPrivacyPolicy();
|
||||||
|
console.log('getPrivacyPolicy response:', res);
|
||||||
|
policy.value = res;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('loadPrivacyPolicy error:', e);
|
||||||
|
uni.showToast({ title: e.msg || "加载失败", icon: "none" });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateStr: string) => {
|
||||||
|
if (!dateStr) return "";
|
||||||
|
return dateStr.split("T")[0].replace(/-/g, ".");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
emit('back');
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => loadPrivacyPolicy());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.privacy-screen {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f0efe9;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar-placeholder {
|
||||||
|
height: var(--status-bar-height, 0);
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.3;
|
||||||
|
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.privacy-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
border-bottom: 1rpx solid #dcd3c9;
|
||||||
|
background-color: rgba(253, 251, 247, 0.8);
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-back-btn {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: -16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-back-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #5a5a5a;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-header-placeholder {
|
||||||
|
width: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content */
|
||||||
|
.privacy-content {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-content-inner {
|
||||||
|
padding: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading */
|
||||||
|
.privacy-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 400rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-loading-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Info */
|
||||||
|
.privacy-header-info {
|
||||||
|
background-color: #fffdf9;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
padding: 32rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-doc-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-meta-item {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Body */
|
||||||
|
.privacy-body {
|
||||||
|
background-color: #fffdf9;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
padding: 32rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-rich-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2c2c2c;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty */
|
||||||
|
.privacy-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 400rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-empty-icon {
|
||||||
|
font-size: 96rpx;
|
||||||
|
opacity: 0.3;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-empty-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.privacy-footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 32rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-footer-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
415
前端源码/uni-app/components/screens/ProfileReports.vue
Normal file
415
前端源码/uni-app/components/screens/ProfileReports.vue
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
<template>
|
||||||
|
<view class="reports-screen">
|
||||||
|
<view class="reports-bg"></view>
|
||||||
|
|
||||||
|
<!-- 状态栏占位 -->
|
||||||
|
<view class="status-bar-placeholder"></view>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<view class="reports-header">
|
||||||
|
<view class="reports-back-btn" @click="$emit('back')">
|
||||||
|
<text class="reports-back-icon">‹</text>
|
||||||
|
</view>
|
||||||
|
<text class="reports-title">已解锁报告</text>
|
||||||
|
<view class="reports-header-placeholder"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<scroll-view scroll-y class="reports-list">
|
||||||
|
<template v-if="reports.length > 0">
|
||||||
|
<view v-for="(item, index) in reports" :key="item.id" class="reports-item"
|
||||||
|
:style="{ animationDelay: index * 0.1 + 's' }">
|
||||||
|
<!-- Card Header accent -->
|
||||||
|
<view class="reports-item-accent"
|
||||||
|
:class="item.type === 'fortune' ? 'reports-item-accent-gold' : 'reports-item-accent-red'"></view>
|
||||||
|
|
||||||
|
<view class="reports-item-body">
|
||||||
|
<!-- Icon -->
|
||||||
|
<view class="reports-item-icon"
|
||||||
|
:class="item.type === 'fortune' ? 'reports-item-icon-gold' : 'reports-item-icon-red'">
|
||||||
|
<text class="reports-item-icon-text">报</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="reports-item-content">
|
||||||
|
<text class="reports-item-title">{{ item.title }}</text>
|
||||||
|
<view class="reports-item-meta">
|
||||||
|
<text class="reports-item-date">{{ item.date }}</text>
|
||||||
|
<view class="reports-item-divider"></view>
|
||||||
|
<text class="reports-item-price">已支付 {{ item.price }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="reports-item-actions">
|
||||||
|
<view class="reports-btn-download">
|
||||||
|
<text class="reports-btn-download-icon">↓</text>
|
||||||
|
<text class="reports-btn-download-text">下载PDF</text>
|
||||||
|
</view>
|
||||||
|
<view class="reports-btn-preview">
|
||||||
|
<text class="reports-btn-preview-text">在线预览</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<view class="reports-empty">
|
||||||
|
<text class="reports-empty-icon">🔒</text>
|
||||||
|
<text class="reports-empty-text">暂无解锁报告</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Upsell Banner -->
|
||||||
|
<view class="reports-upsell">
|
||||||
|
<view class="reports-upsell-glow"></view>
|
||||||
|
<view class="reports-upsell-content">
|
||||||
|
<view class="reports-upsell-left">
|
||||||
|
<view class="reports-upsell-title-row">
|
||||||
|
<text class="reports-upsell-crown">👑</text>
|
||||||
|
<text class="reports-upsell-title">解锁更多财运玄机</text>
|
||||||
|
</view>
|
||||||
|
<text class="reports-upsell-desc">助您掌握流年运势,趋吉避凶</text>
|
||||||
|
</view>
|
||||||
|
<view class="reports-upsell-btn" @click="$emit('navigate', 'test')">
|
||||||
|
<text class="reports-upsell-btn-text">去测算</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface ReportItem {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
type: 'fortune' | 'naming' | 'renaming';
|
||||||
|
date: string;
|
||||||
|
price: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
back: [];
|
||||||
|
navigate: [screen: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const reports: ReportItem[] = [
|
||||||
|
{ id: '1', title: '2024年度个人财运深度解析', type: 'fortune', date: '2023-12-12', price: '¥666' },
|
||||||
|
{ id: '2', title: '"宏图科技"品牌改名运势报告', type: 'renaming', date: '2023-11-20', price: '¥888' },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.reports-screen {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f0efe9;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar-placeholder {
|
||||||
|
height: var(--status-bar-height, 0);
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.3;
|
||||||
|
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.reports-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
border-bottom: 1rpx solid #dcd3c9;
|
||||||
|
background-color: rgba(253, 251, 247, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-back-btn {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: -16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-back-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #5a5a5a;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-header-placeholder {
|
||||||
|
width: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List */
|
||||||
|
.reports-list {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-item {
|
||||||
|
background-color: #fffdf9;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: fadeInUp 0.3s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-item-accent {
|
||||||
|
height: 8rpx;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-item-accent-gold {
|
||||||
|
background-color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-item-accent-red {
|
||||||
|
background-color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-item-body {
|
||||||
|
padding: 32rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-item-icon {
|
||||||
|
width: 96rpx;
|
||||||
|
height: 112rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-item-icon-gold {
|
||||||
|
background: linear-gradient(180deg, #d4af37 0%, #c4a130 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-item-icon-red {
|
||||||
|
background: linear-gradient(180deg, #8b2323 0%, #701c1c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-item-icon-text {
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-item-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-item-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
line-height: 1.4;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-item-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-item-date,
|
||||||
|
.reports-item-price {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-item-divider {
|
||||||
|
width: 2rpx;
|
||||||
|
height: 20rpx;
|
||||||
|
background-color: #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-item-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-btn-download {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #2c2c2c;
|
||||||
|
color: #f2e6d8;
|
||||||
|
padding: 16rpx 0;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-btn-download-icon {
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-btn-download-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-btn-preview {
|
||||||
|
flex: 1;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
padding: 16rpx 0;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-btn-preview-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #5a5a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.reports-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 400rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-empty-icon {
|
||||||
|
font-size: 96rpx;
|
||||||
|
opacity: 0.2;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-empty-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Upsell Banner */
|
||||||
|
.reports-upsell {
|
||||||
|
margin-top: 48rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
background-color: #2c2c2c;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-upsell-glow {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
background-color: #d4af37;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(100rpx);
|
||||||
|
opacity: 0.2;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-upsell-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-upsell-left {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-upsell-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-upsell-crown {
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-upsell-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-upsell-desc {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: rgba(242, 230, 216, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-upsell-btn {
|
||||||
|
background-color: #d4af37;
|
||||||
|
padding: 16rpx 32rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-upsell-btn-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
433
前端源码/uni-app/components/screens/ProfileSettings.vue
Normal file
433
前端源码/uni-app/components/screens/ProfileSettings.vue
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
<template>
|
||||||
|
<view class="settings-screen">
|
||||||
|
<view class="settings-bg"></view>
|
||||||
|
<!-- 状态栏占位 -->
|
||||||
|
<view class="status-bar-placeholder"></view>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<view class="settings-header">
|
||||||
|
<view class="settings-back-btn" @click="handleBack">
|
||||||
|
<text class="settings-back-icon">‹</text>
|
||||||
|
</view>
|
||||||
|
<text class="settings-title">设置与反馈</text>
|
||||||
|
<view class="settings-header-placeholder"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<scroll-view scroll-y class="settings-content">
|
||||||
|
<view class="settings-content-inner">
|
||||||
|
|
||||||
|
<!-- Section 1: 偏好设置 -->
|
||||||
|
<!-- <view class="settings-section">
|
||||||
|
<view class="settings-item settings-item-border">
|
||||||
|
<view class="settings-item-left">
|
||||||
|
<text class="settings-item-icon">🔔</text>
|
||||||
|
<text class="settings-item-label">推送通知</text>
|
||||||
|
</view>
|
||||||
|
<view class="settings-switch" :class="{ 'settings-switch-active': pushEnabled }"
|
||||||
|
@click="togglePush">
|
||||||
|
<view class="settings-switch-thumb"
|
||||||
|
:class="{ 'settings-switch-thumb-active': pushEnabled }"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="settings-item">
|
||||||
|
<view class="settings-item-left">
|
||||||
|
<text class="settings-item-icon">🔊</text>
|
||||||
|
<text class="settings-item-label">音效反馈</text>
|
||||||
|
</view>
|
||||||
|
<view class="settings-switch" :class="{ 'settings-switch-active': soundEnabled }"
|
||||||
|
@click="toggleSound">
|
||||||
|
<view class="settings-switch-thumb"
|
||||||
|
:class="{ 'settings-switch-thumb-active': soundEnabled }"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view> -->
|
||||||
|
|
||||||
|
<!-- Section 2: 支持 -->
|
||||||
|
<view class="settings-section">
|
||||||
|
<view class="settings-item settings-item-border settings-item-clickable" @click="handleFeedback">
|
||||||
|
<view class="settings-item-left">
|
||||||
|
<text class="settings-item-icon">💬</text>
|
||||||
|
<text class="settings-item-label">意见反馈</text>
|
||||||
|
</view>
|
||||||
|
<text class="settings-item-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="settings-item settings-item-border settings-item-clickable" @click="handleFAQ">
|
||||||
|
<view class="settings-item-left">
|
||||||
|
<text class="settings-item-icon">❓</text>
|
||||||
|
<text class="settings-item-label">常见问题</text>
|
||||||
|
</view>
|
||||||
|
<text class="settings-item-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="settings-item settings-item-clickable" @click="handlePrivacy">
|
||||||
|
<view class="settings-item-left">
|
||||||
|
<text class="settings-item-icon">🛡️</text>
|
||||||
|
<text class="settings-item-label">隐私政策</text>
|
||||||
|
</view>
|
||||||
|
<text class="settings-item-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Section 3: 快速反馈 -->
|
||||||
|
<view class="settings-feedback-section">
|
||||||
|
<text class="settings-feedback-title">快速反馈</text>
|
||||||
|
<textarea class="settings-feedback-textarea" v-model="feedbackText" placeholder="您遇到的问题或建议..."
|
||||||
|
:maxlength="500" />
|
||||||
|
<view class="settings-feedback-btn" @click="submitFeedback">
|
||||||
|
<text class="settings-feedback-btn-text">提交反馈</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 退出登录 -->
|
||||||
|
<view class="settings-logout-btn" @click="handleLogout">
|
||||||
|
<text class="settings-logout-text">退出登录</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 版本信息 -->
|
||||||
|
<view class="settings-version">
|
||||||
|
<text class="settings-version-text">当前版本 v1.0.2</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { userApi } from "@/api";
|
||||||
|
import { logout } from "@/utils/auth";
|
||||||
|
|
||||||
|
declare const uni: any;
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
back: [];
|
||||||
|
navigate: [screen: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const pushEnabled = ref(true);
|
||||||
|
const soundEnabled = ref(true);
|
||||||
|
const feedbackText = ref("");
|
||||||
|
|
||||||
|
const togglePush = () => {
|
||||||
|
pushEnabled.value = !pushEnabled.value;
|
||||||
|
uni.showToast({
|
||||||
|
title: pushEnabled.value ? "已开启推送通知" : "已关闭推送通知",
|
||||||
|
icon: "none"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSound = () => {
|
||||||
|
soundEnabled.value = !soundEnabled.value;
|
||||||
|
uni.showToast({
|
||||||
|
title: soundEnabled.value ? "已开启音效反馈" : "已关闭音效反馈",
|
||||||
|
icon: "none"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFeedback = () => {
|
||||||
|
emit('navigate', 'feedback');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFAQ = () => {
|
||||||
|
emit('navigate', 'faq');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrivacy = () => {
|
||||||
|
emit('navigate', 'privacy');
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitFeedback = async () => {
|
||||||
|
if (!feedbackText.value.trim()) {
|
||||||
|
uni.showToast({ title: "请输入反馈内容", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await userApi.submitFeedback({
|
||||||
|
content: feedbackText.value.trim(),
|
||||||
|
feedback_type: 'other'
|
||||||
|
});
|
||||||
|
uni.showToast({ title: "感谢您的反馈!", icon: "success" });
|
||||||
|
feedbackText.value = "";
|
||||||
|
} catch (error: any) {
|
||||||
|
uni.showToast({
|
||||||
|
title: error.msg || "提交失败,请稍后重试",
|
||||||
|
icon: "none"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
// Web 环境使用 confirm,uni-app 环境使用 showModal
|
||||||
|
if (typeof uni?.showModal === "function") {
|
||||||
|
uni.showModal({
|
||||||
|
title: "提示",
|
||||||
|
content: "确定要退出登录吗?",
|
||||||
|
success: (res: any) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
logout();
|
||||||
|
uni.showToast({ title: "已退出登录", icon: "success" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const confirmed = confirm("确定要退出登录吗?");
|
||||||
|
if (confirmed) {
|
||||||
|
logout();
|
||||||
|
if (typeof uni?.showToast === "function") {
|
||||||
|
uni.showToast({ title: "已退出登录", icon: "success" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
emit('back');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.settings-screen {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f0efe9;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar-placeholder {
|
||||||
|
height: var(--status-bar-height, 0);
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.3;
|
||||||
|
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.settings-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
border-bottom: 1rpx solid #dcd3c9;
|
||||||
|
background-color: rgba(253, 251, 247, 0.8);
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-back-btn {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: -16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-back-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #5a5a5a;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-header-placeholder {
|
||||||
|
width: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content */
|
||||||
|
.settings-content {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content-inner {
|
||||||
|
padding: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section */
|
||||||
|
.settings-section {
|
||||||
|
background-color: #fffdf9;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item-border {
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item-clickable {
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item-clickable:active {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item-icon {
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item-arrow {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Switch */
|
||||||
|
.settings-switch {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
background-color: #ccc;
|
||||||
|
position: relative;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-switch-active {
|
||||||
|
background-color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-switch-thumb {
|
||||||
|
position: absolute;
|
||||||
|
top: 4rpx;
|
||||||
|
left: 4rpx;
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
|
||||||
|
transition: left 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-switch-thumb-active {
|
||||||
|
left: 44rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Feedback Section */
|
||||||
|
.settings-feedback-section {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-feedback-title {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #8b2323;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
display: block;
|
||||||
|
padding: 0 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-feedback-textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 192rpx;
|
||||||
|
background-color: #fffdf9;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2c2c2c;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-feedback-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
background-color: #2c2c2c;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-feedback-btn-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f2e6d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logout Button */
|
||||||
|
.settings-logout-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
background-color: #fffdf9;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-logout-btn:active {
|
||||||
|
background-color: #fff5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-logout-icon {
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-logout-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Version */
|
||||||
|
.settings-version {
|
||||||
|
text-align: center;
|
||||||
|
padding: 32rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-version-text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
248
前端源码/uni-app/components/screens/ProfileUserInfo.vue
Normal file
248
前端源码/uni-app/components/screens/ProfileUserInfo.vue
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
<template>
|
||||||
|
<view class="userinfo-screen">
|
||||||
|
<view class="userinfo-bg"></view>
|
||||||
|
<view class="status-bar-placeholder"></view>
|
||||||
|
|
||||||
|
<view class="userinfo-header">
|
||||||
|
<view class="userinfo-back-btn" @click="handleBack">
|
||||||
|
<text class="userinfo-back-icon">‹</text>
|
||||||
|
</view>
|
||||||
|
<text class="userinfo-title">我的信息</text>
|
||||||
|
<view class="userinfo-header-placeholder"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="userinfo-content">
|
||||||
|
<view class="userinfo-inner">
|
||||||
|
<view class="userinfo-section">
|
||||||
|
<text class="userinfo-label">用户名</text>
|
||||||
|
<input
|
||||||
|
v-model="username"
|
||||||
|
class="userinfo-input"
|
||||||
|
type="text"
|
||||||
|
maxlength="32"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="userinfo-section userinfo-section-mt">
|
||||||
|
<text class="userinfo-label">手机号</text>
|
||||||
|
<input
|
||||||
|
v-model="mobile"
|
||||||
|
class="userinfo-input"
|
||||||
|
type="tel"
|
||||||
|
maxlength="11"
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="userinfo-save" @click="handleSave">
|
||||||
|
<text class="userinfo-save-text">{{ saving ? "保存中…" : "保存" }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { getUserInfo, setUserInfo } from "@/utils/auth";
|
||||||
|
import { userApi } from "@/api";
|
||||||
|
|
||||||
|
declare const uni: any;
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
back: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const username = ref("");
|
||||||
|
const mobile = ref("");
|
||||||
|
const saving = ref(false);
|
||||||
|
|
||||||
|
const isValidPhone = (phone: string) => /^1[3-9]\d{9}$/.test(phone);
|
||||||
|
|
||||||
|
const loadFromStorage = () => {
|
||||||
|
const info = getUserInfo();
|
||||||
|
if (!info || typeof info !== "object") return;
|
||||||
|
const u = info as Record<string, unknown>;
|
||||||
|
username.value = String(
|
||||||
|
u.username ?? u.name ?? ""
|
||||||
|
).trim();
|
||||||
|
mobile.value = String(u.mobile ?? u.phone ?? "").trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadFromStorage();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
emit("back");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
const name = username.value.trim();
|
||||||
|
const tel = mobile.value.trim();
|
||||||
|
if (!name) {
|
||||||
|
uni.showToast({ title: "请输入用户名", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!tel) {
|
||||||
|
uni.showToast({ title: "请输入手机号", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isValidPhone(tel)) {
|
||||||
|
uni.showToast({ title: "请输入正确的手机号", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (saving.value) return;
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
await userApi.updateCurrentUserUsernameMobile({
|
||||||
|
username: name,
|
||||||
|
mobile: tel,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 仅把表单结果合并进已有 userInfo,不把接口 data 整包写入,避免覆盖/清空头像、id 等字段
|
||||||
|
const prev = getUserInfo();
|
||||||
|
if (!prev || typeof prev !== "object") {
|
||||||
|
uni.showToast({ title: "登录状态异常,请重新登录", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setUserInfo({
|
||||||
|
...prev,
|
||||||
|
nickname: name,
|
||||||
|
name: name,
|
||||||
|
username: name,
|
||||||
|
mobile: tel,
|
||||||
|
phone: tel,
|
||||||
|
});
|
||||||
|
|
||||||
|
uni.showToast({ title: "已保存", icon: "success" });
|
||||||
|
} catch (e: any) {
|
||||||
|
uni.showToast({
|
||||||
|
title: e?.msg || e?.message || "保存失败,请稍后重试",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.userinfo-screen {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f0efe9;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar-placeholder {
|
||||||
|
height: var(--status-bar-height, 0);
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.3;
|
||||||
|
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
border-bottom: 1rpx solid #dcd3c9;
|
||||||
|
background-color: rgba(253, 251, 247, 0.8);
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo-back-btn {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: -16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo-back-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #5a5a5a;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo-header-placeholder {
|
||||||
|
width: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo-content {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo-inner {
|
||||||
|
padding: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo-section {
|
||||||
|
background-color: #fffdf9;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
border: 1rpx solid #e5e5e5;
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo-section-mt {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo-input {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #2c2c2c;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo-save {
|
||||||
|
margin-top: 48rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 44rpx;
|
||||||
|
background: linear-gradient(135deg, #8b7355, #6b5344);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo-save-text {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #fffdf9;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1297
前端源码/uni-app/components/screens/Renaming.vue
Normal file
1297
前端源码/uni-app/components/screens/Renaming.vue
Normal file
File diff suppressed because it is too large
Load Diff
190
前端源码/uni-app/components/screens/RenamingDetail.vue
Normal file
190
前端源码/uni-app/components/screens/RenamingDetail.vue
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
<template>
|
||||||
|
<view class="renaming-detail">
|
||||||
|
<view class="detail-header">
|
||||||
|
<view class="detail-header-back" @click="$emit('back')">
|
||||||
|
<text class="back-icon">‹</text>
|
||||||
|
<text class="back-text">返回</text>
|
||||||
|
</view>
|
||||||
|
<text class="detail-header-title">改名详解</text>
|
||||||
|
<view class="detail-header-placeholder"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="detail-content">
|
||||||
|
<view class="hero-card">
|
||||||
|
<text class="hero-name">{{ data?.name || '新名' }}</text>
|
||||||
|
<text class="hero-pinyin">{{ data?.pinyin || '' }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">
|
||||||
|
<text class="section-icon">📖</text>
|
||||||
|
<text class="section-text">寓意</text>
|
||||||
|
</view>
|
||||||
|
<view class="card">
|
||||||
|
<text class="card-text">{{ data?.meaning || '' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">
|
||||||
|
<text class="section-icon">🪶</text>
|
||||||
|
<text class="section-text">出处</text>
|
||||||
|
</view>
|
||||||
|
<view class="card">
|
||||||
|
<text class="card-text">{{ data?.source || '' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section" v-if="data?.tags?.length">
|
||||||
|
<view class="section-title">
|
||||||
|
<text class="section-icon">🏷️</text>
|
||||||
|
<text class="section-text">标签</text>
|
||||||
|
</view>
|
||||||
|
<view class="tag-row">
|
||||||
|
<text v-for="(t, i) in data.tags" :key="i" class="tag">{{ t }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bottom-spacer"></view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
type Mode = 'personal' | 'company';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
data: any;
|
||||||
|
mode?: Mode;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
back: [];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.renaming-detail {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f0efe9;
|
||||||
|
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
border-bottom: 1px solid #eaddcf;
|
||||||
|
background: rgba(253, 251, 247, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header-back {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-icon {
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header-placeholder {
|
||||||
|
width: 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
margin: 32rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
border: 1px solid #dcd3c9;
|
||||||
|
background: #fdfbf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-name {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #8b2323;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-pinyin {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5a5a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin: 0 32rpx 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 24rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
border: 1px solid #dcd3c9;
|
||||||
|
background: #f9f7f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2c2c2c;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 6rpx 10rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
border: 1px solid rgba(139, 35, 35, 0.2);
|
||||||
|
color: #8b2323;
|
||||||
|
background: rgba(139, 35, 35, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-spacer {
|
||||||
|
height: 80rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
11
前端源码/uni-app/components/screens/RenamingPlaceholder.vue
Normal file
11
前端源码/uni-app/components/screens/RenamingPlaceholder.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<view class="h-full flex flex-col items-center justify-center bg-[#fdfbf7] text-[#2c2c2c] font-serif relative overflow-hidden">
|
||||||
|
<view class="absolute inset-0 opacity-10 pointer-events-none bg-[url('https://www.transparenttextures.com/patterns/rice-paper.png')]"></view>
|
||||||
|
<view class="relative z-10 text-center px-8">
|
||||||
|
<text class="block text-4xl mb-3 text-[#d4af37]">更</text>
|
||||||
|
<text class="block text-xl font-bold tracking-[0.3em] mb-2">改名/重塑模块</text>
|
||||||
|
<text class="block text-sm text-[#5a5a5a]">稍后将迁移完整改名流程与报告</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
917
前端源码/uni-app/components/screens/TestName.vue
Normal file
917
前端源码/uni-app/components/screens/TestName.vue
Normal file
@@ -0,0 +1,917 @@
|
|||||||
|
<template>
|
||||||
|
<view class="testname-screen">
|
||||||
|
<!-- 背景纹理 -->
|
||||||
|
<view class="testname-bg-texture"></view>
|
||||||
|
|
||||||
|
<!-- 状态栏占位 -->
|
||||||
|
<view class="status-bar-placeholder"></view>
|
||||||
|
|
||||||
|
<!-- 顶部装饰 -->
|
||||||
|
<view class="testname-top-bar"></view>
|
||||||
|
|
||||||
|
<view class="testname-container">
|
||||||
|
<!-- 标题区 -->
|
||||||
|
<view class="testname-header">
|
||||||
|
<text class="testname-title">八字测名</text>
|
||||||
|
|
||||||
|
<!-- Mode Toggle -->
|
||||||
|
<view class="testname-mode-toggle">
|
||||||
|
<view class="testname-mode-toggle-bg">
|
||||||
|
<!-- Active Indicator -->
|
||||||
|
<view class="testname-mode-toggle-slider"
|
||||||
|
:style="{ left: mode === 'personal' ? '4px' : 'calc(50% + 2px)', width: 'calc(50% - 6px)' }" />
|
||||||
|
|
||||||
|
<view class="testname-mode-toggle-btn" :class="{ 'testname-mode-toggle-btn-active': mode === 'personal' }"
|
||||||
|
@click="mode = 'personal'">
|
||||||
|
<text>个人</text>
|
||||||
|
</view>
|
||||||
|
<view class="testname-mode-toggle-btn" :class="{ 'testname-mode-toggle-btn-active': mode === 'company' }"
|
||||||
|
@click="mode = 'company'">
|
||||||
|
<text>公司</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 主表单卡片 -->
|
||||||
|
<view class="testname-form-card" :class="{ 'testname-form-card-company': mode === 'company' }">
|
||||||
|
<!-- 四角装饰纹样 -->
|
||||||
|
<view v-for="(corner, i) in corners" :key="i" class="testname-corner" :style="corner"></view>
|
||||||
|
|
||||||
|
<!-- 个人表单 -->
|
||||||
|
<view v-if="mode === 'personal'" class="testname-form-personal">
|
||||||
|
<view class="testname-name-row">
|
||||||
|
<view class="testname-name-group">
|
||||||
|
<text class="testname-label">姓氏</text>
|
||||||
|
<view class="testname-input-wrapper testname-input-wrapper-focus">
|
||||||
|
<input v-model="personalData.lastName" type="text" class="testname-input-name" placeholder="李" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="testname-name-group">
|
||||||
|
<text class="testname-label">名字</text>
|
||||||
|
<view class="testname-input-wrapper testname-input-wrapper-focus">
|
||||||
|
<input v-model="personalData.firstName" type="text" class="testname-input-name" placeholder="逍遥" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="testname-gender-section">
|
||||||
|
<text class="testname-label-center">性别</text>
|
||||||
|
<view class="testname-gender-group">
|
||||||
|
<view class="testname-gender-btn"
|
||||||
|
:class="{ 'testname-gender-btn-active': personalData.gender === 'male' }"
|
||||||
|
@click="personalData.gender = 'male'">
|
||||||
|
<text class="testname-gender-symbol">乾</text>
|
||||||
|
<text class="testname-gender-label">男</text>
|
||||||
|
</view>
|
||||||
|
<view class="testname-gender-btn"
|
||||||
|
:class="{ 'testname-gender-btn-active': personalData.gender === 'female' }"
|
||||||
|
@click="personalData.gender = 'female'">
|
||||||
|
<text class="testname-gender-symbol">坤</text>
|
||||||
|
<text class="testname-gender-label">女</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="testname-date-section">
|
||||||
|
<view class="testname-label-with-icon">
|
||||||
|
<CalendarIcon :size="14" class="testname-icon" />
|
||||||
|
<text class="testname-label" style="margin-bottom: 0px;">生辰</text>
|
||||||
|
</view>
|
||||||
|
<view class="testname-date-picker-trigger" @click="activeDateField = 'personal'">
|
||||||
|
<text class="testname-date-picker-text"
|
||||||
|
:class="{ 'testname-date-picker-text-filled': personalData.birthDateDisplay }">
|
||||||
|
{{ personalData.birthDateDisplay || '请择生辰' }}
|
||||||
|
</text>
|
||||||
|
<view class="testname-date-picker-arrow">
|
||||||
|
<ChevronDownIcon :size="16" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 公司表单 -->
|
||||||
|
<view v-else class="testname-form-company">
|
||||||
|
<!-- 基础信息 -->
|
||||||
|
<view class="testname-company-basic">
|
||||||
|
<view class="testname-company-field">
|
||||||
|
<view class="testname-label-with-icon">
|
||||||
|
<HomeIcon :size="14" class="testname-icon" />
|
||||||
|
<text class="testname-label" style="margin-bottom: 0px;">公司名称</text>
|
||||||
|
</view>
|
||||||
|
<input v-model="companyData.companyName" type="text" class="testname-input-company"
|
||||||
|
placeholder="例:鼎盛科技" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="testname-company-field">
|
||||||
|
<view class="testname-label-with-icon">
|
||||||
|
<HomeIcon :size="14" class="testname-icon" />
|
||||||
|
<text class="testname-label" style="margin-bottom: 0px;">主营业务 / 行业</text>
|
||||||
|
</view>
|
||||||
|
<input v-model="companyData.industry" type="text" class="testname-input-company"
|
||||||
|
placeholder="例:科技、餐饮、文化..." />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="testname-company-row">
|
||||||
|
<view class="testname-company-field testname-company-field-half">
|
||||||
|
<text class="testname-label">经营地址</text>
|
||||||
|
<input v-model="companyData.address" type="text" class="testname-input-company" placeholder="城市/方位" />
|
||||||
|
</view>
|
||||||
|
<view class="testname-company-field testname-company-field-half">
|
||||||
|
<text class="testname-label">服务群体</text>
|
||||||
|
<input v-model="companyData.targetAudience" type="text" class="testname-input-company"
|
||||||
|
placeholder="年轻人、高端..." />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="testname-divider"></view>
|
||||||
|
|
||||||
|
<!-- 核心成员 -->
|
||||||
|
<view class="testname-members-section">
|
||||||
|
<view class="testname-members-header">
|
||||||
|
<view class="testname-label-with-icon">
|
||||||
|
<ProfileIcon :size="14" class="testname-icon" />
|
||||||
|
<text class="testname-label" style="margin-bottom: 0px;">核心成员 (五行匹配)</text>
|
||||||
|
</view>
|
||||||
|
<text class="testname-members-tip">至少需填一位</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="testname-members-list">
|
||||||
|
<view v-for="(member, idx) in companyData.members" :key="idx" class="testname-member-item">
|
||||||
|
<view class="testname-member-number">{{ chNum[Number(idx) + 1] }}</view>
|
||||||
|
<input v-model="member.name" type="text" class="testname-member-name" placeholder="姓名" />
|
||||||
|
<view class="testname-member-divider"></view>
|
||||||
|
<view class="testname-member-date" :class="{ 'testname-member-date-filled': member.birthDate }"
|
||||||
|
@click="activeDateField = `member-${idx}`">
|
||||||
|
<text>{{ member.birthDate ? member.birthDate.split('年')[0] + '年...' : '选择诞辰' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部按钮 -->
|
||||||
|
<view class="testname-submit-section">
|
||||||
|
<button class="testname-submit-btn" :class="{ 'testname-submit-btn-disabled': !isValid }" @click="handleStart">
|
||||||
|
<view class="testname-submit-btn-content">
|
||||||
|
<SearchIcon :size="18" class="testname-submit-icon" />
|
||||||
|
<text class="testname-submit-text">立即排盘</text>
|
||||||
|
</view>
|
||||||
|
<view class="testname-submit-btn-border"></view>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="testname-footer-tip">
|
||||||
|
<text class="testname-footer-text">
|
||||||
|
易经数理 · 五行生克 · {{ mode === 'personal' ? '三才五格' : '商号吉凶' }}
|
||||||
|
<text class="testname-footer-subtext">隐私保护:您的信息仅用于本次测算,不做留存</text>
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 自定义日期选择器 Modal -->
|
||||||
|
<MysticDatePicker :is-open="!!activeDateField" :title="activeDateField === 'personal' ? '请择良辰' : '核心成员诞辰'"
|
||||||
|
:default-value="getDefaultValue()" @close="activeDateField = null" @confirm="handleDateConfirm" />
|
||||||
|
|
||||||
|
<!-- 加载界面 -->
|
||||||
|
<MysticCompass
|
||||||
|
v-if="isLoading"
|
||||||
|
:title="mode === 'personal' ? '正在推演命盘' : '正在测算商号'"
|
||||||
|
:subtitle="mode === 'personal' ? '易经数理 · 五行生克 · 三才五格' : '易经数理 · 五行生克 · 商号吉凶'"
|
||||||
|
:desktop="isDesktopLayout"
|
||||||
|
@back="handleLoadingBack"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue';
|
||||||
|
import { getIsDesktopLayout } from '../../utils/device-layout';
|
||||||
|
import MysticDatePicker from '../MysticDatePicker.vue';
|
||||||
|
import MysticCompass from '../MysticCompass.vue';
|
||||||
|
import CalendarIcon from '../icons/CalendarIcon.vue';
|
||||||
|
import ProfileIcon from '../icons/ProfileIcon.vue';
|
||||||
|
import HomeIcon from '../icons/HomeIcon.vue';
|
||||||
|
import ChevronDownIcon from '../icons/ChevronDownIcon.vue';
|
||||||
|
import SearchIcon from '../icons/SearchIcon.vue';
|
||||||
|
|
||||||
|
interface CoreMember {
|
||||||
|
name: string;
|
||||||
|
birthDate: string;
|
||||||
|
birthDateApi: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PersonalTestParams {
|
||||||
|
lastName: string;
|
||||||
|
firstName: string;
|
||||||
|
gender: 'male' | 'female';
|
||||||
|
birthDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CompanyTestParams {
|
||||||
|
industry: string;
|
||||||
|
address: string;
|
||||||
|
target_audience: string;
|
||||||
|
members: Array<{ name: string; birth_date: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
test: [mode: 'personal' | 'company', params: PersonalTestParams | CompanyTestParams];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
type TestMode = 'personal' | 'company';
|
||||||
|
|
||||||
|
const mode = ref<TestMode>('personal');
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
const isDesktopLayout = ref(
|
||||||
|
typeof window !== 'undefined' ? getIsDesktopLayout() : false,
|
||||||
|
);
|
||||||
|
const syncDesktopLayout = () => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
isDesktopLayout.value = getIsDesktopLayout();
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
syncDesktopLayout();
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('resize', syncDesktopLayout, { passive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.removeEventListener('resize', syncDesktopLayout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 个人表单数据
|
||||||
|
const personalData = reactive({
|
||||||
|
lastName: '',
|
||||||
|
firstName: '',
|
||||||
|
gender: 'male' as 'male' | 'female',
|
||||||
|
birthDateDisplay: '',
|
||||||
|
birthDateApi: '' // 接口格式
|
||||||
|
});
|
||||||
|
|
||||||
|
// 公司表单数据
|
||||||
|
const companyData = reactive({
|
||||||
|
companyName: '',
|
||||||
|
industry: '',
|
||||||
|
address: '',
|
||||||
|
targetAudience: '',
|
||||||
|
members: Array(5).fill(null).map(() => ({ name: '', birthDate: '', birthDateApi: '' } as CoreMember))
|
||||||
|
});
|
||||||
|
|
||||||
|
// 日期选择器状态
|
||||||
|
const activeDateField = ref<string | null>(null);
|
||||||
|
|
||||||
|
// 中文数字
|
||||||
|
const chNum = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
|
||||||
|
|
||||||
|
// 四角装饰样式
|
||||||
|
const corners = [
|
||||||
|
{ top: '8px', left: '8px', borderTopWidth: '1px', borderLeftWidth: '1px', borderRightWidth: '0', borderBottomWidth: '0' },
|
||||||
|
{ top: '8px', right: '8px', borderTopWidth: '1px', borderRightWidth: '1px', borderLeftWidth: '0', borderBottomWidth: '0' },
|
||||||
|
{ bottom: '8px', left: '8px', borderBottomWidth: '1px', borderLeftWidth: '1px', borderTopWidth: '0', borderRightWidth: '0' },
|
||||||
|
{ bottom: '8px', right: '8px', borderBottomWidth: '1px', borderRightWidth: '1px', borderTopWidth: '0', borderLeftWidth: '0' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleDateConfirm = (displayVal: string, apiVal: string) => {
|
||||||
|
if (!activeDateField.value) return;
|
||||||
|
|
||||||
|
if (activeDateField.value === 'personal') {
|
||||||
|
personalData.birthDateDisplay = displayVal;
|
||||||
|
personalData.birthDateApi = apiVal;
|
||||||
|
} else if (activeDateField.value.startsWith('member-')) {
|
||||||
|
const index = parseInt(activeDateField.value.split('-')[1]);
|
||||||
|
companyData.members[index].birthDate = displayVal;
|
||||||
|
companyData.members[index].birthDateApi = apiVal;
|
||||||
|
}
|
||||||
|
activeDateField.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDefaultValue = () => {
|
||||||
|
if (!activeDateField.value) return '';
|
||||||
|
|
||||||
|
if (activeDateField.value === 'personal') {
|
||||||
|
return personalData.birthDateDisplay || '';
|
||||||
|
} else if (activeDateField.value.startsWith('member-')) {
|
||||||
|
const index = parseInt(activeDateField.value.split('-')[1]);
|
||||||
|
return companyData.members[index].birthDate || '';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValid = computed(() => {
|
||||||
|
if (mode.value === 'personal') {
|
||||||
|
return personalData.lastName && personalData.firstName && personalData.birthDateDisplay;
|
||||||
|
} else {
|
||||||
|
return companyData.industry && companyData.address &&
|
||||||
|
companyData.members.some((m: CoreMember) => m.name && m.birthDate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleStart = () => {
|
||||||
|
if (!isValid.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: mode.value === 'personal' ? '请填写完整个人信息以获取准确命盘' : '请至少填写主营业务、地址及一位核心成员信息',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示加载界面
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
// 触发提交事件,由父组件处理接口调用
|
||||||
|
if (mode.value === 'personal') {
|
||||||
|
emit('test', 'personal', {
|
||||||
|
lastName: personalData.lastName,
|
||||||
|
firstName: personalData.firstName,
|
||||||
|
gender: personalData.gender,
|
||||||
|
birthDate: personalData.birthDateApi
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
emit('test', 'company', {
|
||||||
|
companyName: companyData.companyName,
|
||||||
|
industry: companyData.industry,
|
||||||
|
address: companyData.address,
|
||||||
|
target_audience: companyData.targetAudience,
|
||||||
|
members: companyData.members
|
||||||
|
.filter((m: CoreMember) => m.name && m.birthDateApi)
|
||||||
|
.map((m: CoreMember) => ({
|
||||||
|
name: m.name,
|
||||||
|
birth_date: m.birthDateApi
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 暴露方法供父组件调用
|
||||||
|
defineExpose({
|
||||||
|
closeLoading: () => {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理 loading 页面的返回按钮
|
||||||
|
const handleLoadingBack = () => {
|
||||||
|
isLoading.value = false;
|
||||||
|
uni.showToast({
|
||||||
|
title: '测算结果可在"我的方案"中查看',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.testname-screen {
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background: #fdfbf7;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态栏占位 */
|
||||||
|
.status-bar-placeholder {
|
||||||
|
height: var(--status-bar-height, 0);
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-bg-texture {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0.1;
|
||||||
|
pointer-events: none;
|
||||||
|
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-top-bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: #8b2323;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 40px 20px 32px;
|
||||||
|
z-index: 10;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.testname-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c2c2c;
|
||||||
|
letter-spacing: 0.3em;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-family: SimSun, serif;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-mode-toggle {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-mode-toggle-bg {
|
||||||
|
background: rgba(234, 221, 207, 0.5);
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 999px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid #dcd3c9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-mode-toggle-slider {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background: #fffdf9;
|
||||||
|
border-radius: 999px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid #dcd3c9;
|
||||||
|
transition: left 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-mode-toggle-btn {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 8px 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 0.24em;
|
||||||
|
color: #8a8a8a;
|
||||||
|
transition: color 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-mode-toggle-btn-active {
|
||||||
|
color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Card */
|
||||||
|
.testname-form-card {
|
||||||
|
background: #fffdf9;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid #eaddcf;
|
||||||
|
box-shadow: 0 4px 20px -10px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-corner {
|
||||||
|
position: absolute;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-color: #8b2323;
|
||||||
|
opacity: 0.4;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Personal Form */
|
||||||
|
.testname-form-personal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-name-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-name-group {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
letter-spacing: 0.24em;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-label-center {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
letter-spacing: 0.24em;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-label-with-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
border-bottom: 2px solid #e5e5e5;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-input-wrapper-focus {
|
||||||
|
border-bottom-color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-input-name {
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-family: SimSun, serif;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-input-name::placeholder {
|
||||||
|
color: #dcd3c9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gender Section */
|
||||||
|
.testname-gender-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-gender-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-gender-btn {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #dcd3c9;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s;
|
||||||
|
color: #5a5a5a;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-gender-btn-active {
|
||||||
|
border-color: #8b2323;
|
||||||
|
background: #8b2323;
|
||||||
|
color: #fdfbf7;
|
||||||
|
box-shadow: 0 2px 8px rgba(139, 35, 35, 0.2);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-gender-symbol {
|
||||||
|
font-size: 24px;
|
||||||
|
font-family: SimSun, serif;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-gender-label {
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.24em;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Date Section */
|
||||||
|
.testname-date-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-date-picker-trigger {
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid #eaddcf;
|
||||||
|
background: #fcfaf5;
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-date-picker-trigger:active {
|
||||||
|
border-color: rgba(139, 35, 35, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-date-picker-text {
|
||||||
|
font-family: SimSun, serif;
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: #dcd3c9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-date-picker-text-filled {
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-date-picker-arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-date-picker-trigger:active .testname-date-picker-arrow {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Company Form */
|
||||||
|
.testname-form-company {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-company-basic {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-company-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-company-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-company-field-half {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-input-company {
|
||||||
|
width: 100%;
|
||||||
|
background: #fcfaf5;
|
||||||
|
border: 1px solid #eaddcf;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #2c2c2c;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-input-company:focus {
|
||||||
|
border-color: #8b2323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-input-company::placeholder {
|
||||||
|
color: #dcd3c9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-divider {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background: #eaddcf;
|
||||||
|
opacity: 0.5;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Members Section */
|
||||||
|
.testname-members-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-members-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-members-tip {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(139, 35, 35, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-members-list {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-member-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: #fcfaf5;
|
||||||
|
border: 1px solid #eaddcf;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-member-number {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(234, 221, 207, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
font-family: SimSun, serif;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-member-name {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2c2c2c;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-member-name::placeholder {
|
||||||
|
color: #dcd3c9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-member-divider {
|
||||||
|
height: 16px;
|
||||||
|
width: 1px;
|
||||||
|
background: #eaddcf;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-member-date {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: #dcd3c9;
|
||||||
|
transition: all 0.3s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-member-date:active {
|
||||||
|
background: rgba(234, 221, 207, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-member-date-filled {
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Submit Button */
|
||||||
|
.testname-submit-section {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-submit-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px 0;
|
||||||
|
background: #2c2c2c;
|
||||||
|
color: #fdfbf7;
|
||||||
|
letter-spacing: 0.4em;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 18px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-submit-btn:active:not(.testname-submit-btn-disabled) {
|
||||||
|
background: #1a1a1a;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-submit-btn-disabled {
|
||||||
|
background: #dcd3c9;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-submit-btn-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-submit-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #fdfbf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-submit-btn-disabled .testname-submit-icon {
|
||||||
|
color: #fdfbf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-submit-text {
|
||||||
|
font-size: 18px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-submit-btn-border {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
left: 4px;
|
||||||
|
right: 4px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer Tip */
|
||||||
|
.testname-footer-tip {
|
||||||
|
margin-top: 32px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-footer-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(138, 138, 138, 0.8);
|
||||||
|
line-height: 1.8;
|
||||||
|
font-family: SimSun, serif;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testname-footer-subtext {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(138, 138, 138, 0.6);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
2873
前端源码/uni-app/components/screens/TestNameDetail.vue
Normal file
2873
前端源码/uni-app/components/screens/TestNameDetail.vue
Normal file
File diff suppressed because it is too large
Load Diff
162
前端源码/uni-app/components/screens/TestResult.vue
Normal file
162
前端源码/uni-app/components/screens/TestResult.vue
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<view class="test-result-screen">
|
||||||
|
<!-- 背景 -->
|
||||||
|
<view class="test-result-bg"></view>
|
||||||
|
|
||||||
|
<!-- 头部 -->
|
||||||
|
<view class="test-result-header">
|
||||||
|
<view class="header-left" @click="emit('back')">
|
||||||
|
<text class="header-back">‹</text>
|
||||||
|
</view>
|
||||||
|
<view class="header-title-wrap">
|
||||||
|
<text class="header-title">{{ mode === 'personal' ? '个人测名结果' : '公司测名结果' }}</text>
|
||||||
|
<text class="header-subtitle">接口原始数据展示(无任何模拟与加工)</text>
|
||||||
|
</view>
|
||||||
|
<view class="header-right" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 内容 -->
|
||||||
|
<scroll-view scroll-y class="test-result-body">
|
||||||
|
<view class="test-result-card">
|
||||||
|
<text class="section-title">原始返回 JSON</text>
|
||||||
|
<view class="json-box">
|
||||||
|
<text class="json-text">
|
||||||
|
{{ formattedJson }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<text class="tip-text">
|
||||||
|
当前为后端返回数据的直接展示,用于确保与接口保持 100% 一致;后续可以在此基础上做更精细的可视化拆解。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
mode: 'personal' | 'company';
|
||||||
|
data: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
back: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const formattedJson = computed(() => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(props.data ?? {}, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
return String(props.data ?? '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.test-result-screen {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
background-color: #050508;
|
||||||
|
color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-result-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(circle at top, #1a1a2e 0, #050508 40%, #000 100%);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-result-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
padding-top: calc(24rpx + env(safe-area-inset-top, 0px));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
background: rgba(5, 5, 8, 0.9);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left,
|
||||||
|
.header-right {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-back {
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title-wrap {
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
color: #f2e6d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-subtitle {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: rgba(226, 232, 240, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-result-body {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
flex: 1;
|
||||||
|
padding: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-result-card {
|
||||||
|
background: rgba(15, 23, 42, 0.9);
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.4);
|
||||||
|
box-shadow: 0 20rpx 40rpx rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-box {
|
||||||
|
background: #020617;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-text {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #e5e7eb;
|
||||||
|
line-height: 1.7;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-text {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
211
前端源码/uni-app/components/screens/UserAgreement.vue
Normal file
211
前端源码/uni-app/components/screens/UserAgreement.vue
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
<template>
|
||||||
|
<view class="agreement-screen">
|
||||||
|
<view class="agreement-header">
|
||||||
|
<view class="agreement-back" @click="handleBack">
|
||||||
|
<text class="agreement-back-icon">←</text>
|
||||||
|
</view>
|
||||||
|
<text class="agreement-title">用户协议</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="agreement-content">
|
||||||
|
<view class="agreement-section">
|
||||||
|
<text class="agreement-section-title">一、协议的接受与修改</text>
|
||||||
|
<text class="agreement-text">
|
||||||
|
欢迎使用壹梵起名服务。本协议是您与壹梵起名之间关于使用壹梵起名服务所订立的协议。请您仔细阅读本协议,您点击"同意"按钮后,本协议即构成对双方有约束力的法律文件。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="agreement-section">
|
||||||
|
<text class="agreement-section-title">二、服务说明</text>
|
||||||
|
<text class="agreement-text">
|
||||||
|
壹梵起名向用户提供包括但不限于宝宝起名、姓名测试、公司起名、改名建议、择吉日等服务。具体服务内容以平台实际提供为准。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="agreement-section">
|
||||||
|
<text class="agreement-section-title">三、用户账号</text>
|
||||||
|
<text class="agreement-text">
|
||||||
|
1. 用户需要注册账号才能使用本服务。用户应当提供真实、准确、完整的个人信息。
|
||||||
|
</text>
|
||||||
|
<text class="agreement-text">
|
||||||
|
2. 用户应妥善保管账号和密码,对账号下的所有行为负责。
|
||||||
|
</text>
|
||||||
|
<text class="agreement-text">
|
||||||
|
3. 用户不得将账号转让、出借或以其他方式提供给第三方使用。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="agreement-section">
|
||||||
|
<text class="agreement-section-title">四、用户行为规范</text>
|
||||||
|
<text class="agreement-text">
|
||||||
|
用户在使用本服务时,应遵守国家法律法规,不得利用本服务从事违法违规活动,包括但不限于:
|
||||||
|
</text>
|
||||||
|
<text class="agreement-text">
|
||||||
|
1. 发布、传播违法违规信息;
|
||||||
|
</text>
|
||||||
|
<text class="agreement-text">
|
||||||
|
2. 侵犯他人知识产权或其他合法权益;
|
||||||
|
</text>
|
||||||
|
<text class="agreement-text">
|
||||||
|
3. 干扰或破坏服务的正常运行;
|
||||||
|
</text>
|
||||||
|
<text class="agreement-text">
|
||||||
|
4. 其他违反法律法规或本协议的行为。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="agreement-section">
|
||||||
|
<text class="agreement-section-title">五、知识产权</text>
|
||||||
|
<text class="agreement-text">
|
||||||
|
本服务中的所有内容,包括但不限于文字、图片、软件、程序等,其知识产权均归壹梵起名或相关权利人所有。未经授权,用户不得擅自使用。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="agreement-section">
|
||||||
|
<text class="agreement-section-title">六、免责声明</text>
|
||||||
|
<text class="agreement-text">
|
||||||
|
1. 本服务提供的起名、测名等内容仅供参考,不构成任何承诺或保证。
|
||||||
|
</text>
|
||||||
|
<text class="agreement-text">
|
||||||
|
2. 因不可抗力、网络故障等原因导致的服务中断或数据丢失,壹梵起名不承担责任。
|
||||||
|
</text>
|
||||||
|
<text class="agreement-text">
|
||||||
|
3. 用户因使用本服务产生的任何纠纷,应依法解决。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="agreement-section">
|
||||||
|
<text class="agreement-section-title">七、协议的变更与终止</text>
|
||||||
|
<text class="agreement-text">
|
||||||
|
壹梵起名有权根据需要修改本协议,修改后的协议将在平台上公布。用户继续使用服务即视为接受修改后的协议。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="agreement-section">
|
||||||
|
<text class="agreement-section-title">八、其他</text>
|
||||||
|
<text class="agreement-text">
|
||||||
|
本协议的解释、效力及纠纷的解决,适用中华人民共和国法律。如有争议,双方应友好协商解决;协商不成的,任何一方均可向壹梵起名所在地人民法院提起诉讼。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="agreement-footer">
|
||||||
|
<text class="agreement-footer-text">壹梵起名</text>
|
||||||
|
<text class="agreement-footer-text">生效日期:2024年1月1日</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.agreement-screen {
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
background: #fdfbf7 url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: rgba(253, 251, 247, 0.95);
|
||||||
|
border-bottom: 1px solid #eaddcf;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-back {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-back-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #8b2323;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px 20px 40px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-section-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #8b2323;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #4a4a4a;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-footer {
|
||||||
|
margin-top: 40px;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid #eaddcf;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-footer-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
font-family: SimSun, "Songti SC", serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
136
前端源码/uni-app/constants/wuxing.ts
Normal file
136
前端源码/uni-app/constants/wuxing.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* 五行(金木水火土)枚举及颜色定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum WuxingElement {
|
||||||
|
/** 金 */
|
||||||
|
METAL = '金',
|
||||||
|
/** 木 */
|
||||||
|
WOOD = '木',
|
||||||
|
/** 水 */
|
||||||
|
WATER = '水',
|
||||||
|
/** 火 */
|
||||||
|
FIRE = '火',
|
||||||
|
/** 土 */
|
||||||
|
EARTH = '土'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 五行颜色配置
|
||||||
|
*/
|
||||||
|
export interface WuxingColorConfig {
|
||||||
|
/** 元素名称 */
|
||||||
|
label: WuxingElement;
|
||||||
|
/** 主色调(用于背景、填充等) */
|
||||||
|
color: string;
|
||||||
|
/** 浅色版本(用于浅色背景) */
|
||||||
|
lightColor?: string;
|
||||||
|
/** 深色版本(用于深色背景) */
|
||||||
|
darkColor?: string;
|
||||||
|
/** RGB 值(用于计算) */
|
||||||
|
rgb?: { r: number; g: number; b: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 五行颜色映射表
|
||||||
|
*/
|
||||||
|
export const WUXING_COLORS: Record<WuxingElement, WuxingColorConfig> = {
|
||||||
|
[WuxingElement.METAL]: {
|
||||||
|
label: WuxingElement.METAL,
|
||||||
|
color: '#E0E0E0', // 浅灰色/银色
|
||||||
|
lightColor: '#F5F5F5',
|
||||||
|
darkColor: '#BDBDBD',
|
||||||
|
rgb: { r: 224, g: 224, b: 224 }
|
||||||
|
},
|
||||||
|
[WuxingElement.WOOD]: {
|
||||||
|
label: WuxingElement.WOOD,
|
||||||
|
color: '#4CAF50', // 绿色
|
||||||
|
lightColor: '#81C784',
|
||||||
|
darkColor: '#388E3C',
|
||||||
|
rgb: { r: 76, g: 175, b: 80 }
|
||||||
|
},
|
||||||
|
[WuxingElement.WATER]: {
|
||||||
|
label: WuxingElement.WATER,
|
||||||
|
color: '#2196F3', // 蓝色
|
||||||
|
lightColor: '#64B5F6',
|
||||||
|
darkColor: '#1976D2',
|
||||||
|
rgb: { r: 33, g: 150, b: 243 }
|
||||||
|
},
|
||||||
|
[WuxingElement.FIRE]: {
|
||||||
|
label: WuxingElement.FIRE,
|
||||||
|
color: '#F44336', // 红色
|
||||||
|
lightColor: '#E57373',
|
||||||
|
darkColor: '#D32F2F',
|
||||||
|
rgb: { r: 244, g: 67, b: 54 }
|
||||||
|
},
|
||||||
|
[WuxingElement.EARTH]: {
|
||||||
|
label: WuxingElement.EARTH,
|
||||||
|
color: '#795548', // 棕色
|
||||||
|
lightColor: '#A1887F',
|
||||||
|
darkColor: '#5D4037',
|
||||||
|
rgb: { r: 121, g: 85, b: 72 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取五行元素的颜色
|
||||||
|
* @param element 五行元素
|
||||||
|
* @param variant 颜色变体(默认使用主色调)
|
||||||
|
* @returns 颜色值(hex 格式)
|
||||||
|
*/
|
||||||
|
export function getWuxingColor(
|
||||||
|
element: WuxingElement,
|
||||||
|
variant: 'color' | 'lightColor' | 'darkColor' = 'color'
|
||||||
|
): string {
|
||||||
|
const config = WUXING_COLORS[element];
|
||||||
|
return config[variant] || config.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取五行元素的 RGB 值
|
||||||
|
* @param element 五行元素
|
||||||
|
* @returns RGB 对象
|
||||||
|
*/
|
||||||
|
export function getWuxingRgb(element: WuxingElement): { r: number; g: number; b: number } {
|
||||||
|
return WUXING_COLORS[element].rgb || { r: 0, g: 0, b: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 五行数组(按顺序:金木水火土)
|
||||||
|
*/
|
||||||
|
export const WUXING_ARRAY: WuxingElement[] = [
|
||||||
|
WuxingElement.METAL,
|
||||||
|
WuxingElement.WOOD,
|
||||||
|
WuxingElement.WATER,
|
||||||
|
WuxingElement.FIRE,
|
||||||
|
WuxingElement.EARTH
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 五行数据格式(用于图表、列表等)
|
||||||
|
*/
|
||||||
|
export interface WuxingData {
|
||||||
|
/** 元素名称 */
|
||||||
|
label: WuxingElement;
|
||||||
|
/** 数值(0-100) */
|
||||||
|
value: number;
|
||||||
|
/** 颜色 */
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建五行数据数组
|
||||||
|
* @param values 五行数值数组,顺序为 [金, 木, 水, 火, 土]
|
||||||
|
* @returns 五行数据数组
|
||||||
|
*/
|
||||||
|
export function createWuxingData(values: number[]): WuxingData[] {
|
||||||
|
if (values.length !== 5) {
|
||||||
|
throw new Error('五行数值数组必须包含5个元素');
|
||||||
|
}
|
||||||
|
|
||||||
|
return WUXING_ARRAY.map((element, index) => ({
|
||||||
|
label: element,
|
||||||
|
value: Math.max(0, Math.min(100, values[index] || 0)), // 限制在 0-100 范围
|
||||||
|
color: WUXING_COLORS[element].color
|
||||||
|
}));
|
||||||
|
}
|
||||||
17
前端源码/uni-app/index.html
Normal file
17
前端源码/uni-app/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>起名应用</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/main.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
16
前端源码/uni-app/main.js
Normal file
16
前端源码/uni-app/main.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import './style/index.css'
|
||||||
|
import './style/uni-compat.css' // uni-app 组件兼容样式
|
||||||
|
import './utils/uni-compat' // 引入 uni-app 兼容层
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(router)
|
||||||
|
app.mount('#app')
|
||||||
|
|
||||||
|
// 将 router 挂载到全局,供兼容层使用
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.vueRouter = router
|
||||||
|
}
|
||||||
|
|
||||||
42
前端源码/uni-app/manifest.json
Normal file
42
前端源码/uni-app/manifest.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name" : "起名小程序",
|
||||||
|
"appid" : "__UNI__ED3711F",
|
||||||
|
"description" : "React 版迁移到 uni-app Vue3 的骨架",
|
||||||
|
"versionName" : "1.0.0",
|
||||||
|
"versionCode" : "100",
|
||||||
|
"vueVersion" : "3",
|
||||||
|
"transformPx" : false,
|
||||||
|
"app-plus" : {
|
||||||
|
"compatible" : {
|
||||||
|
"ignoreVersion" : true
|
||||||
|
},
|
||||||
|
"distribute" : {
|
||||||
|
"ios" : {
|
||||||
|
"dSYMs" : false
|
||||||
|
},
|
||||||
|
"sdkConfigs" : {
|
||||||
|
"maps" : {
|
||||||
|
"google" : {
|
||||||
|
"APIKey_ios" : "",
|
||||||
|
"APIKey_android" : ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modules" : {
|
||||||
|
"Record" : {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quickapp" : {},
|
||||||
|
"mp-weixin" : {
|
||||||
|
"appid" : "wx062a6d09abb115fe",
|
||||||
|
"setting" : {
|
||||||
|
"es6" : true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"h5" : {
|
||||||
|
"router" : {
|
||||||
|
"mode" : "history"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1679
前端源码/uni-app/package-lock.json
generated
Normal file
1679
前端源码/uni-app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
前端源码/uni-app/package.json
Normal file
26
前端源码/uni-app/package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "naming-h5",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "壹梵起名",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.6.0",
|
||||||
|
"echarts": "^6.0.0",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
|
"jspdf": "^4.2.1",
|
||||||
|
"vue": "^3.4.0",
|
||||||
|
"vue-router": "^4.2.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.0.0",
|
||||||
|
"postcss-pxtorem": "^6.1.0",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "^5.0.0"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a"
|
||||||
|
}
|
||||||
40
前端源码/uni-app/pages.json
Normal file
40
前端源码/uni-app/pages.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"easycom": {
|
||||||
|
"autoscan": true,
|
||||||
|
"custom": {
|
||||||
|
"^(.*)Screen$": "@/components/screens/$1Screen.vue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "pages/index/index",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/login/login",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
,
|
||||||
|
{
|
||||||
|
"path": "pages/test-name-live/test-name-live",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"globalStyle": {
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"navigationBarTitleText": "起名小程序",
|
||||||
|
"navigationBarBackgroundColor": "#fdfbf7",
|
||||||
|
"backgroundColor": "#f2e6d8"
|
||||||
|
},
|
||||||
|
"permission": {
|
||||||
|
"scope.writePhotosAlbum": {
|
||||||
|
"desc": "保存海报图片到相册"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1372
前端源码/uni-app/pages/index/index.vue
Normal file
1372
前端源码/uni-app/pages/index/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
185
前端源码/uni-app/pages/login/login.vue
Normal file
185
前端源码/uni-app/pages/login/login.vue
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
class="login-page login-page--fantasy"
|
||||||
|
:class="{ 'login-page--desktop': isDesktop, 'login-page--mobile': !isDesktop }"
|
||||||
|
>
|
||||||
|
<MysticFantasyBg />
|
||||||
|
|
||||||
|
<!-- 登录页面 -->
|
||||||
|
<LoginScreen v-if="showLogin" @success="handleLoginSuccess" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import LoginScreen from '../../components/screens/Login.vue';
|
||||||
|
import MysticFantasyBg from '../../components/MysticFantasyBg.vue';
|
||||||
|
import type { MobileLoginResponse, MobileRegisterResponse, ForgotPasswordResponse } from '../../api/types';
|
||||||
|
import { setToken, setUserInfo, isLoggedIn } from '../../utils/auth';
|
||||||
|
import { getIsDesktopLayout } from '../../utils/device-layout';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const redirectUrl = ref('/');
|
||||||
|
const showLogin = ref(false);
|
||||||
|
|
||||||
|
const isDesktop = ref(
|
||||||
|
typeof window !== 'undefined' ? getIsDesktopLayout() : false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const syncDeviceLayout = () => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
isDesktop.value = getIsDesktopLayout();
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.removeEventListener('resize', syncDeviceLayout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取页面参数
|
||||||
|
try {
|
||||||
|
if (route.query.redirect) {
|
||||||
|
redirectUrl.value = decodeURIComponent(route.query.redirect as string);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取页面参数失败:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigateToTarget = () => {
|
||||||
|
const targetUrl = redirectUrl.value || '/';
|
||||||
|
router.replace(targetUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
syncDeviceLayout();
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('resize', syncDeviceLayout, { passive: true });
|
||||||
|
}
|
||||||
|
if (isLoggedIn()) {
|
||||||
|
if (typeof window !== 'undefined' && getIsDesktopLayout()) {
|
||||||
|
router.replace('/test-name-live');
|
||||||
|
} else {
|
||||||
|
navigateToTarget();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showLogin.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleLoginSuccess = (data: MobileLoginResponse | MobileRegisterResponse | ForgotPasswordResponse) => {
|
||||||
|
if (data.access_token) {
|
||||||
|
setToken(data.access_token);
|
||||||
|
}
|
||||||
|
if (data.user_info) {
|
||||||
|
setUserInfo(data.user_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (typeof window !== 'undefined' && getIsDesktopLayout()) {
|
||||||
|
router.replace('/test-name-live');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigateToTarget();
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-page {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 盖住 Login 组件自带米色宣纸底(含 rice-paper 纹理),避免与玄幻底冲突 */
|
||||||
|
.login-page--fantasy :deep(.login-screen) {
|
||||||
|
background: transparent !important;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page--fantasy :deep(.login-bg),
|
||||||
|
.login-page--fantasy :deep(.login-bg-pattern) {
|
||||||
|
opacity: 0 !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 电脑端:表单区深色玄幻风 */
|
||||||
|
.login-page--desktop :deep(.login-screen) {
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page--desktop :deep(.login-bg) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page--desktop :deep(.login-content) {
|
||||||
|
max-width: 440px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page--desktop :deep(.login-title) {
|
||||||
|
color: #f2e6d8;
|
||||||
|
text-shadow: 0 0 24px rgba(212, 175, 55, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page--desktop :deep(.login-subtitle) {
|
||||||
|
color: rgba(233, 233, 239, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page--desktop :deep(.login-form-wrapper) {
|
||||||
|
background: rgba(15, 23, 42, 0.55);
|
||||||
|
border: 1px solid rgba(212, 175, 55, 0.22);
|
||||||
|
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page--desktop :deep(.login-tabs) {
|
||||||
|
border-bottom-color: rgba(212, 175, 55, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page--desktop :deep(.login-tab.active) {
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page--desktop :deep(.login-tab.active::after) {
|
||||||
|
background: linear-gradient(90deg, rgba(212, 175, 55, 0.2), #d4af37, rgba(212, 175, 55, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page--desktop :deep(.login-tab-text) {
|
||||||
|
color: rgba(233, 233, 239, 0.78);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page--desktop :deep(.login-tab.active .login-tab-text) {
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page--desktop :deep(.login-form-label) {
|
||||||
|
color: rgba(242, 230, 216, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page--desktop :deep(.login-form-input) {
|
||||||
|
background: rgba(2, 6, 23, 0.45);
|
||||||
|
border-color: rgba(148, 163, 184, 0.35);
|
||||||
|
color: #f2e6d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page--desktop :deep(.login-agreement-text) {
|
||||||
|
color: rgba(233, 233, 239, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page--desktop :deep(.login-agreement-link) {
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page--desktop :deep(.login-forgot-text),
|
||||||
|
.login-page--desktop :deep(.login-back-text) {
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1040
前端源码/uni-app/pages/test-name-live/test-name-live.vue
Normal file
1040
前端源码/uni-app/pages/test-name-live/test-name-live.vue
Normal file
File diff suppressed because it is too large
Load Diff
932
前端源码/uni-app/personal-wealth-analysis-params.json
Normal file
932
前端源码/uni-app/personal-wealth-analysis-params.json
Normal file
@@ -0,0 +1,932 @@
|
|||||||
|
{
|
||||||
|
"basicInfo": {
|
||||||
|
"gender": "女",
|
||||||
|
"genderType": "坤造 (女命)",
|
||||||
|
"solarDateTime": "1997年10月5日 19:48",
|
||||||
|
"lunarDate": "丁丑年九月初四 戌时",
|
||||||
|
"taiyuan": "庚子 (壁上土)",
|
||||||
|
"minggong": "己酉 (大驿土)",
|
||||||
|
"kongwang": "申酉 (年/日)"
|
||||||
|
},
|
||||||
|
"karma": {
|
||||||
|
"pastLife": {
|
||||||
|
"icon": "史",
|
||||||
|
"title": "前世印记",
|
||||||
|
"description": "您前世多半是书香门第或修行之人,故今生悟性极高,对玄学、哲学有天然亲近感。",
|
||||||
|
"highlights": [
|
||||||
|
"书香门第或修行之人"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"currentLife": {
|
||||||
|
"icon": "灯",
|
||||||
|
"title": "今生课题",
|
||||||
|
"description": "修"口德"与"包容"。因八字金气过旺,言语易伤人而不自知,需修柔和之气。",
|
||||||
|
"highlights": [
|
||||||
|
"口德",
|
||||||
|
"包容"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"baziPillars": [
|
||||||
|
{
|
||||||
|
"title": "年柱",
|
||||||
|
"gan": "丁",
|
||||||
|
"zhi": "丑",
|
||||||
|
"god": "正官",
|
||||||
|
"hidden": [
|
||||||
|
"己:正印",
|
||||||
|
"辛:劫财",
|
||||||
|
"癸:伤官"
|
||||||
|
],
|
||||||
|
"stage": "墓",
|
||||||
|
"active": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "月柱",
|
||||||
|
"gan": "己",
|
||||||
|
"zhi": "酉",
|
||||||
|
"god": "正印",
|
||||||
|
"hidden": [
|
||||||
|
"辛:劫财"
|
||||||
|
],
|
||||||
|
"stage": "帝旺",
|
||||||
|
"active": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "日柱",
|
||||||
|
"gan": "庚",
|
||||||
|
"zhi": "辰",
|
||||||
|
"god": "日主",
|
||||||
|
"hidden": [
|
||||||
|
"戊:枭神",
|
||||||
|
"乙:正财",
|
||||||
|
"癸:伤官"
|
||||||
|
],
|
||||||
|
"stage": "养",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
"destinyGrade": {
|
||||||
|
"level": "上等",
|
||||||
|
"pattern": "正官佩印格",
|
||||||
|
"description": ""官星佩印,名利双收。"此命格主贵气,利于公职、管理。一生多得贵人扶持,逢凶化吉。"
|
||||||
|
},
|
||||||
|
"classicalText": {
|
||||||
|
"poems": [
|
||||||
|
""金水若相逢,必是美丽容。"",
|
||||||
|
""庚金生于酉月,羊刃之格。丁火正官,炼金成器。"",
|
||||||
|
""官印相生,福禄双全之造。只恐七杀暗藏,中年多劳碌。""
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"qimen": {
|
||||||
|
"title": "阳遁六局 · 值符天辅",
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"location": "SE",
|
||||||
|
"spirit": "九地",
|
||||||
|
"star": "天辅",
|
||||||
|
"door": "杜门",
|
||||||
|
"stems": [
|
||||||
|
"乙",
|
||||||
|
"庚"
|
||||||
|
],
|
||||||
|
"gong": "巽"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "S",
|
||||||
|
"spirit": "玄武",
|
||||||
|
"star": "天英",
|
||||||
|
"door": "景门",
|
||||||
|
"stems": [
|
||||||
|
"壬",
|
||||||
|
"丁"
|
||||||
|
],
|
||||||
|
"gong": "离"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "SW",
|
||||||
|
"spirit": "白虎",
|
||||||
|
"star": "天芮",
|
||||||
|
"door": "死门",
|
||||||
|
"stems": [
|
||||||
|
"丁",
|
||||||
|
"壬"
|
||||||
|
],
|
||||||
|
"gong": "坤"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "E",
|
||||||
|
"spirit": "九天",
|
||||||
|
"star": "天冲",
|
||||||
|
"door": "伤门",
|
||||||
|
"stems": [
|
||||||
|
"丙",
|
||||||
|
"戊"
|
||||||
|
],
|
||||||
|
"gong": "震"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"center": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "W",
|
||||||
|
"spirit": "六合",
|
||||||
|
"star": "天柱",
|
||||||
|
"door": "惊门",
|
||||||
|
"stems": [
|
||||||
|
"庚",
|
||||||
|
"乙"
|
||||||
|
],
|
||||||
|
"gong": "兑"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "NE",
|
||||||
|
"spirit": "直符",
|
||||||
|
"star": "天任",
|
||||||
|
"door": "生门",
|
||||||
|
"stems": [
|
||||||
|
"戊",
|
||||||
|
"丙"
|
||||||
|
],
|
||||||
|
"gong": "艮"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "N",
|
||||||
|
"spirit": "腾蛇",
|
||||||
|
"star": "天蓬",
|
||||||
|
"door": "休门",
|
||||||
|
"stems": [
|
||||||
|
"癸",
|
||||||
|
"辛"
|
||||||
|
],
|
||||||
|
"gong": "坎"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "NW",
|
||||||
|
"spirit": "太阴",
|
||||||
|
"star": "天心",
|
||||||
|
"door": "开门",
|
||||||
|
"stems": [
|
||||||
|
"辛",
|
||||||
|
"癸"
|
||||||
|
],
|
||||||
|
"gong": "乾"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"analysis": [
|
||||||
|
"【戊加壬】为「青龙入天牢」,凡阴阳皆不吉利。主田宅文书失落,官司破财,小凶。",
|
||||||
|
"【杜门加死】主田宅文书失落,官司破财,小凶。杜门加壬,主奸盗事,凶。"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
"elementScores": [
|
||||||
|
{
|
||||||
|
"label": "金 (同类 · 极旺)",
|
||||||
|
"score": "3.008",
|
||||||
|
"width": "60%",
|
||||||
|
"color": "#e5e7eb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "土 (同类 · 旺)",
|
||||||
|
"score": "2.500",
|
||||||
|
"width": "50%",
|
||||||
|
"color": "#ca8a04"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "火 (异类 · 中)",
|
||||||
|
"score": "2.200",
|
||||||
|
"width": "44%",
|
||||||
|
"color": "#ef4444"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "水 (异类 · 弱)",
|
||||||
|
"score": "0.600",
|
||||||
|
"width": "12%",
|
||||||
|
"color": "#3b82f6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "木 (异类 · 极弱)",
|
||||||
|
"score": "0.300",
|
||||||
|
"width": "6%",
|
||||||
|
"color": "#22c55e"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"elementAdvice": [
|
||||||
|
{
|
||||||
|
"k": "五行",
|
||||||
|
"good": "木、火",
|
||||||
|
"bad": "土、金"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "颜色",
|
||||||
|
"good": "绿、红、紫",
|
||||||
|
"bad": "黄、白、金"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "方位",
|
||||||
|
"good": "正东、正南",
|
||||||
|
"bad": "中宫、正西"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "数字",
|
||||||
|
"good": "3, 4, 9",
|
||||||
|
"bad": "5, 6, 7, 8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"personality": {
|
||||||
|
"main": {
|
||||||
|
"title": "主性格:正官格",
|
||||||
|
"description": "为人正直,做事循规蹈矩,重视名誉地位,有时过于保守,缺乏变通。"
|
||||||
|
},
|
||||||
|
"blindSpot": {
|
||||||
|
"title": "潜在盲点:七杀暗藏",
|
||||||
|
"description": "内心深处有叛逆因子,压力大时易爆发极端情绪,需注意情绪疏导。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"career": {
|
||||||
|
"industries": [
|
||||||
|
"房地产",
|
||||||
|
"金融",
|
||||||
|
"法律",
|
||||||
|
"五金"
|
||||||
|
],
|
||||||
|
"wealthLevel": "小富之命 (A级)",
|
||||||
|
"wealthDescription": "一生衣食无忧,中晚年财库丰盈。"
|
||||||
|
},
|
||||||
|
"marriage": {
|
||||||
|
"spouseStar": "丁火 · 坐库",
|
||||||
|
"spousePalace": "辰土 · 偏印",
|
||||||
|
"analysis": "夫星得位,丈夫能力强,多为公职或管理人员。但日坐偏印,夫妻沟通易有隔阂,且辰戌相冲(时支冲夫宫),晚婚为宜,早婚易有波折。"
|
||||||
|
},
|
||||||
|
"health": [
|
||||||
|
{
|
||||||
|
"issue": "金气过旺",
|
||||||
|
"risks": "呼吸系统、肺部、皮肤"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"issue": "木气极弱",
|
||||||
|
"risks": "肝胆、免疫力、神经系统"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
"wealthRadar": {
|
||||||
|
"dimensions": [
|
||||||
|
{
|
||||||
|
"label": "赚钱",
|
||||||
|
"score": "S",
|
||||||
|
"position": "top"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "投资",
|
||||||
|
"score": "A",
|
||||||
|
"position": "right"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "守财",
|
||||||
|
"score": "A",
|
||||||
|
"position": "bottom-right"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "抗险",
|
||||||
|
"score": "B",
|
||||||
|
"position": "bottom-left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "偏财",
|
||||||
|
"score": "S",
|
||||||
|
"position": "left"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"wealthBars": [
|
||||||
|
{
|
||||||
|
"label": "赚钱爆发力",
|
||||||
|
"score": 98,
|
||||||
|
"width": "98%",
|
||||||
|
"color": "#d4af37"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "守财稳健度",
|
||||||
|
"score": 85,
|
||||||
|
"width": "85%",
|
||||||
|
"color": "#eab308"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "投资敏锐度",
|
||||||
|
"score": 78,
|
||||||
|
"width": "78%",
|
||||||
|
"color": "#60a5fa"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shensha": [
|
||||||
|
{
|
||||||
|
"name": "天乙贵人",
|
||||||
|
"pos": "月柱",
|
||||||
|
"level": "吉"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "文昌贵人",
|
||||||
|
"pos": "月柱",
|
||||||
|
"level": "吉"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "月德贵人",
|
||||||
|
"pos": "日柱",
|
||||||
|
"level": "吉"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "红艳煞",
|
||||||
|
"pos": "时柱",
|
||||||
|
"level": "平"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "寡宿",
|
||||||
|
"pos": "时柱",
|
||||||
|
"level": "凶"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dayun": [
|
||||||
|
{
|
||||||
|
"name": "庚戌",
|
||||||
|
"range": "1-10岁",
|
||||||
|
"desc": "土金相生,金气更旺。日主得助,童年生活安稳。",
|
||||||
|
"current": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "辛亥",
|
||||||
|
"range": "11-20岁",
|
||||||
|
"desc": "金水相生,伤官透出。思维活跃,学业上有钻研精神。",
|
||||||
|
"current": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "壬子",
|
||||||
|
"range": "21-30岁",
|
||||||
|
"desc": "【当前大运】水势旺盛,伤官成局。主事业上易有突破,靠才智技术谋生。",
|
||||||
|
"current": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "癸丑",
|
||||||
|
"range": "31-40岁",
|
||||||
|
"desc": "土水相杂,印星制伤。运势趋于稳重,易有职务提升。",
|
||||||
|
"current": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"taisui": {
|
||||||
|
"general": "吴遂",
|
||||||
|
"title": "乙巳年值年太岁",
|
||||||
|
"description": "吴遂大将军,字至道。乙巳太岁,在东南方。信条:赏罚分明,无私无畏。",
|
||||||
|
"warning": "您本年刑太岁。建议年初八拜太岁,或在钱包内放置一张"太岁符"化解。"
|
||||||
|
}
|
||||||
|
|
||||||
|
"annualAnalysis": {
|
||||||
|
"year": "2025",
|
||||||
|
"ganzhi": "乙巳",
|
||||||
|
"age": 28,
|
||||||
|
"tianGan": {
|
||||||
|
"element": "乙木",
|
||||||
|
"star": "正财星 · 透出"
|
||||||
|
},
|
||||||
|
"diZhi": {
|
||||||
|
"element": "巳火",
|
||||||
|
"star": "七杀星 · 暗藏"
|
||||||
|
},
|
||||||
|
"pattern": "财官相生局 · 犯太岁 (刑)",
|
||||||
|
"aspects": [
|
||||||
|
{
|
||||||
|
"k": "事业",
|
||||||
|
"v": "升职期"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "感情",
|
||||||
|
"v": "暗桃花"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "健康",
|
||||||
|
"v": "防炎症"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"chart": {
|
||||||
|
"title": "2025 财运起伏",
|
||||||
|
"data": [
|
||||||
|
40,
|
||||||
|
45,
|
||||||
|
60,
|
||||||
|
80,
|
||||||
|
95,
|
||||||
|
70,
|
||||||
|
65,
|
||||||
|
85,
|
||||||
|
90,
|
||||||
|
55,
|
||||||
|
50,
|
||||||
|
60
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"yearlyStars": [
|
||||||
|
{
|
||||||
|
"name": "流年禄神",
|
||||||
|
"bad": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "流年天乙",
|
||||||
|
"bad": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "流年红鸾",
|
||||||
|
"bad": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "流年金舆",
|
||||||
|
"bad": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "流年羊刃",
|
||||||
|
"bad": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "流年丧门",
|
||||||
|
"bad": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "流年吊客",
|
||||||
|
"bad": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "流年官符",
|
||||||
|
"bad": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"roleAdvice": [
|
||||||
|
{
|
||||||
|
"icon": "职",
|
||||||
|
"title": "职场 / 公务员",
|
||||||
|
"desc": "官星透出,利于升职考核。但要注意和上司的关系,容易因为意见不合而产生隔阂。建议多做少说,以退为进。",
|
||||||
|
"bg": "rgba(59,130,246,0.2)",
|
||||||
|
"color": "#60a5fa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "财",
|
||||||
|
"title": "经商 / 创业者",
|
||||||
|
"desc": "财源不错,但开销也大。今年适合稳扎稳打,不宜盲目扩张。特别注意合同细节,防止因为文书错误导致破财。",
|
||||||
|
"bg": "rgba(212,175,55,0.2)",
|
||||||
|
"color": "#d4af37"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
"directions": {
|
||||||
|
"lucky": [
|
||||||
|
"东北",
|
||||||
|
"正南",
|
||||||
|
"东南"
|
||||||
|
],
|
||||||
|
"unlucky": [
|
||||||
|
"西南",
|
||||||
|
"中宫"
|
||||||
|
],
|
||||||
|
"compass": {
|
||||||
|
"north": {
|
||||||
|
"label": "正北",
|
||||||
|
"type": "neutral"
|
||||||
|
},
|
||||||
|
"south": {
|
||||||
|
"label": "正南 (喜神)",
|
||||||
|
"type": "lucky"
|
||||||
|
},
|
||||||
|
"east": {
|
||||||
|
"label": "正东",
|
||||||
|
"type": "neutral"
|
||||||
|
},
|
||||||
|
"west": {
|
||||||
|
"label": "正西",
|
||||||
|
"type": "neutral"
|
||||||
|
},
|
||||||
|
"northeast": {
|
||||||
|
"label": "东北 (正财)",
|
||||||
|
"type": "lucky"
|
||||||
|
},
|
||||||
|
"southeast": {
|
||||||
|
"label": "东南 (文昌)",
|
||||||
|
"type": "lucky"
|
||||||
|
},
|
||||||
|
"southwest": {
|
||||||
|
"label": "西南 (病符)",
|
||||||
|
"type": "unlucky"
|
||||||
|
},
|
||||||
|
"northwest": {
|
||||||
|
"label": "西北",
|
||||||
|
"type": "neutral"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"incomeSource": {
|
||||||
|
"chart": "pie",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"label": "正职收入",
|
||||||
|
"percentage": 65,
|
||||||
|
"color": "#d4af37"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "副业兼职",
|
||||||
|
"percentage": 20,
|
||||||
|
"color": "#3b82f6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "投资理财",
|
||||||
|
"percentage": 15,
|
||||||
|
"color": "#22c55e"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"investment": {
|
||||||
|
"good": {
|
||||||
|
"level": "大吉 · 宜入",
|
||||||
|
"items": [
|
||||||
|
"文化传媒",
|
||||||
|
"教育培训",
|
||||||
|
"林业"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bad": {
|
||||||
|
"level": "大凶 · 避让",
|
||||||
|
"items": [
|
||||||
|
"水产养殖",
|
||||||
|
"远洋运输",
|
||||||
|
"酒水"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flyingStars": [
|
||||||
|
{
|
||||||
|
"n": 4,
|
||||||
|
"good": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 9,
|
||||||
|
"good": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 2,
|
||||||
|
"good": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 3,
|
||||||
|
"good": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 5,
|
||||||
|
"good": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 7,
|
||||||
|
"good": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 8,
|
||||||
|
"good": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 1,
|
||||||
|
"good": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 6,
|
||||||
|
"good": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"misfortuneBreaker": [
|
||||||
|
{
|
||||||
|
"issue": "犯太岁 (刑)",
|
||||||
|
"solution": "今年与太岁相刑,易有是非口舌。建议立春后捐血或洗牙,以化解"血光之灾"。",
|
||||||
|
"severity": "high"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"issue": "白虎煞",
|
||||||
|
"solution": "注意交通安全,避免从事高风险运动。车内可挂平安符。",
|
||||||
|
"severity": "medium"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
"monthlyRows": [
|
||||||
|
{
|
||||||
|
"m": "正月",
|
||||||
|
"g": "庚寅",
|
||||||
|
"d": "驿马星动,求财宜动不宜静,利远行。",
|
||||||
|
"l": "中平"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"m": "二月",
|
||||||
|
"g": "辛卯",
|
||||||
|
"d": "桃花入命,单身有利,已婚防烂桃花。",
|
||||||
|
"l": "吉"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"m": "三月",
|
||||||
|
"g": "壬辰",
|
||||||
|
"d": "财库大开,投资理财如有神助,忌担保。",
|
||||||
|
"l": "大吉"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"m": "四月",
|
||||||
|
"g": "癸巳",
|
||||||
|
"d": "官杀混杂,工作压力大,易有口舌是非。",
|
||||||
|
"l": "凶"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"m": "五月",
|
||||||
|
"g": "甲午",
|
||||||
|
"d": "午火太旺,心浮气躁,防破财消灾。",
|
||||||
|
"l": "小凶"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"m": "六月",
|
||||||
|
"g": "乙未",
|
||||||
|
"d": "印星护身,贵人提拔,事业有新机遇。",
|
||||||
|
"l": "吉"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"m": "七月",
|
||||||
|
"g": "丙申",
|
||||||
|
"d": "食神生财,技术生财,利于创意工作。",
|
||||||
|
"l": "吉"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"m": "八月",
|
||||||
|
"g": "丁酉",
|
||||||
|
"d": "比劫夺财,朋友借钱需谨慎,守财为上。",
|
||||||
|
"l": "凶"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"m": "九月",
|
||||||
|
"g": "戊戌",
|
||||||
|
"d": "土气过重,做事迟缓,宜静心进修。",
|
||||||
|
"l": "中平"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"m": "十月",
|
||||||
|
"g": "己亥",
|
||||||
|
"d": "水木相生,思维活跃,利于考试考核。",
|
||||||
|
"l": "吉"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"m": "冬月",
|
||||||
|
"g": "庚子",
|
||||||
|
"d": "桃花带煞,注意男女关系,防桃色纠纷。",
|
||||||
|
"l": "小凶"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"m": "腊月",
|
||||||
|
"g": "辛丑",
|
||||||
|
"d": "库门重开,年底分红可观,诸事顺遂。",
|
||||||
|
"l": "大吉"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"deepDive": [
|
||||||
|
{
|
||||||
|
"month": "正月 (戊寅)",
|
||||||
|
"ganzhi": "庚寅年 · 戊寅月",
|
||||||
|
"nayin": "城头土",
|
||||||
|
"score": 4,
|
||||||
|
"desc": "本月驿马星动,虽有奔波之苦,但动中生财。适合出差、旅行、谈合作。注意交通安全,避免疲劳驾驶。",
|
||||||
|
"lucky": "2日, 15日, 28日",
|
||||||
|
"unlucky": "5日, 17日, 29日",
|
||||||
|
"advice": "随身佩戴黑曜石手串,化解是非口舌。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"month": "二月 (己卯)",
|
||||||
|
"ganzhi": "庚寅年 · 己卯月",
|
||||||
|
"nayin": "城头土",
|
||||||
|
"score": 5,
|
||||||
|
"desc": "桃花星入命,人际关系和谐,利于拓展人脉。单身者有机会结识心仪对象,已婚者需防墙外桃花。",
|
||||||
|
"lucky": "6日, 18日, 30日",
|
||||||
|
"unlucky": "1日, 13日, 25日",
|
||||||
|
"advice": "在卧室正东方摆放粉水晶,催旺正缘。"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"monthlyTrend": [
|
||||||
|
45,
|
||||||
|
55,
|
||||||
|
72,
|
||||||
|
38,
|
||||||
|
60,
|
||||||
|
78,
|
||||||
|
66,
|
||||||
|
40,
|
||||||
|
52,
|
||||||
|
70,
|
||||||
|
48,
|
||||||
|
62
|
||||||
|
]
|
||||||
|
|
||||||
|
"weekdays": [
|
||||||
|
"日",
|
||||||
|
"一",
|
||||||
|
"二",
|
||||||
|
"三",
|
||||||
|
"四",
|
||||||
|
"五",
|
||||||
|
"六"
|
||||||
|
],
|
||||||
|
"hourList": [
|
||||||
|
"子(凶)",
|
||||||
|
"丑(吉)",
|
||||||
|
"寅(平)",
|
||||||
|
"卯(吉)",
|
||||||
|
"辰(凶)",
|
||||||
|
"巳(平)",
|
||||||
|
"午(吉)",
|
||||||
|
"未(平)"
|
||||||
|
],
|
||||||
|
"calendar": {
|
||||||
|
"year": 2025,
|
||||||
|
"month": 5,
|
||||||
|
"days": [
|
||||||
|
{
|
||||||
|
"date": 20,
|
||||||
|
"lunar": "四月廿三",
|
||||||
|
"lucky": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": 21,
|
||||||
|
"lunar": "四月廿四",
|
||||||
|
"lucky": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": 22,
|
||||||
|
"lunar": "四月廿五",
|
||||||
|
"lucky": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": 23,
|
||||||
|
"lunar": "四月廿六",
|
||||||
|
"lucky": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": 24,
|
||||||
|
"lunar": "四月廿七",
|
||||||
|
"lucky": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": 25,
|
||||||
|
"lunar": "四月廿八",
|
||||||
|
"lucky": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dailyFortune": {
|
||||||
|
"date": "5月24日",
|
||||||
|
"caishenDirection": "东北",
|
||||||
|
"xishenDirection": "正南",
|
||||||
|
"suitable": [
|
||||||
|
"签约",
|
||||||
|
"交易",
|
||||||
|
"纳财",
|
||||||
|
"开市"
|
||||||
|
],
|
||||||
|
"unsuitable": [
|
||||||
|
"借贷",
|
||||||
|
"诉讼",
|
||||||
|
"动土"
|
||||||
|
],
|
||||||
|
"luckyTip": "今日午时(11:00-13:00),面向南方晒背15分钟,可吸纳纯阳之气,增强运势。"
|
||||||
|
},
|
||||||
|
"nobleman": {
|
||||||
|
"type": "天乙",
|
||||||
|
"features": "脸型微方,声音洪亮。",
|
||||||
|
"direction": "西北方 (乾宫)",
|
||||||
|
"occupation": [
|
||||||
|
"金融",
|
||||||
|
"法律",
|
||||||
|
"政府"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shaQi": [
|
||||||
|
{
|
||||||
|
"name": "天斩煞",
|
||||||
|
"desc": "两楼夹缝对窗",
|
||||||
|
"fix": "挂凸面镜 / 摆放龙龟"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "路冲煞",
|
||||||
|
"desc": "直路冲大门/窗",
|
||||||
|
"fix": "种植树木 / 摆放泰山石敢当"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "尖角煞",
|
||||||
|
"desc": "建筑尖角对窗",
|
||||||
|
"fix": "挂葫芦 / 放置麒麟"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rooms": [
|
||||||
|
{
|
||||||
|
"icon": "厅",
|
||||||
|
"title": "客厅 (聚气)",
|
||||||
|
"tag": "明堂宜亮",
|
||||||
|
"tagCls": "room-good",
|
||||||
|
"desc": "沙发必须靠墙(有靠山)。进门对角线位置不要摆放饮水机或鱼缸,以免"见财化水"。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "厨",
|
||||||
|
"title": "厨房 (财库)",
|
||||||
|
"tag": "水火不容",
|
||||||
|
"tagCls": "room-bad",
|
||||||
|
"desc": "米缸常满,冰箱忌空。灶台不可正对水槽(水火相冲),中间可用绿色植物化解。"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
"plants": [
|
||||||
|
{
|
||||||
|
"place": "客厅",
|
||||||
|
"plant": "发财树"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"place": "玄关",
|
||||||
|
"plant": "仙人掌"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"place": "阳台",
|
||||||
|
"plant": "万年青"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lovePosition": {
|
||||||
|
"direction": "正南方",
|
||||||
|
"single": "在此方位摆放9枝红玫瑰(去刺),催旺正缘。",
|
||||||
|
"married": "在此方位放置紫水晶球或夫妻合照,防烂桃花。"
|
||||||
|
},
|
||||||
|
"pets": {
|
||||||
|
"cat": {
|
||||||
|
"direction": "东北 (寅)",
|
||||||
|
"reason": "老虎同宗,增旺气场。"
|
||||||
|
},
|
||||||
|
"dog": {
|
||||||
|
"direction": "西北 (戌) / 南 (午)",
|
||||||
|
"reason": "三合火局,守家护院。"
|
||||||
|
},
|
||||||
|
"warning": "宠物便盆切忌放在全屋正中心(太极点),易导致全家病痛。"
|
||||||
|
},
|
||||||
|
"digitalEnergy": {
|
||||||
|
"phoneNumber": {
|
||||||
|
"lucky": [
|
||||||
|
"1",
|
||||||
|
"6",
|
||||||
|
"8",
|
||||||
|
"9"
|
||||||
|
],
|
||||||
|
"reason": "利于贵人运与正财运。"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"examples": [
|
||||||
|
"1314",
|
||||||
|
"8899"
|
||||||
|
],
|
||||||
|
"meaning": "一生一世 / 发发久久。"
|
||||||
|
},
|
||||||
|
"wallpaper": "金山银山、日出东方、或您的贵人属相图(如属猴者用蛇)。"
|
||||||
|
},
|
||||||
|
"car": {
|
||||||
|
"decoration": [
|
||||||
|
"出入平安符",
|
||||||
|
"葫芦(收煞)"
|
||||||
|
],
|
||||||
|
"scent": [
|
||||||
|
"檀香",
|
||||||
|
"沉香(定神)"
|
||||||
|
],
|
||||||
|
"forbidden": "车内堆放太多玩偶、摆放刀剑利器。"
|
||||||
|
},
|
||||||
|
"wallet": {
|
||||||
|
"goodColors": [
|
||||||
|
{
|
||||||
|
"color": "黑色",
|
||||||
|
"effect": "守财"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "深咖色",
|
||||||
|
"effect": "聚财"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"badColors": [
|
||||||
|
{
|
||||||
|
"color": "红色",
|
||||||
|
"effect": "赤字"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "蓝色",
|
||||||
|
"effect": "流财"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"luckyColors": [
|
||||||
|
"#d4af37",
|
||||||
|
"#ffffff",
|
||||||
|
"#92400e"
|
||||||
|
],
|
||||||
|
"accessories": [
|
||||||
|
"黄水晶",
|
||||||
|
"金发晶",
|
||||||
|
"和田玉"
|
||||||
|
]
|
||||||
|
}
|
||||||
20
前端源码/uni-app/postcss-rpx-to-vw.js
Normal file
20
前端源码/uni-app/postcss-rpx-to-vw.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// PostCSS 插件:将 rpx 单位转换为 vw
|
||||||
|
// 基于 750px 设计稿,1rpx = 100vw / 750 = 0.1333vw
|
||||||
|
|
||||||
|
const postcssRpxToVw = () => {
|
||||||
|
return {
|
||||||
|
postcssPlugin: 'postcss-rpx-to-vw',
|
||||||
|
Declaration(decl) {
|
||||||
|
if (decl.value.includes('rpx')) {
|
||||||
|
decl.value = decl.value.replace(/(\d+(?:\.\d+)?)rpx/g, (match, num) => {
|
||||||
|
const vw = (parseFloat(num) / 750 * 100).toFixed(4);
|
||||||
|
return `${vw}vw`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
postcssRpxToVw.postcss = true;
|
||||||
|
|
||||||
|
export default postcssRpxToVw;
|
||||||
67
前端源码/uni-app/router/index.js
Normal file
67
前端源码/uni-app/router/index.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
// 主页面容器
|
||||||
|
const IndexPage = () => import('../pages/index/index.vue')
|
||||||
|
const LoginPage = () => import('../pages/login/login.vue')
|
||||||
|
const TestNameLivePage = () => import('../pages/test-name-live/test-name-live.vue')
|
||||||
|
const UserAgreementPage = () => import('../components/screens/UserAgreement.vue')
|
||||||
|
const PrivacyPolicyPage = () => import('../components/screens/PrivacyPolicy.vue')
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: LoginPage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Index',
|
||||||
|
component: IndexPage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/user-agreement',
|
||||||
|
name: 'UserAgreement',
|
||||||
|
component: UserAgreementPage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/privacy-policy',
|
||||||
|
name: 'PrivacyPolicy',
|
||||||
|
component: PrivacyPolicyPage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/home',
|
||||||
|
redirect: '/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/test-name-live',
|
||||||
|
name: 'TestNameLive',
|
||||||
|
component: TestNameLivePage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
redirect: '/'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
// 路由错误处理
|
||||||
|
router.onError((error) => {
|
||||||
|
console.error('Router error:', error)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 路由守卫 - 处理未匹配的路由
|
||||||
|
router.beforeEach((to, _from, next) => {
|
||||||
|
// 如果路由不存在,重定向到首页
|
||||||
|
if (!to.matched.length) {
|
||||||
|
console.warn(`Route not found: ${to.path}, redirecting to /`)
|
||||||
|
next('/')
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
4284
前端源码/uni-app/style/index.css
Normal file
4284
前端源码/uni-app/style/index.css
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user