Compare commits

..

6 Commits

Author SHA1 Message Date
zdl
6a51fc3c88 Merge branch 'feature' into develop 2025-10-27 18:03:17 +08:00
zdl
7cca5e73c0 Merge branch 'feature' into develop 2025-10-21 11:02:02 +08:00
zdl
112fbbd42d Merge branch 'feature' into develop 2025-10-17 15:01:54 +08:00
zdl
3a4dade8ec Merge branch 'main' into develop 2025-10-15 21:01:59 +08:00
zdl
6f81259f8c feat: 解决导航跳转失效的问题 2025-10-15 11:57:28 +08:00
zdl
864844a52b feat: 10.10线上最新代码提交 2025-10-15 11:56:34 +08:00
2359 changed files with 679910 additions and 272320 deletions

View File

@@ -0,0 +1,18 @@
{
"permissions": {
"allow": [
"Read(//Users/qiye/**)",
"Bash(npm run lint:check)",
"Bash(npm run build)",
"Bash(chmod +x /Users/qiye/Desktop/jzqy/vf_react/scripts/*.sh)",
"Bash(node scripts/parseIndustryCSV.js)",
"Bash(cat:*)",
"Bash(npm cache clean --force)",
"Bash(npm install)",
"Bash(npm run start:mock)",
"Bash(npm install fsevents@latest --save-optional --force)"
],
"deny": [],
"ask": []
}
}

View File

@@ -6,7 +6,7 @@
SERVER_HOST=your-server-ip-or-domain
# SSH 用户名
SERVER_USER=ubuntu
SERVER_USER=ubuntu
# SSH 端口
SERVER_PORT=22

View File

@@ -1,5 +1,5 @@
# 开发环境配置(连接真实后端)
# 使用方式: npm run start:dev
# 使用方式: npm start
# React 构建优化配置
GENERATE_SOURCEMAP=false
@@ -18,8 +18,3 @@ REACT_APP_ENABLE_MOCK=false
# 开发环境标识
REACT_APP_ENV=development
# 性能监控配置
REACT_APP_ENABLE_PERFORMANCE_MONITOR=true
REACT_APP_ENABLE_PERFORMANCE_PANEL=true
REACT_APP_REPORT_TO_POSTHOG=false

View File

@@ -29,10 +29,6 @@ NODE_OPTIONS=--max_old_space_size=4096
# MSW 会在浏览器层拦截这些请求,不需要真实的后端地址
REACT_APP_API_URL=
# Socket.IO 连接地址Mock 模式下连接生产环境)
# 注意WebSocket 不被 MSW 拦截,可以独立配置
REACT_APP_SOCKET_URL=https://valuefrontier.cn
# 启用 Mock 数据(核心配置)
# 此配置会触发 src/index.js 中的 MSW 初始化
REACT_APP_ENABLE_MOCK=true

View File

@@ -1,48 +0,0 @@
# ========================================
# 生产环境配置
# ========================================
# 环境标识
REACT_APP_ENV=production
NODE_ENV=production
# Mock 配置(生产环境禁用 Mock
REACT_APP_ENABLE_MOCK=false
# 🔧 调试模式(生产环境临时调试用)
# 开启后会在全局暴露 window.__DEBUG__
REACT_APP_ENABLE_DEBUG=false
# 后端 API 地址(生产环境)
# 使用单独的 API 域名,静态资源走 CDNAPI 走专用域名
REACT_APP_API_URL=https://api.valuefrontier.cn
# PostHog 分析配置(生产环境)
# PostHog API Key从 PostHog 项目设置中获取)
REACT_APP_POSTHOG_KEY=phc_xKlRyG69Bx7hgOdFeCeLUvQWvSjw18ZKFgCwCeYezWF
# PostHog API Host使用 PostHog Cloud
REACT_APP_POSTHOG_HOST=https://app.posthog.com
# 启用会话录制Session Recording用于回放用户操作、排查问题
REACT_APP_ENABLE_SESSION_RECORDING=true
# React 构建优化配置
# 禁用 source map 生成(生产环境不需要,提升打包速度和安全性)
GENERATE_SOURCEMAP=false
# 跳过预检查(加快启动速度)
SKIP_PREFLIGHT_CHECK=true
# 禁用 ESLint 检查(生产构建时不需要)
DISABLE_ESLINT_PLUGIN=true
# TypeScript 编译错误时继续
TSC_COMPILE_ON_ERROR=true
# 图片内联大小限制
IMAGE_INLINE_SIZE_LIMIT=10000
# Node.js 内存限制(适用于大型项目)
NODE_OPTIONS=--max_old_space_size=4096
# 性能监控配置(生产环境)
# 启用性能监控
REACT_APP_ENABLE_PERFORMANCE_MONITOR=true
# 禁用性能面板(仅开发环境)
REACT_APP_ENABLE_PERFORMANCE_PANEL=false
# 启用 PostHog 性能数据上报
REACT_APP_REPORT_TO_POSTHOG=true

View File

@@ -1,42 +0,0 @@
# ========================================
# 本地测试环境(前后端都在本地)
# ========================================
# 使用方式: npm run start:test
#
# 工作原理:
# 1. concurrently 同时启动前端和后端
# 2. 前端: localhost:3000
# 3. 后端: localhost:5001 (python app_2.py)
# 4. 数据: 本地数据库
#
# 适用场景:
# - 调试后端代码
# - 性能测试
# - 离线开发
# - 数据库调试
# ========================================
# 环境标识
REACT_APP_ENV=test
NODE_ENV=development
# Mock 配置(关闭 MSW
REACT_APP_ENABLE_MOCK=false
# 后端 API 地址(本地后端)
REACT_APP_API_URL=http://localhost:5001
# PostHog 配置(测试环境)
# 留空 = 仅控制台 debug
# 填入 Key = 控制台 + PostHog Cloud 双模式
REACT_APP_POSTHOG_KEY=
REACT_APP_POSTHOG_HOST=https://app.posthog.com
REACT_APP_ENABLE_SESSION_RECORDING=false
# React 构建优化配置
GENERATE_SOURCEMAP=true # 测试环境保留 sourcemap 便于调试
SKIP_PREFLIGHT_CHECK=true
DISABLE_ESLINT_PLUGIN=false # 测试环境开启 ESLint
TSC_COMPILE_ON_ERROR=true
IMAGE_INLINE_SIZE_LIMIT=10000
NODE_OPTIONS=--max_old_space_size=4096

View File

@@ -1,94 +0,0 @@
module.exports = {
root: true,
/* 环境配置 */
env: {
browser: true,
es2021: true,
node: true,
},
/* 扩展配置 */
extends: [
'react-app', // Create React App 默认规则
'react-app/jest', // Jest 测试规则
'eslint:recommended', // ESLint 推荐规则
'plugin:react/recommended', // React 推荐规则
'plugin:react-hooks/recommended', // React Hooks 规则
'plugin:prettier/recommended', // Prettier 集成
],
/* 解析器选项 */
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
/* 插件 */
plugins: ['react', 'react-hooks', 'prettier'],
/* 规则配置 */
rules: {
// React
'react/react-in-jsx-scope': 'off', // React 17+ 不需要导入 React
'react/prop-types': 'off', // 使用 TypeScript 类型检查,不需要 PropTypes
'react/display-name': 'off', // 允许匿名组件
// 通用
'no-console': ['warn', { allow: ['warn', 'error'] }], // 仅警告 console.log
'no-unused-vars': ['warn', {
argsIgnorePattern: '^_', // 忽略以 _ 开头的未使用参数
varsIgnorePattern: '^_', // 忽略以 _ 开头的未使用变量
}],
'prettier/prettier': ['warn', {}, { usePrettierrc: true }], // 使用项目的 Prettier 配置
},
/* 设置 */
settings: {
react: {
version: 'detect', // 自动检测 React 版本
},
},
/* TypeScript 文件特殊配置 */
// 注意react-app 已包含完整的 @typescript-eslint 配置
// 此处仅覆盖特定规则,不重复加载插件
overrides: [
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
// TypeScript 特定规则覆盖
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': ['warn', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
}],
'@typescript-eslint/no-non-null-assertion': 'warn',
'no-unused-vars': 'off',
},
},
// Company 视图主题硬编码检测
{
files: ['src/views/Company/**/*.{ts,tsx,js,jsx}'],
excludedFiles: ['**/theme/**', '**/*.test.*', '**/*.spec.*'],
plugins: ['local-rules'],
rules: {
// warning 级别:提醒开发者但不阻塞构建
'local-rules/no-hardcoded-fui-colors': 'warn',
},
},
],
/* 忽略文件(与 .eslintignore 等效)*/
ignorePatterns: [
'node_modules/',
'build/',
'dist/',
'*.config.js',
'public/mockServiceWorker.js',
],
};

16
.gitignore vendored
View File

@@ -22,10 +22,6 @@ node_modules/
.env.test.local
.env.production.local
# 部署配置(包含密钥,不提交)
.env.cos
.env.deploy
# 日志
npm-debug.log*
yarn-debug.log*
@@ -39,9 +35,6 @@ pnpm-debug.log*
*.swo
*~
# Claude Code 配置
.claude/settings.local.json
# macOS
.DS_Store
@@ -53,13 +46,4 @@ Thumbs.db
!README.md
!CLAUDE.md
# 忽略 docs 目录(开发文档不提交到 Git
docs/
src/assets/img/original-backup/
# 涨停分析静态数据(由 export_zt_data.py 生成,不提交到 Git
public/data/zt/
# 概念涨跌幅静态数据(由 export_concept_data.py 生成,不提交到 Git
public/data/concept/

255
CLAUDE.md
View File

@@ -1,208 +1,91 @@
# CLAUDE.md
> **🌐 语言偏好**: 请始终使用中文与用户交流,包括所有解释、分析、文档编写和代码注释。
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
本文件为 Claude Code 提供在此代码库中工作的指导说明。
## Project Overview
---
This is a hybrid React dashboard application with a Flask/Python backend. The project is built on the Argon Dashboard Chakra PRO template and includes financial/trading analysis features.
## 项目概览
### Frontend (React + Chakra UI)
- **Framework**: React 18.3.1 with Chakra UI 2.8.2
- **Styling**: Tailwind CSS + custom Chakra theme
- **Build Tool**: React Scripts with custom Gulp tasks
- **Charts**: ApexCharts, ECharts, and custom visualization components
混合式 React 仪表板,用于金融/交易分析,采用 Flask 后端。基于 Argon Dashboard Chakra PRO 模板构建。
### Backend (Flask/Python)
- **Framework**: Flask with SQLAlchemy ORM
- **Database**: ClickHouse for analytics + MySQL/PostgreSQL
- **Features**: Real-time data processing, trading analysis, user authentication
- **Task Queue**: Celery for background processing
### 技术栈
**前端**
- **核心**: React 18.3.1 + TypeScript 5.9.3(渐进式迁移中)
- **UI**: Chakra UI 2.10.9(主要)+ Ant Design 5.27.4(表格/表单)+ HeroUI 3.0.0-betaAgentChat
- **状态**: Redux Toolkit 2.9.2
- **路由**: React Router v6.30.1 + React.lazy() 代码分割
- **构建**: CRACO 7.1.0 + webpack 5 优化
- **图表**: ECharts 5.6.0、ApexCharts、Recharts、D3、Visx
- **其他**: Framer Motion、FullCalendar、Socket.IO Client、MSW、PostHog
**后端**
- Flask + SQLAlchemy ORM
- ClickHouse分析型+ MySQL事务型+ Redis缓存
- Flask-SocketIOWebSocket+ Celery后台任务
---
## 开发命令
## Development Commands
### Frontend Development
```bash
# 前端开发
npm start # Mock 模式启动(默认)
npm run start:real # 真实后端
npm run build # 生产构建
npm run lint:check # ESLint 检查
npm run type-check # TypeScript 类型检查
# 后端开发
python app.py # Flask 服务器
python simulation_background_processor.py # Celery 后台处理
# 部署
npm run deploy # 部署到生产环境
npm start # Start development server (port 3000, proxies to localhost:5001)
npm run build # Production build with license headers
npm test # Run React test suite
npm run lint:check # Check ESLint rules
npm run lint:fix # Auto-fix ESLint issues
npm run install:clean # Clean install (removes node_modules and package-lock)
```
---
## 架构
### 应用入口流程
```
src/index.js → App.js
├── AppProviders (Redux → Chakra → Ant ConfigProvider → Notification → Auth)
├── AppRoutes (src/routes/index.js)
└── GlobalComponents
```
### 路由保护模式
- `PUBLIC` - 无需认证
- `MODAL` - 未登录显示认证模态框
- `REDIRECT` - 未登录重定向到登录页
### 目录结构速查
| 目录 | 用途 |
|------|------|
| `src/views/` | 页面组件(按功能模块组织) |
| `src/components/` | 可复用 UI 组件 |
| `src/services/` | API 服务层 |
| `src/store/slices/` | Redux 状态 |
| `src/hooks/` | 自定义 Hooks |
| `src/contexts/` | React Context |
| `src/utils/` | 工具函数 |
| `src/constants/` | 常量定义 |
| `src/types/` | TypeScript 类型 |
| `src/theme/` | Chakra UI 主题 |
| `src/routes/` | 路由配置 |
| `src/mocks/` | MSW Mock 数据 |
| `src/layouts/` | 页面布局模板 |
### 路径别名
```
@/ → src/
@components/ → src/components/
@views/ → src/views/
@services/ → src/services/
@store/ → src/store/
@hooks/ → src/hooks/
@utils/ → src/utils/
@types/ → src/types/
@constants/ → src/constants/
```
---
## 开发工作流
### 添加新路由
1. `src/routes/lazy-components.js` - 添加 lazy import
2. `src/routes/routeConfig.js` - 配置路由path、component、protection、layout
### 添加新 API
1. `src/services/` - 创建服务函数
2. `src/mocks/handlers/` - 添加 MSW handlerMock 模式)
### 添加新 Redux Slice
1. `src/store/slices/yourSlice.ts` - 创建 slice
2. `src/store/index.js` - 导入并添加到 store
### 组件组织
- **原子设计**: Atoms → Molecules → Organisms
- **页面专属组件**: 放在 `views/{PageName}/components/`
- **可复用组件**: 放在 `src/components/`
---
## 配置
### 环境文件
- `.env.mock` - Mock 模式默认REACT_APP_ENABLE_MOCK=true
- `.env.development` - 开发模式
- `.env.production` - 生产环境
### MSW Mock
- 激活: `REACT_APP_ENABLE_MOCK=true`
- Handlers: `src/mocks/handlers/`
- 数据: `src/mocks/data/`
---
## TypeScript 接入
**状态**: 渐进式迁移中,支持 JS/TS 混合开发
**类型定义位置**: `src/types/`
- `api.ts` - ApiResponse、PaginatedResponse、ApiError
- `stock.ts` - StockInfo、StockQuote、KLineData
- `user.ts` - UserInfo、AuthInfo
**开发规范**:
- 新代码必须使用 TypeScript
- 避免使用 `any`
- 组件 Props 使用 `interface` 定义
**命令**:
### Backend Development
```bash
npm run type-check # 类型检查
npm run type-check:watch # 监听模式
python app_2.py # Start Flask server (main backend)
python simulation_background_processor.py # Background data processor
```
详细指南参考: [TYPESCRIPT_MIGRATION.md](./TYPESCRIPT_MIGRATION.md)
---
## 后端架构
### 核心文件
- `app.py` - Flask 主应用API + WebSocket
- `simulation_background_processor.py` - Celery 后台任务
- `concept_api.py` - 概念分析独立服务
### 数据库
| 数据库 | 用途 |
|--------|------|
| ClickHouse | 时序数据(股票日线、分钟线) |
| MySQL | 事务数据(用户、订单、持仓) |
| Redis | 缓存 + Celery 消息队列 |
### API 规范
```python
# RESTful 风格
GET /api/stocks # 获取列表
GET /api/stocks/:id # 获取单个
POST /api/stocks # 创建
PUT /api/stocks/:id # 更新
DELETE /api/stocks/:id # 删除
# 统一响应格式
{ "code": 200, "message": "success", "data": {...} }
### Python Dependencies
Install from requirements.txt:
```bash
pip install -r requirements.txt
```
---
## Architecture
## 代码规范
### Frontend Structure
- `src/layouts/` - Main layout components (Admin, Auth, Home)
- `src/views/` - Page components organized by feature (Dashboard, Company, Community, etc.)
- `src/components/` - Reusable UI components (Charts, Cards, Buttons, etc.)
- `src/theme/` - Chakra UI theme customization
- `src/routes.js` - Application routing configuration
- `src/contexts/` - React context providers
- `src/services/` - API service layer
### 命名约定
- 组件/目录: PascalCase (`EventCard`)
- 文件/函数: camelCase (`formatPrice.js`)
- 常量: SCREAMING_SNAKE_CASE (`API_BASE_URL`)
- 路由路径: kebab-case (`/trading-simulation`)
### Backend Structure
- `app_2.py` - Main Flask application with routes and business logic
- `simulation_background_processor.py` - Background data processing service
- `wechat_pay.py` / `wechat_pay_config.py` - Payment integration
- `tdays.csv` - Trading days data
### 最佳实践
- 组件不直接调用 axios通过 service 层
- 使用 `getApiBase()` 获取 API 基础 URL
- 页面超过 500 行考虑拆分
- 复用组件需在 2+ 个页面使用
### Key Integrations
- ClickHouse for high-performance analytics queries
- Celery + Redis for background task processing
- Flask-SocketIO for real-time data updates
- Tencent Cloud services (SMS, etc.)
- WeChat Pay integration
---
## Configuration
## 更新本文档
### Proxy Setup
The React dev server proxies API calls to `http://localhost:5001` (see package.json).
在以下情况下更新此文档:
- 添加新的架构模式
- 做出重要技术决策
- 进行重要代码重构
### Environment Files
- `.env` - Environment variables for both frontend and backend
### Build Process
The build process includes custom Gulp tasks that add Creative Tim license headers to JS, CSS, and HTML files.
### Styling Architecture
- Tailwind CSS for utility classes
- Custom Chakra UI theme with extended color palette
- Component-specific SCSS files in `src/assets/scss/`
## Testing
- React Testing Library setup for frontend components
- Test command: `npm test`
## Deployment
- Build: `npm run build`
- Deploy: `npm run deploy` (builds the project)

View File

@@ -48,19 +48,17 @@ npm start
### 3. 触发通知
**测试通知**
- 使用调试 API 发送测试通知
**Mock 模式**(默认)
- 等待 60 秒,会自动推送 1-2 条通知
- 或在控制台执行:
```javascript
// 方式1: 使用调试工具(推荐)
window.__DEBUG__.notification.forceNotification({
title: '测试通知',
body: '验证暗色模式下的通知样式'
});
// 方式2: 等待后端真实推送
// 确保已连接后端,等待真实事件推送
import { mockSocketService } from './services/mockSocketService.js';
mockSocketService.sendTestNotification();
```
**Real 模式**
- 创建测试事件(运行后端测试脚本)
### 4. 验证效果
检查以下项目:
@@ -141,46 +139,61 @@ npm start
### 手动触发各类型通知
> **注意**: Mock Socket 已移除,请使用调试工具或真实后端测试。
```javascript
// 使用调试工具测试不同类型的通知
// 确保已开启调试模式REACT_APP_ENABLE_DEBUG=true
// 引入服务
import { mockSocketService } from './services/mockSocketService.js';
import { NOTIFICATION_TYPES, PRIORITY_LEVELS } from './constants/notificationTypes.js';
// 测试公告通知
window.__DEBUG__.notification.forceNotification({
// 测试公告通知(蓝色)
mockSocketService.sendTestNotification({
type: NOTIFICATION_TYPES.ANNOUNCEMENT,
priority: PRIORITY_LEVELS.IMPORTANT,
title: '测试公告通知',
body: '这是暗色模式下的蓝色通知',
tag: 'test_announcement',
content: '这是暗色模式下的蓝色通知',
timestamp: Date.now(),
autoClose: 0,
});
// 测试股票上涨(红色)
window.__DEBUG__.notification.forceNotification({
title: '🔴 测试股票上涨',
body: '宁德时代 +5.2%',
tag: 'test_stock_up',
mockSocketService.sendTestNotification({
type: NOTIFICATION_TYPES.STOCK_ALERT,
priority: PRIORITY_LEVELS.URGENT,
title: '测试股票上涨',
content: '宁德时代 +5.2%',
extra: { priceChange: '+5.2%' },
timestamp: Date.now(),
autoClose: 0,
});
// 测试股票下跌(绿色)
window.__DEBUG__.notification.forceNotification({
title: '🟢 测试股票下跌',
body: '比亚迪 -3.8%',
tag: 'test_stock_down',
mockSocketService.sendTestNotification({
type: NOTIFICATION_TYPES.STOCK_ALERT,
priority: PRIORITY_LEVELS.IMPORTANT,
title: '测试股票下跌',
content: '比亚迪 -3.8%',
extra: { priceChange: '-3.8%' },
timestamp: Date.now(),
autoClose: 0,
});
// 测试事件动向(橙色)
window.__DEBUG__.notification.forceNotification({
title: '🟠 测试事件动向',
body: '央行宣布降准',
tag: 'test_event',
mockSocketService.sendTestNotification({
type: NOTIFICATION_TYPES.EVENT_ALERT,
priority: PRIORITY_LEVELS.IMPORTANT,
title: '测试事件动向',
content: '央行宣布降准',
timestamp: Date.now(),
autoClose: 0,
});
// 测试分析报告(紫色)
window.__DEBUG__.notification.forceNotification({
title: '🟣 测试分析报告',
body: '医药行业深度报告',
tag: 'test_report',
mockSocketService.sendTestNotification({
type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
priority: PRIORITY_LEVELS.NORMAL,
title: '测试分析报告',
content: '医药行业深度报告',
timestamp: Date.now(),
autoClose: 0,
});
```

626
ENHANCED_FEATURES_GUIDE.md Normal file
View File

@@ -0,0 +1,626 @@
# 通知系统增强功能 - 使用指南
## 📋 概述
本指南介绍通知系统的三大增强功能:
1. **智能桌面通知** - 自动请求权限,系统级通知
2. **性能监控** - 追踪推送效果,数据驱动优化
3. **历史记录** - 持久化存储,随时查询
---
## 🎯 功能 1智能桌面通知
### 功能说明
首次收到重要/紧急通知时,自动请求浏览器通知权限,确保用户不错过关键信息。
### 工作原理
```javascript
// 在 NotificationContext 中的逻辑
if (priority === URGENT || priority === IMPORTANT) {
if (browserPermission === 'default' && !hasRequestedPermission) {
// 首次遇到重要通知,自动请求权限
await requestBrowserPermission();
setHasRequestedPermission(true); // 避免重复请求
}
}
```
### 权限状态
- **granted**: 已授权,可以发送桌面通知
- **denied**: 已拒绝,无法发送桌面通知
- **default**: 未请求,首次重要通知时会自动请求
### 使用示例
**自动触发**(推荐)
```javascript
// 无需任何代码,系统自动处理
// 首次收到重要/紧急通知时会自动弹出权限请求
```
**手动请求**
```javascript
import { useNotification } from 'contexts/NotificationContext';
function SettingsPage() {
const { requestBrowserPermission, browserPermission } = useNotification();
return (
<div>
<p>当前状态: {browserPermission}</p>
<button onClick={requestBrowserPermission}>
开启桌面通知
</button>
</div>
);
}
```
### 通知分发策略
| 优先级 | 页面在前台 | 页面在后台 |
|-------|----------|----------|
| 紧急 | 桌面通知 + 网页通知 | 桌面通知 + 网页通知 |
| 重要 | 网页通知 | 桌面通知 |
| 普通 | 网页通知 | 网页通知 |
### 测试步骤
1. **清除已保存的权限状态**
```javascript
localStorage.removeItem('browser_notification_requested');
```
2. **刷新页面**
3. **触发一个重要/紧急通知**
- Mock 模式:等待自动推送
- Real 模式:创建测试事件
4. **观察权限请求弹窗**
- 浏览器会弹出通知权限请求
- 点击"允许"授权
5. **验证桌面通知**
- 切换到其他标签页
- 收到重要通知时应该看到桌面通知
---
## 📊 功能 2性能监控
### 功能说明
追踪通知推送的各项指标,包括:
- **到达率**: 发送 vs 接收
- **点击率**: 点击 vs 接收
- **响应时间**: 收到通知到点击的平均时间
- **类型分布**: 各类型通知的数量和效果
- **时段分布**: 每小时推送量
### API 参考
#### 获取汇总统计
```javascript
import { notificationMetricsService } from 'services/notificationMetricsService';
const summary = notificationMetricsService.getSummary();
console.log(summary);
/* 输出:
{
totalSent: 100,
totalReceived: 98,
totalClicked: 45,
totalDismissed: 53,
avgResponseTime: 5200, // 毫秒
clickRate: '45.92', // 百分比
deliveryRate: '98.00' // 百分比
}
*/
```
#### 获取按类型统计
```javascript
const byType = notificationMetricsService.getByType();
console.log(byType);
/* 输出:
{
announcement: { sent: 20, received: 20, clicked: 15, dismissed: 5, clickRate: '75.00' },
stock_alert: { sent: 30, received: 30, clicked: 20, dismissed: 10, clickRate: '66.67' },
event_alert: { sent: 40, received: 38, clicked: 10, dismissed: 28, clickRate: '26.32' },
analysis_report: { sent: 10, received: 10, clicked: 0, dismissed: 10, clickRate: '0.00' }
}
*/
```
#### 获取按优先级统计
```javascript
const byPriority = notificationMetricsService.getByPriority();
console.log(byPriority);
/* 输出:
{
urgent: { sent: 10, received: 10, clicked: 9, dismissed: 1, clickRate: '90.00' },
important: { sent: 40, received: 39, clicked: 25, dismissed: 14, clickRate: '64.10' },
normal: { sent: 50, received: 49, clicked: 11, dismissed: 38, clickRate: '22.45' }
}
*/
```
#### 获取每日数据
```javascript
const dailyData = notificationMetricsService.getDailyData(7); // 最近 7 天
console.log(dailyData);
/* 输出:
[
{ date: '2025-01-15', sent: 15, received: 14, clicked: 6, dismissed: 8, clickRate: '42.86' },
{ date: '2025-01-16', sent: 20, received: 20, clicked: 10, dismissed: 10, clickRate: '50.00' },
...
]
*/
```
#### 获取完整指标
```javascript
const allMetrics = notificationMetricsService.getAllMetrics();
console.log(allMetrics);
```
#### 导出数据
```javascript
// 导出为 JSON
const json = notificationMetricsService.exportToJSON();
console.log(json);
// 导出为 CSV
const csv = notificationMetricsService.exportToCSV();
console.log(csv);
```
#### 重置指标
```javascript
notificationMetricsService.reset();
```
### 在控制台查看实时指标
打开浏览器控制台,执行:
```javascript
// 引入服务
import { notificationMetricsService } from './services/notificationMetricsService.js';
// 查看汇总
console.table(notificationMetricsService.getSummary());
// 查看按类型分布
console.table(notificationMetricsService.getByType());
// 查看最近 7 天数据
console.table(notificationMetricsService.getDailyData(7));
```
### 监控埋点(自动)
监控服务已自动集成到 `NotificationContext`,无需手动调用:
- **trackReceived**: 收到通知时自动调用
- **trackClicked**: 点击通知时自动调用
- **trackDismissed**: 关闭通知时自动调用
### 可视化展示(可选)
你可以基于监控数据创建仪表板:
```javascript
import { notificationMetricsService } from 'services/notificationMetricsService';
import { PieChart, LineChart } from 'recharts';
function MetricsDashboard() {
const summary = notificationMetricsService.getSummary();
const dailyData = notificationMetricsService.getDailyData(7);
const byType = notificationMetricsService.getByType();
return (
<div>
{/* 汇总卡片 */}
<StatsCard title="总推送数" value={summary.totalSent} />
<StatsCard title="点击率" value={`${summary.clickRate}%`} />
<StatsCard title="平均响应时间" value={`${summary.avgResponseTime}ms`} />
{/* 类型分布饼图 */}
<PieChart data={Object.entries(byType).map(([type, data]) => ({
name: type,
value: data.received
}))} />
{/* 每日趋势折线图 */}
<LineChart data={dailyData} />
</div>
);
}
```
---
## 📜 功能 3历史记录
### 功能说明
持久化存储所有接收到的通知,支持:
- 查询和筛选
- 搜索关键词
- 标记已读/已点击
- 批量删除
- 导出JSON/CSV
### API 参考
#### 获取历史记录(支持筛选和分页)
```javascript
import { notificationHistoryService } from 'services/notificationHistoryService';
const result = notificationHistoryService.getHistory({
type: 'event_alert', // 可选:筛选类型
priority: 'urgent', // 可选:筛选优先级
readStatus: 'unread', // 可选:'read' | 'unread' | 'all'
startDate: Date.now() - 7 * 24 * 60 * 60 * 1000, // 可选:开始日期
endDate: Date.now(), // 可选:结束日期
page: 1, // 页码
pageSize: 20, // 每页数量
});
console.log(result);
/* 输出:
{
records: [...], // 当前页的记录
total: 150, // 总记录数
page: 1, // 当前页
pageSize: 20, // 每页数量
totalPages: 8 // 总页数
}
*/
```
#### 搜索历史记录
```javascript
const results = notificationHistoryService.searchHistory('降准');
console.log(results); // 返回标题/内容中包含"降准"的所有记录
```
#### 标记已读/已点击
```javascript
// 标记已读
notificationHistoryService.markAsRead('notification_id');
// 标记已点击
notificationHistoryService.markAsClicked('notification_id');
```
#### 删除记录
```javascript
// 删除单条
notificationHistoryService.deleteRecord('notification_id');
// 批量删除
notificationHistoryService.deleteRecords(['id1', 'id2', 'id3']);
// 清空所有
notificationHistoryService.clearHistory();
```
#### 获取统计数据
```javascript
const stats = notificationHistoryService.getStats();
console.log(stats);
/* 输出:
{
total: 500, // 总记录数
read: 320, // 已读数
unread: 180, // 未读数
clicked: 150, // 已点击数
clickRate: '30.00', // 点击率
byType: { // 按类型统计
announcement: 100,
stock_alert: 150,
event_alert: 200,
analysis_report: 50
},
byPriority: { // 按优先级统计
urgent: 50,
important: 200,
normal: 250
}
}
*/
```
#### 导出历史记录
```javascript
// 导出为 JSON 字符串
const json = notificationHistoryService.exportToJSON({
type: 'event_alert' // 可选:只导出特定类型
});
// 导出为 CSV 字符串
const csv = notificationHistoryService.exportToCSV();
// 直接下载 JSON 文件
notificationHistoryService.downloadJSON();
// 直接下载 CSV 文件
notificationHistoryService.downloadCSV();
```
### 在控制台使用
打开浏览器控制台,执行:
```javascript
// 引入服务
import { notificationHistoryService } from './services/notificationHistoryService.js';
// 查看所有历史
console.table(notificationHistoryService.getHistory().records);
// 搜索
const results = notificationHistoryService.searchHistory('央行');
console.table(results);
// 查看统计
console.table(notificationHistoryService.getStats());
// 导出并下载
notificationHistoryService.downloadJSON();
```
### 数据结构
每条历史记录包含:
```javascript
{
id: 'notif_123', // 通知 ID
notification: { // 完整通知对象
type: 'event_alert',
priority: 'urgent',
title: '...',
content: '...',
...
},
receivedAt: 1737459600000, // 接收时间戳
readAt: 1737459650000, // 已读时间戳null 表示未读)
clickedAt: null, // 已点击时间戳null 表示未点击)
}
```
### 存储限制
- **最大数量**: 500 条(超过后自动删除最旧的)
- **存储位置**: localStorage
- **容量估算**: 约 2-5MB取决于通知内容长度
---
## 🔧 技术细节
### 文件结构
```
src/
├── services/
│ ├── browserNotificationService.js [已存在] 浏览器通知服务
│ ├── notificationMetricsService.js [新建] 性能监控服务
│ └── notificationHistoryService.js [新建] 历史记录服务
├── contexts/
│ └── NotificationContext.js [修改] 集成所有功能
└── components/
└── NotificationContainer/
└── index.js [修改] 添加点击追踪
```
### 修改清单
| 文件 | 修改内容 | 状态 |
|------|---------|------|
| `NotificationContext.js` | 添加智能权限请求、监控埋点、历史保存 | ✅ 已完成 |
| `NotificationContainer/index.js` | 添加点击追踪 | ✅ 已完成 |
| `notificationMetricsService.js` | 性能监控服务 | ✅ 已创建 |
| `notificationHistoryService.js` | 历史记录服务 | ✅ 已创建 |
### 数据流
```
用户收到通知
NotificationContext.addWebNotification()
├─ notificationMetricsService.trackReceived() [监控埋点]
├─ notificationHistoryService.saveNotification() [历史保存]
├─ 首次重要通知 → requestBrowserPermission() [智能权限]
└─ 显示网页通知或桌面通知
用户点击通知
NotificationContainer.handleClick()
├─ notificationMetricsService.trackClicked() [监控埋点]
├─ notificationHistoryService.markAsClicked() [历史标记]
└─ 跳转到目标页面
用户关闭通知
NotificationContext.removeNotification()
└─ notificationMetricsService.trackDismissed() [监控埋点]
```
---
## 🧪 测试步骤
### 1. 测试智能桌面通知
```bash
# 1. 清除已保存的权限状态
localStorage.removeItem('browser_notification_requested');
# 2. 刷新页面
# 3. 等待或触发一个重要/紧急通知
# 4. 观察浏览器弹出权限请求
# 5. 授权后验证桌面通知功能
```
### 2. 测试性能监控
```javascript
// 在控制台执行
import { notificationMetricsService } from './services/notificationMetricsService.js';
// 查看实时统计
console.table(notificationMetricsService.getSummary());
// 模拟推送几条通知,再次查看
console.table(notificationMetricsService.getAllMetrics());
// 导出数据
console.log(notificationMetricsService.exportToJSON());
```
### 3. 测试历史记录
```javascript
// 在控制台执行
import { notificationHistoryService } from './services/notificationHistoryService.js';
// 查看历史
console.table(notificationHistoryService.getHistory().records);
// 搜索
console.table(notificationHistoryService.searchHistory('降准'));
// 查看统计
console.table(notificationHistoryService.getStats());
// 导出
notificationHistoryService.downloadJSON();
```
---
## 📈 数据导出示例
### 导出性能监控数据
```javascript
import { notificationMetricsService } from 'services/notificationMetricsService';
// 导出 JSON
const json = notificationMetricsService.exportToJSON();
// 复制到剪贴板或保存
// 导出 CSV
const csv = notificationMetricsService.exportToCSV();
// 可以在 Excel 中打开
```
### 导出历史记录
```javascript
import { notificationHistoryService } from 'services/notificationHistoryService';
// 导出最近 7 天的事件动向通知
const json = notificationHistoryService.exportToJSON({
type: 'event_alert',
startDate: Date.now() - 7 * 24 * 60 * 60 * 1000
});
// 直接下载为文件
notificationHistoryService.downloadJSON({
type: 'event_alert'
});
```
---
## ⚠️ 注意事项
### 1. localStorage 容量限制
- 大多数浏览器限制为 5-10MB
- 建议定期清理历史记录和监控数据
- 使用导出功能备份数据
### 2. 浏览器兼容性
- **桌面通知**: 需要 HTTPS 或 localhost
- **localStorage**: 所有现代浏览器支持
- **权限请求**: 需要用户交互(不能自动授权)
### 3. 隐私和数据安全
- 所有数据存储在本地localStorage
- 不会上传到服务器
- 用户可以随时清空数据
### 4. 性能影响
- 监控埋点非常轻量,几乎无性能影响
- 历史记录保存异步进行,不阻塞 UI
- 数据查询在客户端完成,不增加服务器负担
---
## 🎉 总结
### 已实现的功能
**智能桌面通知**
- 首次重要通知时自动请求权限
- 智能分发策略(前台/后台)
- localStorage 持久化权限状态
**性能监控**
- 到达率、点击率、响应时间追踪
- 按类型、优先级、时段统计
- 数据导出JSON/CSV
**历史记录**
- 持久化存储(最多 500 条)
- 筛选、搜索、分页
- 已读/已点击标记
- 数据导出JSON/CSV
### 未实现的功能(备份,待上线)
⏸️ 历史记录页面 UI代码已备份随时可上线
⏸️ 监控仪表板 UI可选暂未实现
### 下一步建议
1. **用户设置页面**: 允许用户自定义通知偏好
2. **声音提示**: 为紧急通知添加音效
3. **数据同步**: 将历史和监控数据同步到服务器
4. **高级筛选**: 添加更多筛选维度(如关键词、股票代码等)
---
**文档版本**: v1.0
**最后更新**: 2025-01-21
**维护者**: Claude Code

13
ISSUE_TEMPLATE.md Normal file
View File

@@ -0,0 +1,13 @@
<!--
IMPORTANT: Please use the following link to create a new issue:
https://www.creative-tim.com/new-issue/argon-dashboard-chakra-pro
**If your issue was not created using the app above, it will be closed immediately.**
-->
<!--
Love Creative Tim? Do you need Angular, React, Vuejs or HTML? You can visit:
👉 https://www.creative-tim.com/bundles
👉 https://www.creative-tim.com
-->

View File

@@ -0,0 +1,370 @@
# 消息推送系统整合 - 测试指南
## 📋 整合完成清单
**统一事件名称**
- Mock 和真实 Socket.IO 都使用 `new_event` 事件名
- 移除了 `trade_notification` 事件名
**数据适配器**
- 创建了 `adaptEventToNotification` 函数
- 自动识别后端事件格式并转换为前端通知格式
- 重要性映射S → urgent, A → important, B/C → normal
**NotificationContext 升级**
- 监听 `new_event` 事件
- 自动使用适配器转换事件数据
- 支持 Mock 和 Real 模式无缝切换
**EventList 实时推送**
- 集成 `useEventNotifications` Hook
- 实时更新事件列表
- Toast 通知提示
- WebSocket 连接状态指示器
---
## 🧪 测试步骤
### 1. 测试 Mock 模式(开发环境)
#### 1.1 配置环境变量
确保 `.env` 文件包含以下配置:
```bash
REACT_APP_USE_MOCK_SOCKET=true
# 或者
REACT_APP_ENABLE_MOCK=true
```
#### 1.2 启动应用
```bash
npm start
```
#### 1.3 验证功能
**a) 右下角通知卡片**
- 启动后等待 3 秒,应该看到 "连接成功" 系统通知
- 每隔 60 秒会自动推送 1-2 条模拟消息
- 通知类型包括:
- 📢 公告通知(蓝色)
- 📈 股票动向(红/绿色,根据涨跌)
- 📰 事件动向(橙色)
- 📊 分析报告(紫色)
**b) 事件列表页面**
- 访问事件列表页面Community/Events
- 顶部应显示 "🟢 实时推送已开启"
- 收到新事件时:
- 右上角显示 Toast 通知
- 事件自动添加到列表顶部
- 无重复添加
**c) 控制台日志**
打开浏览器控制台,应该看到:
```
[Socket Service] Using MOCK Socket Service
NotificationContext: Socket connected
EventList: 收到新事件推送
```
---
### 2. 测试 Real 模式(生产环境)
#### 2.1 配置环境变量
修改 `.env` 文件:
```bash
REACT_APP_USE_MOCK_SOCKET=false
# 或删除该配置项
```
#### 2.2 启动后端 Flask 服务
```bash
python app_2.py
```
确保后端已启动 Socket.IO 服务并监听事件推送。
#### 2.3 启动前端应用
```bash
npm start
```
#### 2.4 创建测试事件(后端)
使用后端提供的测试脚本:
```bash
python test_create_event.py
```
#### 2.5 验证功能
**a) WebSocket 连接**
- 检查控制台:`[Socket Service] Using REAL Socket Service`
- 事件列表顶部显示 "🟢 实时推送已开启"
**b) 事件推送流程**
1. 运行 `test_create_event.py` 创建新事件
2. 后端轮询检测到新事件(最多等待 30 秒)
3. 后端通过 Socket.IO 推送 `new_event`
4. 前端接收事件并转换格式
5. 同时显示:
- 右下角通知卡片
- 事件列表 Toast 提示
- 事件添加到列表顶部
**c) 数据格式验证**
在控制台查看事件对象,应包含:
```javascript
{
id: 123,
type: "event_alert", // 适配器转换后
priority: "urgent", // importance: S → urgent
title: "事件标题",
content: "事件描述",
clickable: true,
link: "/event-detail/123",
extra: {
eventType: "tech",
importance: "S",
// ... 更多后端字段
}
}
```
---
## 🔍 验证清单
### 功能验证
- [ ] Mock 模式下收到模拟通知
- [ ] Real 模式下收到真实后端推送
- [ ] 通知卡片正确显示(类型、颜色、内容)
- [ ] 事件列表实时更新
- [ ] Toast 通知正常弹出
- [ ] 连接状态指示器正确显示
- [ ] 点击通知可跳转到详情页
- [ ] 无重复事件添加
### 数据验证
- [ ] 后端事件格式正确转换
- [ ] 重要性映射正确S/A/B/C → urgent/important/normal
- [ ] 时间戳正确显示
- [ ] 链接路径正确生成
- [ ] 所有字段完整保留在 extra 中
### 性能验证
- [ ] 事件列表最多保留 100 条
- [ ] 通知自动关闭(紧急=不关闭,重要=30s普通=15s
- [ ] WebSocket 自动重连
- [ ] 无内存泄漏
---
## 🐛 常见问题排查
### Q1: Mock 模式下没有收到通知?
**A:** 检查:
1. 环境变量 `REACT_APP_USE_MOCK_SOCKET=true` 是否设置
2. 控制台是否显示 "Using MOCK Socket Service"
3. 是否等待了 3 秒(首次通知延迟)
### Q2: Real 模式下无法连接?
**A:** 检查:
1. Flask 后端是否启动:`python app_2.py`
2. API_BASE_URL 是否正确配置
3. CORS 设置是否包含前端域名
4. 控制台是否有连接错误
### Q3: 收到重复通知?
**A:** 检查:
1. 是否多次渲染了 EventList 组件
2. 是否在多个地方调用了 `useEventNotifications`
3. 控制台日志中是否有 "事件已存在,跳过添加"
### Q4: 通知卡片样式异常?
**A:** 检查:
1. 事件的 `type` 字段是否正确
2. 是否缺少必要的字段title, content
3. `NOTIFICATION_TYPE_CONFIGS` 是否定义了该类型
### Q5: 事件列表不更新?
**A:** 检查:
1. WebSocket 连接状态(顶部 Badge
2. `onNewEvent` 回调是否触发(控制台日志)
3. `setLocalEvents` 是否正确执行
---
## 📊 测试数据示例
### Mock 模拟数据类型
**公告通知**
```javascript
{
type: "announcement",
priority: "urgent",
title: "贵州茅台发布2024年度财报公告",
content: "2024年度营收同比增长15.2%..."
}
```
**股票动向**
```javascript
{
type: "stock_alert",
priority: "urgent",
title: "您关注的股票触发预警",
extra: {
stockCode: "300750",
priceChange: "+5.2%"
}
}
```
**事件动向**
```javascript
{
type: "event_alert",
priority: "important",
title: "央行宣布降准0.5个百分点",
extra: {
eventId: "evt001",
sectors: ["银行", "地产", "基建"]
}
}
```
**分析报告**
```javascript
{
type: "analysis_report",
priority: "important",
title: "医药行业深度报告:创新药迎来政策拐点",
author: {
name: "李明",
organization: "中信证券"
}
}
```
### 真实后端事件格式
```javascript
{
id: 123,
title: "新能源汽车补贴政策延期",
description: "财政部宣布新能源汽车购置补贴政策延长至2024年底",
event_type: "policy",
importance: "S",
status: "active",
created_at: "2025-01-21T14:30:00",
hot_score: 95.5,
view_count: 1234,
related_avg_chg: 5.2,
related_max_chg: 15.8,
keywords: ["新能源", "补贴", "政策"]
}
```
---
## 🎯 下一步建议
### 1. 用户设置
允许用户控制通知偏好:
```jsx
<Switch
isChecked={enableNotifications}
onChange={handleToggle}
>
启用实时通知
</Switch>
```
### 2. 通知过滤
按重要性、类型过滤通知:
```javascript
useEventNotifications({
eventType: 'tech', // 只订阅科技类
importance: 'S', // 只订阅 S 级
enabled: true
})
```
### 3. 声音提示
添加音效提醒:
```javascript
onNewEvent: (event) => {
if (event.priority === 'urgent') {
new Audio('/alert.mp3').play();
}
}
```
### 4. 桌面通知
利用浏览器通知 API
```javascript
if (Notification.permission === 'granted') {
new Notification(event.title, {
body: event.content,
icon: '/logo.png'
});
}
```
---
## 📝 技术说明
### 架构优势
1. **统一接口**Mock 和 Real 完全相同的 API
2. **自动适配**:智能识别数据格式并转换
3. **解耦设计**:通知系统和事件列表独立工作
4. **向后兼容**:不影响现有功能
### 关键文件
- `src/services/mockSocketService.js` - Mock Socket 服务
- `src/services/socketService.js` - 真实 Socket.IO 服务
- `src/services/socket/index.js` - 统一导出
- `src/contexts/NotificationContext.js` - 通知上下文(含适配器)
- `src/hooks/useEventNotifications.js` - React Hook
- `src/views/Community/components/EventList.js` - 事件列表集成
### 数据流
```
后端创建事件
后端轮询检测30秒
Socket.IO 推送 new_event
前端 socketService 接收
NotificationContext 监听并适配
同时触发:
├─ NotificationContainer右下角卡片
└─ EventList onNewEventToast + 列表更新)
```
---
## ✅ 整合完成
所有代码和功能已经就绪!你现在可以:
1. ✅ 在 Mock 模式下测试实时推送
2. ✅ 在 Real 模式下连接后端
3. ✅ 查看右下角通知卡片
4. ✅ 体验事件列表实时更新
5. ✅ 随时切换 Mock/Real 模式
**祝测试顺利!🎉**

View File

@@ -1 +0,0 @@
17Fo4JhapMw6vtNa

View File

@@ -0,0 +1,280 @@
# 消息推送系统优化总结
## 优化目标
1. 简化通知信息密度,通过视觉层次(边框+背景色)表达优先级
2. 增强紧急通知的视觉冲击力(红色脉冲边框动画)
3. 采用智能显示策略,降低普通通知的视觉干扰
## 实施内容
### 1. 优先级配置更新 (src/constants/notificationTypes.js)
#### 新增配置项
- `borderWidth`: 边框宽度
- 紧急 (urgent): 6px
- 重要 (important): 4px
- 普通 (normal): 2px
- `bgOpacity`: 背景色透明度(亮色模式)
- 紧急: 0.25 (深色背景)
- 重要: 0.15 (中色背景)
- 普通: 0.08 (浅色背景)
- `darkBgOpacity`: 背景色透明度(暗色模式)
- 紧急: 0.30
- 重要: 0.20
- 普通: 0.12
#### 新增辅助函数
- `getPriorityBgOpacity(priority, isDark)`: 获取优先级对应的背景色透明度
- `getPriorityBorderWidth(priority)`: 获取优先级对应的边框宽度
### 2. 紧急通知脉冲动画 (src/components/NotificationContainer/index.js)
#### 动画效果
- 使用 `@emotion/react``keyframes` 创建脉冲动画
- 仅紧急通知 (urgent) 应用动画效果
- 动画特性:
- 边框颜色脉冲效果
- 阴影扩散效果0 → 12px
- 持续时间2秒
- 缓动函数ease-in-out
- 无限循环
```javascript
const pulseAnimation = keyframes`
0%, 100% {
border-left-color: currentColor;
box-shadow: 0 0 0 0 currentColor;
}
50% {
border-left-color: currentColor;
box-shadow: -4px 0 12px 0 currentColor;
}
`;
```
### 3. 背景色优先级优化
#### 亮色模式
- **紧急通知**`${colorScheme}.200` - 深色背景 + 脉冲动画
- **重要通知**`${colorScheme}.100` - 中色背景
- **普通通知**`white` - 极淡背景(降低视觉干扰)
#### 暗色模式
- **紧急通知**`${colorScheme}.800` 或 typeConfig.darkBg
- **重要通知**`${colorScheme}.800` 或 typeConfig.darkBg
- **普通通知**`gray.800` - 暗灰背景(降低视觉干扰)
### 4. 可点击性视觉提示
#### 问题
- 用户需要 hover 才能知道通知是否可点击
- cursor: pointer 不够直观
#### 解决方案
- **可点击的通知**
- 添加完整边框(四周 1px solid
- 保持左侧优先级边框宽度
- 使用更明显的阴影md 级别)
- 产生微妙的悬浮感
- **不可点击的通知**
- 仅左侧边框
- 使用较淡的阴影sm 级别)
```javascript
// 可点击的通知添加完整边框
{...(isActuallyClickable && {
border: '1px solid',
borderLeftWidth: priorityBorderWidth, // 保持优先级
})}
// 可点击的通知使用更明显的阴影
boxShadow={isActuallyClickable
? (isNewest ? '2xl' : 'md')
: (isNewest ? 'xl' : 'sm')}
```
### 5. 通知组件简化 (src/components/NotificationContainer/index.js)
#### 显示元素分级
**LV1 - 必需元素(始终显示)**
- ✅ 标题 (title)
- ✅ 内容 (content, 最多3行)
- ✅ 时间 (publishTime/pushTime)
- ✅ 查看详情 (仅当 clickable=true 时)
- ✅ 关闭按钮
**LV2 - 可选元素(数据存在时显示)**
- ✅ 图标:仅在紧急/重要通知时显示
- ❌ 优先级标签:已移除,改用边框+背景色表示
- ✅ 状态提示:仅当 `extra?.statusHint` 存在时显示
**LV3 - 可选元素(数据存在时显示)**
- ✅ AI 标识:仅当 `isAIGenerated = true` 时显示
- ✅ 预测标识:仅当 `isPrediction = true` 时显示
**其他**
- ✅ 作者信息:移除屏幕尺寸限制,仅当 `author` 存在时显示
#### 优先级视觉样式
- ✅ 边框宽度:根据优先级动态调整 (2px/4px/6px)
- ✅ 背景色深度:根据优先级使用不同深度的颜色
- 亮色模式: .50 (普通) / .100 (重要) / .200 (紧急)
- 暗色模式: 使用 typeConfig 的 darkBg 配置
#### 布局优化
- ✅ 内容和元数据区域的左侧填充根据图标显示状态自适应
- ✅ 无图标时不添加额外的左侧间距
## 预期效果
### 视觉改进
- **清晰度提升**:移除冗余的优先级标签,视觉更整洁
- **优先级强化**
- 紧急通知6px 粗边框 + 深色背景 + **红色脉冲动画** → 视觉冲击力极强
- 重要通知4px 中等边框 + 中色背景 + 图标 → 醒目但不打扰
- 普通通知2px 细边框 + 白色/极淡背景 → 低视觉干扰
- **可点击性一目了然**
- 可点击:完整边框 + 明显阴影 → 卡片悬浮感
- 不可点击:仅左侧边框 + 淡阴影 → 平面感
- **信息密度降低**:减少不必要的视觉元素,关键信息更突出
### 用户体验
- **紧急通知引起注意**:脉冲动画确保用户不会错过紧急信息
- **快速识别优先级**
- 动画 = 紧急(需要立即关注)
- 图标 + 粗边框 = 重要(需要关注)
- 细边框 + 淡背景 = 普通(可稍后查看)
- **可点击性无需 hover**
- 完整边框 + 悬浮感 = 可以点击查看详情
- 仅左侧边框 = 信息已完整,无需跳转
- **智能显示**:可选信息只在数据存在时显示,避免空白占位
- **响应式优化**:所有设备上保持一致的显示逻辑
### 向后兼容
- ✅ 完全兼容现有通知数据结构
- ✅ 可选字段不存在时自动隐藏
- ✅ 不影响现有功能(点击、关闭、自动消失等)
## 测试建议
### 1. 功能测试
```bash
# 启动开发服务器
npm start
# 观察不同优先级通知的显示效果
# - 紧急通知:粗边框 (6px) + 深色背景 + 红色脉冲动画 + 图标 + 不自动关闭
# - 重要通知:中等边框 (4px) + 中色背景 + 图标 + 30秒后关闭
# - 普通通知:细边框 (2px) + 白色背景 + 无图标 + 15秒后关闭
```
### 1.1 动画测试
- [ ] 紧急通知的脉冲动画流畅无卡顿
- [ ] 动画周期为 2 秒
- [ ] 动画在紧急通知显示期间持续循环
- [ ] 阴影扩散效果清晰可见
### 2. 边界测试
- [ ] 仅必需字段的通知(无作者、无 AI 标识、无预测标识)
- [ ] 包含所有可选字段的通知
- [ ] 不同类型的通知(公告、股票、事件、分析报告)
- [ ] 不同优先级的通知(紧急、重要、普通)
### 3. 响应式测试
- [ ] 移动设备 (< 480px)
- [ ] 平板设备 (480px - 768px)
- [ ] 桌面设备 (> 768px)
### 4. 暗色模式测试
- [ ] 切换到暗色模式,确认背景色对比度合适
## 技术细节
### 关键代码变更
#### 1. 脉冲动画实现
```javascript
// 导入 keyframes
import { keyframes } from '@emotion/react';
// 定义脉冲动画
const pulseAnimation = keyframes`
0%, 100% {
border-left-color: currentColor;
box-shadow: 0 0 0 0 currentColor;
}
50% {
border-left-color: currentColor;
box-shadow: -4px 0 12px 0 currentColor;
}
`;
// 应用到紧急通知
<Box
animation={priority === PRIORITY_LEVELS.URGENT
? `${pulseAnimation} 2s ease-in-out infinite`
: undefined}
...
/>
```
#### 2. 优先级标签自动隐藏
```javascript
// PRIORITY_CONFIGS 中所有 show 属性设置为 false
show: false, // 不再显示标签,改用边框+背景色表示
```
#### 3. 背景色优先级优化
```javascript
const getPriorityBgColor = () => {
const colorScheme = typeConfig.colorScheme;
if (!isDark) {
if (priority === PRIORITY_LEVELS.URGENT) {
return `${colorScheme}.200`; // 深色背景 + 脉冲动画
} else if (priority === PRIORITY_LEVELS.IMPORTANT) {
return `${colorScheme}.100`; // 中色背景
} else {
return 'white'; // 极淡背景(降低视觉干扰)
}
} else {
if (priority === PRIORITY_LEVELS.URGENT) {
return typeConfig.darkBg || `${colorScheme}.800`;
} else if (priority === PRIORITY_LEVELS.IMPORTANT) {
return typeConfig.darkBg || `${colorScheme}.800`;
} else {
return 'gray.800'; // 暗灰背景(降低视觉干扰)
}
}
};
```
#### 4. 图标条件显示
```javascript
const shouldShowIcon = priority === PRIORITY_LEVELS.URGENT ||
priority === PRIORITY_LEVELS.IMPORTANT;
{shouldShowIcon && (
<Icon as={typeConfig.icon} ... />
)}
};
```
## 后续改进建议
### 短期
- [ ] 添加通知优先级过渡动画(边框和背景色渐变)
- [ ] 提供配置选项让用户自定义显示元素
### 长期
- [ ] 支持通知分组(按类型或优先级)
- [ ] 添加通知搜索和筛选功能
- [ ] 通知历史记录可视化统计
## 构建状态
✅ 构建成功 (npm run build)
✅ 无语法错误
✅ 无 TypeScript 错误

1551
NOTIFICATION_SYSTEM.md Normal file

File diff suppressed because it is too large Load Diff

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# vf_react
前端

338
TEST_GUIDE.md Normal file
View File

@@ -0,0 +1,338 @@
# 崩溃修复测试指南
> 测试时间2025-10-14
> 测试范围SignInIllustration.js + SignUpIllustration.js
> 服务器地址http://localhost:3000
---
## 🎯 测试目标
验证以下修复是否有效:
- ✅ 响应对象崩溃6处
- ✅ 组件卸载后 setState6处
- ✅ 定时器内存泄漏2处
---
## 📋 测试清单
### ✅ 关键测试(必做)
#### 1. **网络异常测试** - 验证响应对象修复
**登录页面 - 发送验证码**
```
测试步骤:
1. 打开 http://localhost:3000/auth/sign-in
2. 切换到"验证码登录"模式
3. 输入手机号13800138000
4. 打开浏览器开发者工具 (F12) → Network 标签
5. 点击 Offline 模拟断网
6. 点击"发送验证码"按钮
预期结果:
✅ 显示错误提示:"发送验证码失败 - 网络请求失败,请检查网络连接"
✅ 页面不崩溃
✅ 无 JavaScript 错误
修复前:
❌ 页面白屏崩溃
❌ Console 报错Cannot read property 'json' of null
```
**登录页面 - 微信登录**
```
测试步骤:
1. 在登录页面,保持断网状态
2. 点击"扫码登录"按钮
预期结果:
✅ 显示错误提示:"获取微信授权失败 - 网络请求失败,请检查网络连接"
✅ 页面不崩溃
✅ 无 JavaScript 错误
```
**注册页面 - 发送验证码**
```
测试步骤:
1. 打开 http://localhost:3000/auth/sign-up
2. 切换到"验证码注册"模式
3. 输入手机号13800138000
4. 保持断网状态
5. 点击"发送验证码"按钮
预期结果:
✅ 显示错误提示:"发送失败 - 网络请求失败..."
✅ 页面不崩溃
```
---
#### 2. **组件卸载测试** - 验证内存泄漏修复
**倒计时中离开页面**
```
测试步骤:
1. 恢复网络连接
2. 在登录页面输入手机号并发送验证码
3. 等待倒计时开始60秒倒计时
4. 立即点击浏览器后退按钮或切换到其他页面
5. 打开 Console 查看是否有警告
预期结果:
✅ 无警告:"Can't perform a React state update on an unmounted component"
✅ 倒计时定时器正确清理
✅ 无内存泄漏
修复前:
❌ Console 警告Memory leak warning
❌ setState 在组件卸载后仍被调用
```
**请求进行中离开页面**
```
测试步骤:
1. 在注册页面填写完整信息
2. 点击"注册"按钮
3. 在请求响应前loading 状态)快速刷新页面或关闭标签页
4. 打开新标签页查看 Console
预期结果:
✅ 无崩溃
✅ 无警告信息
✅ 请求被正确取消或忽略
```
**注册成功跳转前离开**
```
测试步骤:
1. 完成注册提交
2. 在显示"注册成功"提示后
3. 立即关闭标签页不等待2秒自动跳转
预期结果:
✅ 无警告
✅ navigate 不会在组件卸载后执行
```
---
#### 3. **边界情况测试** - 验证数据完整性检查
**后端返回空响应**
```
测试步骤(需要模拟后端):
1. 使用 Chrome DevTools → Network → 右键请求 → Edit and Resend
2. 修改响应为空对象 {}
3. 观察页面反应
预期结果:
✅ 显示错误:"服务器响应为空"
✅ 不会尝试访问 undefined 属性
✅ 页面不崩溃
```
**后端返回 500 错误**
```
测试步骤:
1. 在登录页面点击"扫码登录"
2. 如果后端返回 500 错误
预期结果:
✅ 显示错误:"获取二维码失败HTTP 500"
✅ 页面不崩溃
```
---
### 🧪 进阶测试(推荐)
#### 4. **弱网环境测试**
**慢速网络模拟**
```
测试步骤:
1. Chrome DevTools → Network → Throttling → Slow 3G
2. 尝试发送验证码
3. 等待 10 秒(超时时间)
预期结果:
✅ 10秒后显示超时错误
✅ 不会无限等待
✅ 用户可以重试
```
**丢包模拟**
```
测试步骤:
1. 使用 Chrome DevTools 模拟丢包
2. 连续点击"发送验证码"多次
预期结果:
✅ 每次请求都有适当的错误提示
✅ 不会因为并发请求而崩溃
✅ 按钮在请求期间正确禁用
```
---
#### 5. **定时器清理测试**
**倒计时清理验证**
```
测试步骤:
1. 在登录页面发送验证码
2. 等待倒计时到 50 秒
3. 快速切换到注册页面
4. 再切换回登录页面
5. 观察倒计时是否重置
预期结果:
✅ 定时器在页面切换时正确清理
✅ 返回登录页面时倒计时重新开始(如果再次发送)
✅ 没有多个定时器同时运行
```
---
#### 6. **并发请求测试**
**快速连续点击**
```
测试步骤:
1. 在登录页面输入手机号
2. 快速连续点击"发送验证码"按钮 5 次
预期结果:
✅ 只发送一次请求(按钮在请求期间禁用)
✅ 不会因为并发而崩溃
✅ 正确显示 loading 状态
```
---
## 🔍 监控指标
### Console 检查清单
在测试过程中,打开 Console (F12) 监控以下内容:
```
✅ 无红色错误Error
✅ 无内存泄漏警告Memory leak warning
✅ 无 setState 警告Can't perform a React state update...
✅ 无 undefined 访问错误Cannot read property of undefined
```
### Network 检查清单
打开 Network 标签监控:
```
✅ 请求超时时间10秒
✅ 失败请求有正确的错误处理
✅ 没有重复的请求
✅ 请求被正确取消(如果页面卸载)
```
### Performance 检查清单
打开 Performance 标签(可选):
```
✅ 无内存泄漏Memory 不会持续增长)
✅ 定时器正确清理Timer count 正确)
✅ EventListener 正确清理
```
---
## 📊 测试记录表
请在测试时填写以下表格:
| 测试项 | 状态 | 问题描述 | 截图 |
|--------|------|---------|------|
| 登录页 - 断网发送验证码 | ⬜ 通过 / ⬜ 失败 | | |
| 登录页 - 断网微信登录 | ⬜ 通过 / ⬜ 失败 | | |
| 注册页 - 断网发送验证码 | ⬜ 通过 / ⬜ 失败 | | |
| 倒计时中离开页面 | ⬜ 通过 / ⬜ 失败 | | |
| 请求进行中离开页面 | ⬜ 通过 / ⬜ 失败 | | |
| 注册成功跳转前离开 | ⬜ 通过 / ⬜ 失败 | | |
| 后端返回空响应 | ⬜ 通过 / ⬜ 失败 | | |
| 慢速网络超时 | ⬜ 通过 / ⬜ 失败 | | |
| 定时器清理 | ⬜ 通过 / ⬜ 失败 | | |
| 并发请求 | ⬜ 通过 / ⬜ 失败 | | |
---
## 🐛 如何报告问题
如果发现问题,请提供:
1. **测试场景**:具体的测试步骤
2. **预期结果**:应该发生什么
3. **实际结果**:实际发生了什么
4. **Console 错误**:完整的错误信息
5. **截图/录屏**:问题的视觉证明
6. **环境信息**
- 浏览器版本
- 操作系统
- 网络状态
---
## ✅ 测试完成检查
测试完成后,确认以下内容:
```
□ 所有关键测试通过
□ Console 无错误
□ Network 请求正常
□ 无内存泄漏警告
□ 用户体验流畅
```
---
## 🎯 快速测试命令
```bash
# 1. 确认服务器运行
curl http://localhost:3000
# 2. 打开浏览器测试
open http://localhost:3000/auth/sign-in
# 3. 查看编译日志
tail -f /tmp/react-build.log
```
---
## 📱 测试页面链接
- **登录页面**: http://localhost:3000/auth/sign-in
- **注册页面**: http://localhost:3000/auth/sign-up
- **首页**: http://localhost:3000/home
---
## 🔧 开发者工具快捷键
```
F12 - 打开开发者工具
Ctrl/Cmd+R - 刷新页面
Ctrl/Cmd+Shift+R - 强制刷新(清除缓存)
Ctrl/Cmd+Shift+C - 元素选择器
```
---
**测试时间**2025-10-14
**预计测试时长**15-30 分钟
**建议测试人员**:开发者 + QA
祝测试顺利!如发现问题请及时反馈。

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1 +0,0 @@
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkBfIjOiu8vfmOSq1BXcjDsAqQ+xtwGO0aCn0VrhVzc0T70nDchaW/TJ4nW8qlRMBlgfTi00jDGFiW4ND9JHc4aES8yiDSNeaBW4gLQhC1isvpOndyu4YgDc+2lMfghv9+6D8uFl9VS8Vk6o7D+5SiE7F8aBn49rrLyvsmdN5i6eLxIuY9NM58/o0xVG5f3ktGqfFKzhtclPbt8ej39EgziCiNFbIk2FnZp9dB56vtmCKht4t3STDpM0RfC8YlQ2WpGu9okLJYSy1rfynhh0hlOy/9y4cYl50wthoQVxH/Hm9abiTMo2u6xWESreavtdDZ8ByKVltnUrRvzDQ4tVkYwIDAQAB

View File

@@ -1 +0,0 @@
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCQF8iM6K7y9+Y5KrUFdyMOwCpD7G3AY7RoKfRWuFXNzRPvScNyFpb9MnidbyqVEwGWB9OLTSMMYWJbg0P0kdzhoRLzKINI15oFbiAtCELWKy+k6d3K7hiANz7aUx+CG/37oPy4WX1VLxWTqjsP7lKITsXxoGfj2usvK+yZ03mLp4vEi5j00znz+jTFUbl/eS0ap8UrOG1yU9u3x6Pf0SDOIKI0VsiTYWdmn10Hnq+2YIqG3i3dJMOkzRF8LxiVDZaka72iQslhLLWt/KeGHSGU7L/3LhxiXnTC2GhBXEf8eb1puJMyja7rFYRKt5q+10NnwHIpWW2dStG/MNDi1WRjAgMBAAECggEAHQ8+2fQfPE70djj/su94+YOVwocPB0rUWmGDrm2UmGGwkISezwZxQvUH0DBYNSJVIo3HgwN2ewu0y2HotY0pL7PNX46fE3Sv0kKIaKyO1iR1glvL6B4mgM0jduJmq1W73iB0dzVNCn3paxNcv/S/XlAMqZNBAHnpDmVcXRWCIMDG1yRN3l5NbfgPoQZI4MfAdthjIcIQmEVjNWy69swsdNndj8yrIu9E9RlWly/xqB/BJ6O53i0n+jyviy2grgHxo9VgWKv89qZiczioLT7aAJITpcMofUkGImZy+DHlf+6GU762UkwbLykYN2RRkw5TPvWt4ZUXeON4flr3ig02yQKBgQDMh82rc3TklOZSCw3lwYE58eeYxe9PXpZzcOyrSZZhCs0y528dmwYbm0mPlpVti1MGKnn2eGVKeGQ8a5CbJCi2T+VwIILWM9U2+h82nJW5KD6CYaE86KK/PlzhTGwmpecv8hafkpn51zvyjI3UkKbv/Ep+Mfy89PLumvh5Ze+EfwKBgQC0Wn1o0JCjgCt3K39tahavBuCPDvk7oLA6JLp2W9437YFeuh9L/gYdAtJU79WpmOfgr228cOlExdGGpHqLPSDFpN3ssx1pkUJ6RlTN8YInc+dbAkC6gsm5XR0Jvj4YqghyWHKhxXErnFGDof2EQq7ghHK9pokpBFPowwlzkpOeHQKBgFbqVwJG/COvCvlObUd3pbzECdEoO/wUjAbetBROHzN57Z12MAf6uuu8X9Q+/50fmdaC8nVE0HaHFsF+TGNBSHPBHBU8G51/RVopjF4eyJl4eqfZaTWC/rYagEnVuhfqZIZBcE+7cudzCayXAiaUmfxd0CI0h9yckyfGf1THdrNtAoGASMtNWwTznEqbQJpZ8HuldDe+Y3+TsTGGb7FrYWJrKv+9+9H719xL82G0K3wyLSX+UX39ONYKESwXCdVRcOnXVG7a9DLHaFitEFVa3VThR7NMajtajO1FJoAivFABGEto5V41xn2+0+9gJ1U20i9oDk7nUQzqx5drlsNCCVfcJTECgYEAlEYIQ3AiVqx0RqZjfbg+nyZQmoUfHPASY2Hu9pJWvLwXsQWSPh1gf03galXzZ46wrCaeLygGaoHXW7W8WwsYBR1tG7voQeSe8mmlWgiscmvmvqSowJ10BnsdWwpOTZ8O6rKy8HdyI8gFJyfJgfz+6KekcdGEnQbmwCvB8j+Y7IQ=

View File

@@ -1 +0,0 @@
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjyULL5tuD2cjfNvgn9fvVfn+WbLhoP3wk3bcYb9AabsZc1GCBlnG579Socc3U9dG5IR7C3KHD4kYHnH4tbK2pEWmmfjbVKXWguLqL5K3Dggnl5KVOlVVjrcsmLPqTj7JdeO0AQolmgdr/o6TlhQdsqINQAK5F5wWwIwwcSoJsWZ6zlPPX/Q/eMIN4zGgK2taMhx656zSxsYE5OKRYkTJhVrMktxQdwbUzoFSID++dTpjxF4w5k5qeVW/1WZaaswMFWh2IcJ5oyc+VjTRqZvtQt4gB0Fa0EblfmSJaozhoWHwzwF+1qtv7gp/TcMYj/Ah2+tY0UPxucEcTqY/i7PPfwIDAQAB

View File

@@ -1,118 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
支付宝支付配置文件
电脑网站支付 (alipay.trade.page.pay)
"""
import os
# 获取当前文件所在目录
_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# 支付宝支付配置
ALIPAY_CONFIG = {
# 应用ID - 从支付宝开放平台获取
'app_id': '2021005183650009',
# 支付宝网关 - 正式环境
'gateway_url': 'https://openapi.alipay.com/gateway.do',
# 沙箱环境网关(测试用)
# 'gateway_url': 'https://openapi-sandbox.dl.alipaydev.com/gateway.do',
# 签名方式 - 必须使用RSA2
'sign_type': 'RSA2',
# 编码格式
'charset': 'utf-8',
# 返回格式
'format': 'json',
# 密钥文件路径
'app_private_key_path': os.path.join(_BASE_DIR, 'alipay', '应用私钥.txt'),
'alipay_public_key_path': os.path.join(_BASE_DIR, 'alipay', '支付宝公钥.txt'),
# 回调地址 - 替换为你的实际域名
'notify_url': 'https://api.valuefrontier.cn/api/payment/alipay/callback', # 异步通知地址(后端)
'return_url': 'https://valuefrontier.cn/pricing?payment_return=alipay', # 同步跳转地址(前端页面)
# 产品码 - 电脑网站支付固定为此值
'product_code': 'FAST_INSTANT_TRADE_PAY',
}
def load_key_from_file(file_path):
"""从文件读取密钥内容"""
import sys
try:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read().strip()
except FileNotFoundError:
print(f"[AlipayConfig] Key file not found: {file_path}", file=sys.stderr)
return None
except Exception as e:
print(f"[AlipayConfig] Read key file failed: {e}", file=sys.stderr)
return None
def get_app_private_key():
"""获取应用私钥"""
return load_key_from_file(ALIPAY_CONFIG['app_private_key_path'])
def get_alipay_public_key():
"""获取支付宝公钥"""
return load_key_from_file(ALIPAY_CONFIG['alipay_public_key_path'])
def validate_config():
"""验证配置是否完整"""
issues = []
# 检查 app_id
if not ALIPAY_CONFIG.get('app_id') or ALIPAY_CONFIG['app_id'].startswith('your_'):
issues.append("app_id not configured")
# 检查密钥文件
if not os.path.exists(ALIPAY_CONFIG['app_private_key_path']):
issues.append(f"Private key file not found: {ALIPAY_CONFIG['app_private_key_path']}")
else:
key = get_app_private_key()
if not key or len(key) < 100:
issues.append("Private key content invalid")
if not os.path.exists(ALIPAY_CONFIG['alipay_public_key_path']):
issues.append(f"Alipay public key file not found: {ALIPAY_CONFIG['alipay_public_key_path']}")
else:
key = get_alipay_public_key()
if not key or len(key) < 100:
issues.append("Alipay public key content invalid")
return len(issues) == 0, issues
if __name__ == "__main__":
print("Alipay Payment Config Validation")
print("=" * 50)
is_valid, issues = validate_config()
if is_valid:
print("[OK] Config validation passed!")
print(f" App ID: {ALIPAY_CONFIG['app_id']}")
print(f" Gateway: {ALIPAY_CONFIG['gateway_url']}")
print(f" Notify URL: {ALIPAY_CONFIG['notify_url']}")
print(f" Return URL: {ALIPAY_CONFIG['return_url']}")
else:
print("[ERROR] Config has issues:")
for issue in issues:
print(f" - {issue}")
print("\nSetup steps:")
print("1. Login to Alipay Open Platform (open.alipay.com)")
print("2. Create app and get App ID")
print("3. Configure RSA2 keys in development settings")
print("4. Put private key and Alipay public key in ./alipay/ folder")
print("5. Update config in this file")
print("=" * 50)

View File

@@ -1,523 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
支付宝支付集成模块
电脑网站支付 (alipay.trade.page.pay)
"""
import json
import time
import base64
import hashlib
from datetime import datetime
from urllib.parse import quote_plus, urlencode
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
class AlipayPay:
"""支付宝电脑网站支付类"""
def __init__(self, app_id, app_private_key, alipay_public_key, notify_url, return_url,
gateway_url='https://openapi.alipay.com/gateway.do',
sign_type='RSA2', charset='utf-8'):
"""
初始化支付宝支付配置
Args:
app_id: 支付宝应用ID
app_private_key: 应用私钥Base64编码的字符串
alipay_public_key: 支付宝公钥Base64编码的字符串
notify_url: 异步通知地址
return_url: 同步跳转地址
gateway_url: 支付宝网关URL
sign_type: 签名类型默认RSA2
charset: 编码默认utf-8
"""
self.app_id = app_id
self.notify_url = notify_url
self.return_url = return_url
self.gateway_url = gateway_url
self.sign_type = sign_type
self.charset = charset
# 加载密钥
self.app_private_key = self._load_private_key(app_private_key)
self.alipay_public_key = self._load_public_key(alipay_public_key)
# 注意:不要在这里使用 print会影响 subprocess 的 JSON 输出
def _load_private_key(self, key_str):
"""加载RSA私钥支持 PKCS#1 和 PKCS#8 格式)"""
try:
# 如果密钥不包含头尾尝试添加PEM格式头尾
if '-----BEGIN' not in key_str:
# 支付宝通常使用 PKCS#8 格式MIIEv 开头)
# 先尝试 PKCS#8 格式
key_str_pkcs8 = f"-----BEGIN PRIVATE KEY-----\n{key_str}\n-----END PRIVATE KEY-----"
try:
private_key = serialization.load_pem_private_key(
key_str_pkcs8.encode('utf-8'),
password=None,
backend=default_backend()
)
return private_key
except Exception:
# 如果 PKCS#8 失败,尝试 PKCS#1 格式
key_str = f"-----BEGIN RSA PRIVATE KEY-----\n{key_str}\n-----END RSA PRIVATE KEY-----"
private_key = serialization.load_pem_private_key(
key_str.encode('utf-8'),
password=None,
backend=default_backend()
)
return private_key
except Exception as e:
import sys
print(f"[AlipayPay] Load private key failed: {e}", file=sys.stderr)
raise
def _load_public_key(self, key_str):
"""加载RSA公钥"""
import sys
try:
# 如果密钥不包含头尾添加PEM格式头尾
if '-----BEGIN' not in key_str:
key_str = f"-----BEGIN PUBLIC KEY-----\n{key_str}\n-----END PUBLIC KEY-----"
public_key = serialization.load_pem_public_key(
key_str.encode('utf-8'),
backend=default_backend()
)
return public_key
except Exception as e:
print(f"[AlipayPay] Load public key failed: {e}", file=sys.stderr)
raise
def _sign(self, unsigned_string):
"""
RSA2签名
Args:
unsigned_string: 待签名字符串
Returns:
Base64编码的签名
"""
import sys
try:
signature = self.app_private_key.sign(
unsigned_string.encode('utf-8'),
padding.PKCS1v15(),
hashes.SHA256()
)
return base64.b64encode(signature).decode('utf-8')
except Exception as e:
print(f"[AlipayPay] Sign failed: {e}", file=sys.stderr)
raise
def _verify(self, message, signature):
"""
验证支付宝签名
Args:
message: 原始消息
signature: Base64编码的签名
Returns:
bool: 验证是否通过
"""
import sys
try:
self.alipay_public_key.verify(
base64.b64decode(signature),
message.encode('utf-8'),
padding.PKCS1v15(),
hashes.SHA256()
)
return True
except Exception as e:
print(f"[AlipayPay] Verify signature failed: {e}", file=sys.stderr)
return False
def _get_sign_content(self, params):
"""
获取待签名字符串
Args:
params: 参数字典
Returns:
排序后的参数字符串
"""
# 过滤空值和sign参数按key排序
filtered_params = {k: v for k, v in params.items()
if v is not None and v != '' and k != 'sign'}
sorted_params = sorted(filtered_params.items())
# 拼接字符串
sign_content = '&'.join([f'{k}={v}' for k, v in sorted_params])
return sign_content
def create_page_pay_url(self, out_trade_no, total_amount, subject, body=None, timeout_express='30m'):
"""
创建电脑网站支付URL
Args:
out_trade_no: 商户订单号
total_amount: 订单总金额(元,精确到小数点后两位)
subject: 订单标题
body: 订单描述(可选)
timeout_express: 订单超时时间默认30分钟
Returns:
dict: 包含支付URL的响应
"""
try:
# 金额格式化为两位小数(支付宝要求)
formatted_amount = f"{float(total_amount):.2f}"
# 业务参数
biz_content = {
'out_trade_no': out_trade_no,
'total_amount': formatted_amount,
'subject': subject,
'product_code': 'FAST_INSTANT_TRADE_PAY', # 电脑网站支付固定值
'timeout_express': timeout_express,
}
if body:
biz_content['body'] = body
# 公共请求参数
params = {
'app_id': self.app_id,
'method': 'alipay.trade.page.pay',
'format': 'json',
'charset': self.charset,
'sign_type': self.sign_type,
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'version': '1.0',
'notify_url': self.notify_url,
'return_url': self.return_url,
'biz_content': json.dumps(biz_content, ensure_ascii=False),
}
# 生成签名
sign_content = self._get_sign_content(params)
params['sign'] = self._sign(sign_content)
# 构建完整的支付URL
pay_url = f"{self.gateway_url}?{urlencode(params)}"
# 日志输出到 stderr避免影响 subprocess JSON 输出
import sys
print(f"[AlipayPay] Order created: {out_trade_no}, amount: {formatted_amount}", file=sys.stderr)
return {
'success': True,
'pay_url': pay_url,
'order_no': out_trade_no
}
except Exception as e:
import sys
print(f"[AlipayPay] Create order failed: {e}", file=sys.stderr)
return {
'success': False,
'error': str(e)
}
def create_wap_pay_url(self, out_trade_no, total_amount, subject, body=None, timeout_express='30m', quit_url=None):
"""
创建手机网站支付URL (H5支付可调起手机支付宝APP)
Args:
out_trade_no: 商户订单号
total_amount: 订单总金额(元,精确到小数点后两位)
subject: 订单标题
body: 订单描述(可选)
timeout_express: 订单超时时间默认30分钟
quit_url: 用户付款中途退出返回商户网站的地址(可选)
Returns:
dict: 包含支付URL的响应
"""
try:
# 金额格式化为两位小数(支付宝要求)
formatted_amount = f"{float(total_amount):.2f}"
# 业务参数
biz_content = {
'out_trade_no': out_trade_no,
'total_amount': formatted_amount,
'subject': subject,
'product_code': 'QUICK_WAP_WAY', # 手机网站支付固定值
'timeout_express': timeout_express,
}
if body:
biz_content['body'] = body
if quit_url:
biz_content['quit_url'] = quit_url
# 公共请求参数
params = {
'app_id': self.app_id,
'method': 'alipay.trade.wap.pay', # 手机网站支付接口
'format': 'json',
'charset': self.charset,
'sign_type': self.sign_type,
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'version': '1.0',
'notify_url': self.notify_url,
'return_url': self.return_url,
'biz_content': json.dumps(biz_content, ensure_ascii=False),
}
# 生成签名
sign_content = self._get_sign_content(params)
params['sign'] = self._sign(sign_content)
# 构建完整的支付URL
pay_url = f"{self.gateway_url}?{urlencode(params)}"
# 日志输出到 stderr避免影响 subprocess JSON 输出
import sys
print(f"[AlipayPay] WAP Order created: {out_trade_no}, amount: {formatted_amount}", file=sys.stderr)
return {
'success': True,
'pay_url': pay_url,
'order_no': out_trade_no,
'pay_type': 'wap' # 标识为手机网站支付
}
except Exception as e:
import sys
print(f"[AlipayPay] Create WAP order failed: {e}", file=sys.stderr)
return {
'success': False,
'error': str(e)
}
def query_order(self, out_trade_no=None, trade_no=None):
"""
查询订单状态
Args:
out_trade_no: 商户订单号
trade_no: 支付宝交易号
Returns:
dict: 订单状态信息
"""
import requests
try:
if not out_trade_no and not trade_no:
return {'success': False, 'error': '订单号和交易号不能同时为空'}
# 业务参数
biz_content = {}
if out_trade_no:
biz_content['out_trade_no'] = out_trade_no
if trade_no:
biz_content['trade_no'] = trade_no
# 公共请求参数
params = {
'app_id': self.app_id,
'method': 'alipay.trade.query',
'format': 'json',
'charset': self.charset,
'sign_type': self.sign_type,
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'version': '1.0',
'biz_content': json.dumps(biz_content, ensure_ascii=False),
}
# 生成签名
sign_content = self._get_sign_content(params)
params['sign'] = self._sign(sign_content)
# 发送请求
response = requests.get(self.gateway_url, params=params, timeout=30)
result = response.json()
# 日志输出到 stderr
import sys
print(f"[AlipayPay] Query response: {result}", file=sys.stderr)
# 解析响应
query_response = result.get('alipay_trade_query_response', {})
if query_response.get('code') == '10000':
trade_status = query_response.get('trade_status')
return {
'success': True,
'trade_status': trade_status,
'trade_no': query_response.get('trade_no'),
'out_trade_no': query_response.get('out_trade_no'),
'total_amount': query_response.get('total_amount'),
'buyer_logon_id': query_response.get('buyer_logon_id'),
'send_pay_date': query_response.get('send_pay_date'),
# 状态映射
'is_paid': trade_status == 'TRADE_SUCCESS' or trade_status == 'TRADE_FINISHED',
'is_closed': trade_status == 'TRADE_CLOSED',
'is_waiting': trade_status == 'WAIT_BUYER_PAY',
}
else:
error_msg = query_response.get('sub_msg') or query_response.get('msg', '查询失败')
return {
'success': False,
'error': error_msg,
'code': query_response.get('code'),
'sub_code': query_response.get('sub_code')
}
except requests.RequestException as e:
import sys
print(f"[AlipayPay] API request failed: {e}", file=sys.stderr)
return {'success': False, 'error': f'网络请求失败: {e}'}
except Exception as e:
import sys
print(f"[AlipayPay] Query order error: {e}", file=sys.stderr)
return {'success': False, 'error': str(e)}
def verify_callback(self, params):
"""
验证支付宝异步回调
Args:
params: 回调参数字典
Returns:
dict: 验证结果
"""
try:
# 获取签名
sign = params.pop('sign', None)
sign_type = params.pop('sign_type', 'RSA2')
if not sign:
return {'success': False, 'error': '缺少签名参数'}
# 构建待验签字符串
sign_content = self._get_sign_content(params)
# 验证签名
import sys
if self._verify(sign_content, sign):
print(f"[AlipayPay] Callback signature verified", file=sys.stderr)
return {
'success': True,
'data': params
}
else:
print(f"[AlipayPay] Callback signature verification failed", file=sys.stderr)
return {
'success': False,
'error': '签名验证失败'
}
except Exception as e:
print(f"[AlipayPay] Verify callback error: {e}", file=sys.stderr)
return {'success': False, 'error': str(e)}
def verify_return(self, params):
"""
验证支付宝同步返回(用户支付后跳转回来)
Args:
params: 同步返回参数字典
Returns:
dict: 验证结果
"""
# 同步返回的验签逻辑与异步回调相同
return self.verify_callback(params)
# 工厂函数
def create_alipay_instance():
"""创建支付宝支付实例"""
try:
from alipay_config import ALIPAY_CONFIG, get_app_private_key, get_alipay_public_key, validate_config
# 验证配置
is_valid, issues = validate_config()
if not is_valid:
raise ValueError(f"支付宝配置错误: {'; '.join(issues)}")
# 获取密钥
app_private_key = get_app_private_key()
alipay_public_key = get_alipay_public_key()
if not app_private_key or not alipay_public_key:
raise ValueError("密钥加载失败")
return AlipayPay(
app_id=ALIPAY_CONFIG['app_id'],
app_private_key=app_private_key,
alipay_public_key=alipay_public_key,
notify_url=ALIPAY_CONFIG['notify_url'],
return_url=ALIPAY_CONFIG['return_url'],
gateway_url=ALIPAY_CONFIG['gateway_url'],
sign_type=ALIPAY_CONFIG['sign_type'],
charset=ALIPAY_CONFIG['charset']
)
except ImportError as e:
raise ValueError(f"无法导入支付宝配置: {e}")
except Exception as e:
raise ValueError(f"创建支付宝实例失败: {e}")
def check_alipay_ready():
"""检查支付宝支付是否就绪"""
try:
instance = create_alipay_instance()
return True, "支付宝支付配置正确"
except Exception as e:
return False, str(e)
if __name__ == '__main__':
"""测试代码"""
import sys
print("=" * 60)
print("Alipay Payment Test")
print("=" * 60)
try:
# 检查配置
is_ready, message = check_alipay_ready()
print(f"\nConfig status: {'READY' if is_ready else 'NOT READY'}")
print(f"Details: {message}")
if is_ready:
# 创建实例
alipay = create_alipay_instance()
# 测试创建订单
test_order_no = f"TEST{int(time.time())}"
result = alipay.create_page_pay_url(
out_trade_no=test_order_no,
total_amount='0.01',
subject='Test Product',
body='This is a test order'
)
print(f"\nCreate order result:")
print(json.dumps(result, indent=2, ensure_ascii=False))
if result['success']:
print(f"\nPayment URL (open in browser):")
print(result['pay_url'][:200] + '...')
except Exception as e:
print(f"\nTest failed: {e}")
import traceback
traceback.print_exc()
print("\n" + "=" * 60)

View File

@@ -1,163 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
支付宝支付工作脚本
用于在 subprocess 中执行,绕过 eventlet DNS 问题
用法:
python alipay_pay_worker.py check # 检查配置
python alipay_pay_worker.py create <order_no> <amount> <subject> [body] [pay_type] # 创建订单
pay_type: page=电脑网站支付(默认), wap=手机网站支付
python alipay_pay_worker.py query <order_no> # 查询订单
"""
import sys
import json
def check_config():
"""检查支付宝配置"""
try:
from alipay_pay import check_alipay_ready
is_ready, message = check_alipay_ready()
return {
'success': is_ready,
'message': message if is_ready else None,
'error': None if is_ready else message
}
except Exception as e:
return {
'success': False,
'error': str(e)
}
def create_order(order_no, amount, subject, body=None, pay_type='page'):
"""创建支付宝订单
Args:
order_no: 订单号
amount: 金额
subject: 标题
body: 描述
pay_type: 支付类型 'page'=电脑网站支付, 'wap'=手机网站支付
"""
try:
from alipay_pay import create_alipay_instance
alipay = create_alipay_instance()
if pay_type == 'wap':
# 手机网站支付
result = alipay.create_wap_pay_url(
out_trade_no=order_no,
total_amount=str(amount),
subject=subject,
body=body,
timeout_express='30m',
quit_url='https://valuefrontier.cn/pricing' # 用户取消支付时返回的页面
)
else:
# 电脑网站支付(默认)
result = alipay.create_page_pay_url(
out_trade_no=order_no,
total_amount=str(amount),
subject=subject,
body=body,
timeout_express='30m'
)
return result
except Exception as e:
return {
'success': False,
'error': str(e)
}
def query_order(order_no):
"""查询支付宝订单"""
try:
from alipay_pay import create_alipay_instance
alipay = create_alipay_instance()
result = alipay.query_order(out_trade_no=order_no)
# 转换状态为统一格式
if result.get('success'):
trade_status = result.get('trade_status')
# 映射支付宝状态到统一状态
if trade_status in ['TRADE_SUCCESS', 'TRADE_FINISHED']:
result['trade_state'] = 'SUCCESS'
elif trade_status == 'WAIT_BUYER_PAY':
result['trade_state'] = 'NOTPAY'
elif trade_status == 'TRADE_CLOSED':
result['trade_state'] = 'CLOSED'
else:
result['trade_state'] = trade_status
return result
except Exception as e:
return {
'success': False,
'error': str(e)
}
def main():
"""主函数"""
if len(sys.argv) < 2:
print(json.dumps({
'success': False,
'error': '缺少命令参数。用法: check | create <order_no> <amount> <subject> | query <order_no>'
}))
sys.exit(1)
command = sys.argv[1].lower()
try:
if command == 'check':
result = check_config()
elif command == 'create':
if len(sys.argv) < 5:
result = {
'success': False,
'error': '创建订单需要参数: order_no, amount, subject'
}
else:
order_no = sys.argv[2]
amount = sys.argv[3]
subject = sys.argv[4]
body = sys.argv[5] if len(sys.argv) > 5 else None
# pay_type: page=电脑网站支付, wap=手机网站支付
pay_type = sys.argv[6] if len(sys.argv) > 6 else 'page'
result = create_order(order_no, amount, subject, body, pay_type)
elif command == 'query':
if len(sys.argv) < 3:
result = {
'success': False,
'error': '查询订单需要参数: order_no'
}
else:
order_no = sys.argv[2]
result = query_order(order_no)
else:
result = {
'success': False,
'error': f'未知命令: {command}'
}
print(json.dumps(result, ensure_ascii=False))
except Exception as e:
print(json.dumps({
'success': False,
'error': str(e)
}, ensure_ascii=False))
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -1,262 +0,0 @@
"""
APNs 推送相关的数据库模型和 API 端点
将以下代码添加到 app.py 中
=== 步骤 1: 添加数据库模型(放在其他模型定义附近)===
"""
# ==================== 数据库模型 ====================
class UserDeviceToken(db.Model):
"""用户设备推送 Token"""
__tablename__ = 'user_device_tokens'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) # 可选,未登录也能注册
device_token = db.Column(db.String(255), unique=True, nullable=False)
platform = db.Column(db.String(20), default='ios') # ios / android
app_version = db.Column(db.String(20), nullable=True)
is_active = db.Column(db.Boolean, default=True)
# 推送订阅设置
subscribe_all = db.Column(db.Boolean, default=True) # 订阅所有事件
subscribe_important = db.Column(db.Boolean, default=True) # 订阅重要事件 (S/A级)
subscribe_types = db.Column(db.Text, nullable=True) # 订阅的事件类型JSON 数组
created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
user = db.relationship('User', backref='device_tokens')
"""
=== 步骤 2: 添加 API 端点(放在其他 API 定义附近)===
"""
# ==================== Device Token API ====================
@app.route('/api/users/device-token', methods=['POST'])
def register_device_token():
"""
注册设备推送 Token
App 启动时调用,保存设备 Token 用于推送
"""
try:
data = request.get_json()
device_token = data.get('device_token')
platform = data.get('platform', 'ios')
app_version = data.get('app_version', '1.0.0')
if not device_token:
return jsonify({'success': False, 'message': '缺少 device_token'}), 400
# 查找是否已存在
existing = UserDeviceToken.query.filter_by(device_token=device_token).first()
if existing:
# 更新现有记录
existing.platform = platform
existing.app_version = app_version
existing.is_active = True
existing.updated_at = datetime.now()
# 如果用户已登录,关联用户
if current_user.is_authenticated:
existing.user_id = current_user.id
else:
# 创建新记录
new_token = UserDeviceToken(
device_token=device_token,
platform=platform,
app_version=app_version,
user_id=current_user.id if current_user.is_authenticated else None,
is_active=True
)
db.session.add(new_token)
db.session.commit()
return jsonify({
'success': True,
'message': 'Device token 注册成功'
})
except Exception as e:
db.session.rollback()
print(f"[API] 注册 device token 失败: {e}")
return jsonify({'success': False, 'message': str(e)}), 500
@app.route('/api/users/device-token', methods=['DELETE'])
def unregister_device_token():
"""
取消注册设备 Token用户登出时调用
"""
try:
data = request.get_json()
device_token = data.get('device_token')
if not device_token:
return jsonify({'success': False, 'message': '缺少 device_token'}), 400
token_record = UserDeviceToken.query.filter_by(device_token=device_token).first()
if token_record:
token_record.is_active = False
token_record.updated_at = datetime.now()
db.session.commit()
return jsonify({
'success': True,
'message': 'Device token 已取消注册'
})
except Exception as e:
db.session.rollback()
print(f"[API] 取消注册 device token 失败: {e}")
return jsonify({'success': False, 'message': str(e)}), 500
@app.route('/api/users/push-subscription', methods=['POST'])
def update_push_subscription():
"""
更新推送订阅设置
"""
try:
data = request.get_json()
device_token = data.get('device_token')
if not device_token:
return jsonify({'success': False, 'message': '缺少 device_token'}), 400
token_record = UserDeviceToken.query.filter_by(device_token=device_token).first()
if not token_record:
return jsonify({'success': False, 'message': 'Device token 不存在'}), 404
# 更新订阅设置
if 'subscribe_all' in data:
token_record.subscribe_all = data['subscribe_all']
if 'subscribe_important' in data:
token_record.subscribe_important = data['subscribe_important']
if 'subscribe_types' in data:
token_record.subscribe_types = json.dumps(data['subscribe_types'])
token_record.updated_at = datetime.now()
db.session.commit()
return jsonify({
'success': True,
'message': '订阅设置已更新'
})
except Exception as e:
db.session.rollback()
print(f"[API] 更新推送订阅失败: {e}")
return jsonify({'success': False, 'message': str(e)}), 500
"""
=== 步骤 3: 修改 broadcast_new_event 函数,添加 APNs 推送 ===
"""
# 在文件顶部导入
from apns_push import send_event_notification
# 修改 broadcast_new_event 函数
def broadcast_new_event_with_apns(event):
"""
广播新事件到所有订阅的客户端WebSocket + APNs
"""
try:
# === 原有的 WebSocket 推送代码 ===
print(f'\n[WebSocket DEBUG] ========== 广播新事件 ==========')
print(f'[WebSocket DEBUG] 事件ID: {event.id}')
print(f'[WebSocket DEBUG] 事件标题: {event.title}')
print(f'[WebSocket DEBUG] 重要性: {event.importance}')
event_data = {
'id': event.id,
'title': event.title,
'importance': event.importance,
'event_type': event.event_type,
'created_at': event.created_at.isoformat() if event.created_at else None,
'source': event.source,
'related_avg_chg': event.related_avg_chg,
'related_max_chg': event.related_max_chg,
'hot_score': event.hot_score,
'keywords': event.keywords_list if hasattr(event, 'keywords_list') else event.keywords,
}
# 发送到 WebSocket 房间
socketio.emit('new_event', event_data, room='events_all', namespace='/')
if event.event_type:
room_name = f"events_{event.event_type}"
socketio.emit('new_event', event_data, room=room_name, namespace='/')
# === 新增APNs 推送 ===
# 只推送重要事件 (S/A 级)
if event.importance in ['S', 'A']:
try:
# 获取所有活跃的设备 token
# 可以根据订阅设置过滤
device_tokens_query = db.session.query(UserDeviceToken.device_token).filter(
UserDeviceToken.is_active == True,
db.or_(
UserDeviceToken.subscribe_all == True,
UserDeviceToken.subscribe_important == True
)
).all()
tokens = [t[0] for t in device_tokens_query]
if tokens:
print(f'[APNs] 准备推送到 {len(tokens)} 个设备')
result = send_event_notification(event, tokens)
print(f'[APNs] 推送结果: 成功 {result["success"]}, 失败 {result["failed"]}')
else:
print('[APNs] 没有活跃的设备 token')
except Exception as apns_error:
print(f'[APNs ERROR] 推送失败: {apns_error}')
import traceback
traceback.print_exc()
# 清除事件列表缓存
clear_events_cache()
print(f'[WebSocket DEBUG] ========== 广播完成 ==========\n')
except Exception as e:
print(f'[WebSocket ERROR] 推送新事件失败: {e}')
import traceback
traceback.print_exc()
"""
=== 步骤 4: 创建数据库表(在 MySQL 中执行)===
"""
SQL_CREATE_TABLE = """
CREATE TABLE IF NOT EXISTS user_device_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NULL,
device_token VARCHAR(255) NOT NULL UNIQUE,
platform VARCHAR(20) DEFAULT 'ios',
app_version VARCHAR(20) NULL,
is_active BOOLEAN DEFAULT TRUE,
subscribe_all BOOLEAN DEFAULT TRUE,
subscribe_important BOOLEAN DEFAULT TRUE,
subscribe_types TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_device_token (device_token),
INDEX idx_user_id (user_id),
INDEX idx_is_active (is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"""
print("请在 MySQL 中执行以下 SQL 创建表:")
print(SQL_CREATE_TABLE)

View File

@@ -1,225 +0,0 @@
"""
APNs 推送通知模块
用于向 iOS 设备发送推送通知
使用前需要安装:
pip install PyAPNs2
配置:
1. 将 AuthKey_HSF578B626.p8 文件放到服务器安全目录
2. 设置环境变量或修改下面的配置
"""
import os
from datetime import datetime
from typing import List, Optional
from apns2.client import APNsClient, NotificationPriority
from apns2.payload import Payload, PayloadAlert
from apns2.credentials import TokenCredentials
# ==================== APNs 配置 ====================
# APNs 认证配置
APNS_KEY_ID = 'HSF578B626'
APNS_TEAM_ID = '6XML2LHR2J'
APNS_BUNDLE_ID = 'com.valuefrontier.meagent'
# Auth Key 文件路径(基于当前文件位置)
_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
APNS_AUTH_KEY_PATH = os.environ.get(
'APNS_AUTH_KEY_PATH',
os.path.join(_BASE_DIR, 'cert', 'AuthKey_HSF578B626.p8')
)
# 是否使用沙盒环境(开发测试用 True生产用 False
APNS_USE_SANDBOX = os.environ.get('APNS_USE_SANDBOX', 'false').lower() == 'true'
# ==================== APNs 客户端初始化 ====================
_apns_client = None
def get_apns_client():
"""获取 APNs 客户端(单例)"""
global _apns_client
if _apns_client is None:
if not os.path.exists(APNS_AUTH_KEY_PATH):
print(f"[APNs] 警告: Auth Key 文件不存在: {APNS_AUTH_KEY_PATH}")
return None
try:
credentials = TokenCredentials(
auth_key_path=APNS_AUTH_KEY_PATH,
auth_key_id=APNS_KEY_ID,
team_id=APNS_TEAM_ID
)
_apns_client = APNsClient(
credentials=credentials,
use_sandbox=APNS_USE_SANDBOX
)
print(f"[APNs] 客户端初始化成功 (sandbox={APNS_USE_SANDBOX})")
except Exception as e:
print(f"[APNs] 客户端初始化失败: {e}")
return None
return _apns_client
# ==================== 推送函数 ====================
def send_push_notification(
device_tokens: List[str],
title: str,
body: str,
data: Optional[dict] = None,
badge: int = 1,
sound: str = "default",
priority: int = 10
) -> dict:
"""
发送推送通知到多个设备
Args:
device_tokens: 设备 Token 列表
title: 通知标题
body: 通知内容
data: 自定义数据(会传递给 App
badge: 角标数字
sound: 提示音
priority: 优先级 (10=立即, 5=省电)
Returns:
dict: {success: int, failed: int, errors: list}
"""
client = get_apns_client()
if not client:
return {'success': 0, 'failed': len(device_tokens), 'errors': ['APNs 客户端未初始化']}
# 构建通知内容
alert = PayloadAlert(title=title, body=body)
payload = Payload(
alert=alert,
sound=sound,
badge=badge,
custom=data or {}
)
# 设置优先级
notification_priority = NotificationPriority.Immediate if priority == 10 else NotificationPriority.PowerConsideration
results = {'success': 0, 'failed': 0, 'errors': []}
for token in device_tokens:
try:
client.send_notification(
token_hex=token,
notification=payload,
topic=APNS_BUNDLE_ID,
priority=notification_priority
)
results['success'] += 1
print(f"[APNs] 推送成功: {token[:20]}...")
except Exception as e:
results['failed'] += 1
results['errors'].append(f"{token[:20]}...: {str(e)}")
print(f"[APNs] 推送失败 {token[:20]}...: {e}")
return results
def send_event_notification(event, device_tokens: List[str]) -> dict:
"""
发送事件推送通知
Args:
event: Event 模型实例
device_tokens: 设备 Token 列表
Returns:
dict: 推送结果
"""
if not device_tokens:
return {'success': 0, 'failed': 0, 'errors': ['没有设备 Token']}
# 根据重要性设置标题
importance_labels = {
'S': '重大事件',
'A': '重要事件',
'B': '一般事件',
'C': '普通事件'
}
title = f"{importance_labels.get(event.importance, '新事件')}"
# 通知内容(截断过长的标题)
body = event.title[:100] + ('...' if len(event.title) > 100 else '')
# 自定义数据
data = {
'event_id': event.id,
'importance': event.importance,
'title': event.title,
'event_type': event.event_type,
'created_at': event.created_at.isoformat() if event.created_at else None
}
return send_push_notification(
device_tokens=device_tokens,
title=title,
body=body,
data=data,
badge=1,
priority=10 if event.importance in ['S', 'A'] else 5
)
# ==================== 数据库操作(需要在 app.py 中实现) ====================
def get_all_device_tokens(db_session) -> List[str]:
"""
获取所有活跃的设备 Token
需要在 app.py 中实现实际的数据库查询
"""
# 示例 SQL:
# SELECT device_token FROM user_device_tokens WHERE is_active = 1
pass
def get_subscribed_device_tokens(db_session, importance_filter: List[str] = None) -> List[str]:
"""
获取订阅了推送的设备 Token
Args:
importance_filter: 重要性过滤,如 ['S', 'A'] 表示只获取订阅了重要事件的用户
"""
# 示例 SQL:
# SELECT device_token FROM user_device_tokens
# WHERE is_active = 1
# AND (subscribe_all = 1 OR subscribe_importance IN ('S', 'A'))
pass
# ==================== 在 app.py 中调用示例 ====================
"""
在 app.py 的 broadcast_new_event() 函数中添加:
from apns_push import send_event_notification
def broadcast_new_event(event):
# ... 现有的 WebSocket 推送代码 ...
# 添加 APNs 推送(只推送重要事件)
if event.importance in ['S', 'A']:
try:
# 获取所有设备 token
device_tokens = db.session.query(UserDeviceToken.device_token).filter(
UserDeviceToken.is_active == True
).all()
tokens = [t[0] for t in device_tokens]
if tokens:
result = send_event_notification(event, tokens)
print(f"[APNs] 事件推送结果: {result}")
except Exception as e:
print(f"[APNs] 推送失败: {e}")
"""

9937
app.py

File diff suppressed because it is too large Load Diff

7417
app_vx.py

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +0,0 @@
node_modules/**/*
.expo/*
npm-debug.*
package-lock.json
yarn.lock
*.jks
*.p12
*.key
*.mobileprovision

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,101 +0,0 @@
import React, { useCallback, useEffect, useState } from "react";
import * as SplashScreen from "expo-splash-screen";
import * as Font from "expo-font";
import { Asset } from "expo-asset";
import { Block, GalioProvider } from "galio-framework";
import { NavigationContainer } from "@react-navigation/native";
import { Image } from "react-native";
import { Provider } from "react-redux";
import { NativeBaseProvider } from "native-base";
// Keep the splash screen visible while we fetch resources
SplashScreen.preventAutoHideAsync();
// Before rendering any navigation stack
import { enableScreens } from "react-native-screens";
enableScreens();
import Screens from "./navigation/Screens";
import { Images, articles, argonTheme } from "./constants";
import store from "./src/store";
import nativeBaseTheme from "./src/theme";
import { AuthProvider } from "./src/contexts/AuthContext";
// cache app images
const assetImages = [
Images.Onboarding,
Images.LogoOnboarding,
Images.Logo,
Images.Pro,
Images.ArgonLogo,
Images.iOSLogo,
Images.androidLogo,
];
// cache product images
articles.map((article) => assetImages.push(article.image));
function cacheImages(images) {
return images.map((image) => {
if (typeof image === "string") {
return Image.prefetch(image);
} else {
return Asset.fromModule(image).downloadAsync();
}
});
}
export default function App() {
const [appIsReady, setAppIsReady] = useState(false);
useEffect(() => {
async function prepare() {
try {
//Load Resources
await _loadResourcesAsync();
// Pre-load fonts, make any API calls you need to do here
await Font.loadAsync({
"open-sans-regular": require("./assets/font/OpenSans-Regular.ttf"),
"open-sans-light": require("./assets/font/OpenSans-Light.ttf"),
"open-sans-bold": require("./assets/font/OpenSans-Bold.ttf"),
});
} catch (e) {
console.warn(e);
} finally {
// Tell the application to render
setAppIsReady(true);
}
}
prepare();
}, []);
const _loadResourcesAsync = async () => {
return Promise.all([...cacheImages(assetImages)]);
};
const onLayoutRootView = useCallback(async () => {
if (appIsReady) {
await SplashScreen.hideAsync();
}
}, [appIsReady]);
if (!appIsReady) {
return null;
}
return (
<Provider store={store}>
<NativeBaseProvider theme={nativeBaseTheme}>
<AuthProvider>
<NavigationContainer onReady={onLayoutRootView}>
<GalioProvider theme={argonTheme}>
<Block flex>
<Screens />
</Block>
</GalioProvider>
</NavigationContainer>
</AuthProvider>
</NativeBaseProvider>
</Provider>
);
}

View File

@@ -1,6 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgLshiAgsJoiJegXNC
55cF2MHBnQCi2AaObrf/qgEavcmgCgYIKoZIzj0DAQehRANCAAQoIgTclBUyCDU2
gFaphqK1I4n1VAkEad144GMKxrdjwfAXbOenkDkUis/6LBEMoOI8tBTcwP1qlY7s
V7zdIhb4
-----END PRIVATE KEY-----

View File

@@ -1,220 +0,0 @@
# [Argon PRO React Native](https://creativetimofficial.github.io/argon-pro-react-native/docs/#) [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social&logo=twitter)](https://twitter.com/intent/tweet?text=Start%20Your%20Development%20With%20A%20Badass%20React%20Native%20app%20inspired%20by%20Argon%20Design%20System.%0Ahttps%3A//demos.creative-tim.com/argon-pro-react-native/)
![version](https://img.shields.io/badge/version-1.6.0-blue.svg) [![GitHub issues open](https://img.shields.io/github/issues/creativetimofficial/ct-argon-pro-react-native.svg?style=flat)](https://github.com/creativetimofficial/ct-argon-pro-react-native/issues?q=is%3Aopen+is%3Aissue) [![GitHub issues closed](https://img.shields.io/github/issues-closed-raw/creativetimofficial/ct-argon-pro-react-native.svg?maxAge=2592000)](https://github.com/creativetimofficial/ct-argon-pro-react-native/issues?q=is%3Aissue+is%3Aclosed)
![Product Gif](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-pro-react-native/argp-rn-thumbnail.jpg)
Argon PRO React Native is a fully coded app template built over [Galio.io](https://galio.io/?ref=creativetim), [React Native](https://facebook.github.io/react-native/?ref=creativetim) and [Expo](https://expo.io/?ref=creativetim) to allow you to create powerful and beautiful e-commerce mobile applications. We have redesigned all the usual components in Galio to make it look like Argon's Design System, minimalistic and easy to use.
Start your development with a badass Design System for React Native inspired by Argon Design System. If you like Argon's Design System, you will love this react native app template! It features a huge number of components and screens built to fit together and look amazing.
### FULLY CODED COMPONENTS
Argon PRO React Native features over 200 variations of components like buttons, inputs, cards, navigations etc, giving you the freedom of choosing and combining. All components can take variations in colour, that you can easily modify inside our theme file.
You will save a lot of time going from prototyping to full-functional code, because all elements are implemented. We wanted the design process to be seamless, so switching from image to the real page is very easy to do.
### Components & Cards
Argon PRO React Native comes packed with a large number of components and cards. Putting together a mobile app has never been easier than matching together different components. From the profile screen to a settings screen, you can easily customise and build your screens. We have created multiple options for you to put together and customise into pixel perfect screens.
View [ all components/cards here](https://demos.creative-tim.com/argon-pro-react-native/index.html#cards).
### Example Screens
If you want to get inspiration or just show something directly to your clients, you can jump start your development with our pre-built example screens. From onboarding screens to profile or discover screens, you will be able to quickly set up the basic structure for your React Native mobile project.
View [all screens here](https://demos.creative-tim.com/argon-pro-react-native/index.html#screens).
Let us know your thoughts below. And good luck with development!
## Table of Contents
* [Versions](#versions)
* [Demo](#demo)
* [Quick Start](#quick-start)
* [Documentation](#documentation)
* [File Structure](#file-structure)
* [OS Support](#os-support)
* [Resources](#resources)
* [Reporting Issues](#reporting-issues)
* [Technical Support or Questions](#technical-support-or-questions)
* [Licensing](#licensing)
* [Useful Links](#useful-links)
## Versions
[<img src="https://github.com/creativetimofficial/public-assets/blob/master/logos/html-logo.jpg?raw=true" width="60" height="60" />](https://www.creative-tim.com/product/argon-design-system)[<img src="https://github.com/creativetimofficial/public-assets/blob/master/logos/vue-logo.jpg?raw=true" width="60" height="60" />](https://www.creative-tim.com/product/vue-argon-design-system)[<img src="https://github.com/creativetimofficial/public-assets/blob/master/logos/react-logo.jpg?raw=true" width="60" height="60" />](https://www.creative-tim.com/product/argon-design-system-react)[<img src="https://github.com/creativetimofficial/public-assets/blob/master/logos/react-native-logo.jpg?raw=true" width="60" height="60" />](https://www.creative-tim.com/product/argon-pro-react-native)[<img src="https://github.com/creativetimofficial/public-assets/blob/master/logos/angular-logo.jpg?raw=true" width="60" height="60" />](https://www.creative-tim.com/product/argon-dashboard-angular)
| HTML | React | Angular |
| --- | --- | --- |
| [![Argon Design System](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-design-system/argon-design-system.jpg)](https://www.creative-tim.com/product/argon-design-system) | [![Argon Design System React](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-design-system-react/argon-design-system-react.jpg)](https://www.creative-tim.com/product/argon-design-system-react) | [![Argon Design System Angular](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-design-system-angular/argon-design-system-angular.jpg)](https://www.creative-tim.com/product/argon-design-system-angular)
## Demo
| Home Screen | Profile Screen | Onboarding Screen | Register Screen |
| --- | --- | --- | --- |
| [![Home Screen](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-react-native/home-screen.png)](https://demos.creative-tim.com/argon-pro-react-native/) | [![Profile Screen](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-react-native/profile-screen.png)](https://demos.creative-tim.com/argon-pro-react-native/) | [![Onboarding Screen](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-pro-react-native/onboarding_screen.PNG)](https://demos.creative-tim.com/argon-pro-react-native/) | [![Register Screen](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-react-native/register-screen.png)](https://demos.creative-tim.com/argon-pro-react-native/) |
- [Start page](https://demos.creative-tim.com/argon-pro-react-native)
- [How to install our product](https://demos.creative-tim.com/argon-pro-react-native/docs/#/install)
[View more](https://demos.creative-tim.com/argon-pro-react-native)
## Quick start
- Buy from [Creative Tim](https://www.creative-tim.com/product/argon-pro-react-native)
## Documentation
The documentation for the Argon PRO React Native is hosted at our [website](https://demos.creative-tim.com/argon-pro-react-native/docs/).
## File Structure
Within the download you'll find the following directories and files:
```
argon-pro-react-native/
├── App.js
├── CHANGELOG.md
├── ISSUE_TEMPLATE.md
├── LICENSE.md
├── README.md
├── app.json
├── assets
│ ├── font
│ ├── imgs
│ └── nucleo\ icons
├── babel.config.js
├── components
│ ├── Button.js
│ ├── Card.js
│ ├── DrawerItem.js
│ ├── Header.js
│ ├── Icon.js
│ ├── Input.js
│ ├── Notification.js
│ ├── Select.js
│ ├── Switch.js
│ ├── Tabs.js
│ └── index.js
├── constants
│ ├── Images.js
│ ├── Theme.js
│ ├── articles.js
│ ├── cart.js
│ ├── categories.js
│ ├── deals.js
│ ├── index.js
│ ├── tabs.js
│ └── utils.js
├── navigation
│ ├── Menu.js
│ └── Screens.js
├── package.json
└── screens
├── About.js
├── Agreement.js
├── Articles.js
├── Beauty.js
├── Cart.js
├── Category.js
├── Chat.js
├── Elements.js
├── Fashion.js
├── Gallery.js
├── Home.js
├── Notifications.js
├── Onboarding.js
├── PersonalNotifications.js
├── Privacy.js
├── Pro.js
├── Product.js
├── Profile.js
├── Register.js
├── Search.js
├── Settings.js
└── SystemNotifications.js
```
## OS Support
At present, we officially aim to support the last two versions of the following operating systems:
[<img src="https://raw.githubusercontent.com/creativetimofficial/ct-material-kit-pro-react-native/master/assets/android-logo.png" width="60" height="60" />](https://www.creative-tim.com/product/material-kit-pro-react-native)[<img src="https://raw.githubusercontent.com/creativetimofficial/ct-material-kit-pro-react-native/master/assets/apple-logo.png" width="60" height="60" />](https://www.creative-tim.com/product/material-kit-pro-react-native)
## Resources
- Demo: <https://demos.creative-tim.com/argon-pro-react-native>
- Download Page: <https://www.creative-tim.com/product/argon-pro-react-native>
- Documentation: <https://demos.creative-tim.com/argon-pro-react-native/docs>
- License Agreement: <https://www.creative-tim.com/license>
- Support: <https://www.creative-tim.com/contact-us>
- Issues: [Github Issues Page](https://github.com/creativetimofficial/ct-argon-pro-react-native/issues)
- [Argon Design System](https://www.creative-tim.com/product/argon-design-system?ref=argonrn-readme) - For Front End Development
- **Dashboards:**
| HTML | React | Vue |
| --- | --- | --- |
| [![Argon HTML](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-dashboard-pro/argon-dashboard-pro.jpg)](https://www.creative-tim.com/product/argon-dashboard-pro) | [![Argon Dashboard React](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-dashboard-pro-react/argon-dashboard-pro-react.jpg)](https://www.creative-tim.com/product/argon-dashboard-pro-react) | [![Argon Dashboard Vue](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/vue-argon-dashboard-pro/vue-argon-dashboard-pro.jpg)](https://www.creative-tim.com/product/vue-argon-dashboard-pro)
| Node.js | Nuxt | Laravel |
| --- | --- | --- |
| [![Argon Dashboard PRO NodeJS](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-dashboard-pro-nodejs/argon-dashboard-pro-nodejs.jpg)](https://www.creative-tim.com/product/argon-dashboard-pro-nodejs) | [![Argon Dashboard PRO Nuxt](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/nuxt-argon-dashboard-pro/nuxt-argon-dashboard-pro.jpg)](https://www.creative-tim.com/product/nuxt-argon-dashboard-pro) | [![Argon Dashboard PRO Laravel](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-dashboard-pro-laravel/argon-dashboard-pro-laravel.jpg)](https://www.creative-tim.com/product/argon-dashboard-pro-laravel)
## Reporting Issues
We use GitHub Issues as the official bug tracker for the Argon PRO React Native. Here are some advices for our users that want to report an issue:
1. Make sure that you are using the latest version of the Argon PRO React Native.
2. Providing us reproducible steps for the issue will shorten the time it takes for it to be fixed.
3. Some issues may be platform specific, so specifying on what platform you encountered the issue might help.
### Technical Support or Questions
If you have questions or need help integrating the product please [contact us](https://www.creative-tim.com/contact-us) instead of opening an issue.
## Licensing
- Copyright 2019 Creative Tim (https://www.creative-tim.com/)
- Creative Tim [license](https://creative-tim.com/license)
## Useful Links
- [Tutorials](https://www.youtube.com/channel/UCVyTG4sCw-rOvB9oHkzZD1w)
- [Affiliate Program](https://www.creative-tim.com/affiliates/new) (earn money)
- [Blog Creative Tim](http://blog.creative-tim.com/)
- [Free Products](https://www.creative-tim.com/bootstrap-themes/free) from Creative Tim
- [Premium Products](https://www.creative-tim.com/bootstrap-themes/premium) from Creative Tim
- [React Products](https://www.creative-tim.com/bootstrap-themes/react-themes) from Creative Tim
- [Angular Products](https://www.creative-tim.com/bootstrap-themes/angular-themes) from Creative Tim
- [VueJS Products](https://www.creative-tim.com/bootstrap-themes/vuejs-themes) from Creative Tim
- [More products](https://www.creative-tim.com/bootstrap-themes) from Creative Tim
- Check our Bundles [here](https://www.creative-tim.com/bundles?ref="argon-github-readme")
### Social Media
Twitter: <https://twitter.com/CreativeTim>
Facebook: <https://www.facebook.com/CreativeTim>
Dribbble: <https://dribbble.com/creativetim>
Google+: <https://plus.google.com/+CreativetimPage>
Instagram: <https://www.instagram.com/CreativeTimOfficial>

View File

@@ -1,50 +0,0 @@
{
"expo": {
"name": "价值前沿",
"slug": "valuefrontier",
"privacy": "public",
"platforms": [
"ios",
"android"
],
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/logo.jpg",
"splash": {
"image": "./assets/logo.jpg",
"resizeMode": "contain",
"backgroundColor": "#000000"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.valuefrontier.meagent",
"infoPlist": {
"UIBackgroundModes": ["remote-notification"]
}
},
"android": {
"package": "com.valuefrontier.meagent",
"adaptiveIcon": {
"foregroundImage": "./assets/logo.jpg",
"backgroundColor": "#000000"
}
},
"plugins": [
[
"expo-notifications",
{
"icon": "./assets/logo.jpg",
"color": "#D4AF37",
"sounds": []
}
]
],
"description": "价值前沿 - 智能投资助手"
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 778 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" width="24" height="24"><g class="nc-icon-wrapper" fill="#444444"><path fill="#444444" d="M20,0H4C2.3,0,1,1.3,1,3v20c0,0.6,0.4,1,1,1h20c0.6,0,1-0.4,1-1V3C23,1.3,21.7,0,20,0z M12,16 c-3.3,0-6-2.7-6-6c0-0.6,0.4-1,1-1s1,0.4,1,1c0,2.2,1.8,4,4,4s4-1.8,4-4c0-0.6,0.4-1,1-1s1,0.4,1,1C18,13.3,15.3,16,12,16z M20,4H4 C3.4,4,3,3.6,3,3s0.4-1,1-1h16c0.6,0,1,0.4,1,1S20.6,4,20,4z"/></g></svg>

Before

Width:  |  Height:  |  Size: 497 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 12 12" width="12" height="12"><g class="nc-icon-wrapper" fill="#444444"><path d="M1,10.5A1.5,1.5,0,0,0,2.5,12h7A1.5,1.5,0,0,0,11,10.5V7H1Z" fill="#444444"/> <path d="M9.838,4,8.171.665a.75.75,0,0,0-1.342.67L8.162,4H3.838L5.171,1.335A.75.75,0,0,0,3.829.665L2.162,4H0V6H12V4Z" fill="#444444" data-color="color-2"/></g></svg>

Before

Width:  |  Height:  |  Size: 434 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" width="24" height="24"><g class="nc-icon-wrapper" fill="#444444"><path d="M20,10V8A8,8,0,0,0,4,8v2a4.441,4.441,0,0,1-1.547,3.193A4.183,4.183,0,0,0,1,16c0,2.5,4.112,4,11,4s11-1.5,11-4a4.183,4.183,0,0,0-1.453-2.807A4.441,4.441,0,0,1,20,10Z" fill="#444444"/> <path data-color="color-2" d="M9.145,21.9a2.992,2.992,0,0,0,5.71,0c-.894.066-1.844.1-2.855.1S10.039,21.968,9.145,21.9Z" fill="#444444"/></g></svg>

Before

Width:  |  Height:  |  Size: 521 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" width="24" height="24"><g class="nc-icon-wrapper" fill="#444444"><rect data-color="color-2" x="4" y="10" width="4" height="3" fill="#444444"/> <rect data-color="color-2" x="10" y="10" width="4" height="3" fill="#444444"/> <rect data-color="color-2" x="4" y="15" width="4" height="3" fill="#444444"/> <rect data-color="color-2" x="10" y="15" width="4" height="3" fill="#444444"/> <rect data-color="color-2" x="16" y="10" width="4" height="3" fill="#444444"/> <path d="M23,3H18V1a1,1,0,0,0-2,0V3H8V1A1,1,0,0,0,6,1V3H1A1,1,0,0,0,0,4V22a1,1,0,0,0,1,1H23a1,1,0,0,0,1-1V4A1,1,0,0,0,23,3ZM22,21H2V7H22Z" fill="#444444"/></g></svg>

Before

Width:  |  Height:  |  Size: 742 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" width="24" height="24"><g class="nc-icon-wrapper" fill="#444444"><path data-color="color-2" fill="#444444" d="M13,11h10.949C23.466,5.181,18.819,0.534,13,0.051V11z"/> <path fill="#444444" d="M12.414,13l-8.155,8.155C6.351,22.926,9.051,24,12,24c6.279,0,11.438-4.851,11.949-11H12.414z"/> <path fill="#444444" d="M11,11.586V0.051C4.851,0.562,0,5.721,0,12c0,2.949,1.074,5.649,2.845,7.741L11,11.586z"/></g></svg>

Before

Width:  |  Height:  |  Size: 524 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" width="24" height="24"><g class="nc-icon-wrapper" fill="#444444"><path fill="#444444" d="M18.768,1.36C18.578,1.132,18.297,1,18,1H6C5.703,1,5.422,1.132,5.232,1.36l-5,6 c-0.294,0.353-0.31,0.861-0.039,1.231l11,15C11.382,23.848,11.682,24,12,24s0.618-0.152,0.807-0.409l11-15 c0.271-0.371,0.256-0.878-0.039-1.231L18.768,1.36z M19,9H5V7h14V9z"/></g></svg>

Before

Width:  |  Height:  |  Size: 467 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 16 16" width="16" height="16"><g class="nc-icon-wrapper" fill="#444444"><path d="M8,15A7,7,0,0,1,3.333,2.783l1.334,1.49a5,5,0,1,0,6.666,0l1.333-1.49A7,7,0,0,1,8,15Z" fill="#444444"/> <rect x="7" width="2" height="7" fill="#444444" data-color="color-2"/></g></svg>

Before

Width:  |  Height:  |  Size: 375 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 16 16" width="16" height="16"><g class="nc-icon-wrapper" fill="#444444"><path fill="#444444" d="M16,3.6L15.2,2C8.3,4,4.8,8.4,4.8,8.4L1.6,6L0,7.6L4.8,14C8.5,7.1,16,3.6,16,3.6z"/></g></svg>

Before

Width:  |  Height:  |  Size: 299 B

Some files were not shown because too many files have changed in this diff Show More