Compare commits
2 Commits
6806df90c9
...
feature_bu
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c49ddf42c | |||
| d64349b606 |
@@ -6,7 +6,7 @@
|
||||
SERVER_HOST=your-server-ip-or-domain
|
||||
|
||||
# SSH 用户名
|
||||
SERVER_USER=ubuntu
|
||||
SERVER_USER=ubuntu
|
||||
|
||||
# SSH 端口
|
||||
SERVER_PORT=22
|
||||
|
||||
@@ -19,7 +19,9 @@ 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
|
||||
# PostHog 配置(开发环境)
|
||||
# 留空 = 仅控制台 debug
|
||||
# 填入 Key = 控制台 + PostHog Cloud 双模式
|
||||
REACT_APP_POSTHOG_KEY=
|
||||
REACT_APP_POSTHOG_HOST=https://app.posthog.com
|
||||
REACT_APP_ENABLE_SESSION_RECORDING=false
|
||||
|
||||
15
.env.mock
@@ -29,13 +29,20 @@ 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
|
||||
|
||||
# Mock 环境标识
|
||||
REACT_APP_ENV=mock
|
||||
|
||||
# PostHog 配置(Mock 环境)
|
||||
# 留空 = 仅控制台 debug
|
||||
# 填入 Key = 控制台 + PostHog Cloud 双模式
|
||||
REACT_APP_POSTHOG_KEY=phc_xKlRyG69Bx7hgOdFeCeLUvQWvSjw18ZKFgCwCeYezWF
|
||||
REACT_APP_POSTHOG_HOST=https://app.posthog.com
|
||||
REACT_APP_ENABLE_SESSION_RECORDING=false
|
||||
|
||||
# PostHog Debug 模式(Mock 环境永久启用)
|
||||
# 在浏览器 Console 中打印详细的事件追踪日志
|
||||
REACT_APP_POSTHOG_DEBUG=true
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
# ========================================
|
||||
# 生产环境配置
|
||||
# ========================================
|
||||
# 使用方式: npm run build
|
||||
#
|
||||
# 工作原理:
|
||||
# 1. 此文件专门用于生产环境构建
|
||||
# 2. 构建时会将环境变量嵌入到打包文件中
|
||||
# 3. 确保 PostHog 等服务使用正确的生产配置
|
||||
# ========================================
|
||||
|
||||
# 环境标识
|
||||
REACT_APP_ENV=production
|
||||
@@ -10,12 +17,16 @@ NODE_ENV=production
|
||||
REACT_APP_ENABLE_MOCK=false
|
||||
|
||||
# 🔧 调试模式(生产环境临时调试用)
|
||||
# 开启后会在全局暴露 window.__DEBUG__
|
||||
REACT_APP_ENABLE_DEBUG=false
|
||||
# 开启后会在全局暴露 window.__DEBUG__ 和 window.__TEST_NOTIFICATION__ 调试 API
|
||||
# ⚠️ 警告: 调试模式会记录所有 API 请求/响应,调试完成后请立即关闭!
|
||||
# 使用方法:
|
||||
# 1. 设置为 true 并重新构建
|
||||
# 2. 在浏览器控制台使用 window.__DEBUG__.help() 查看命令
|
||||
# 3. 调试完成后设置为 false 并重新构建
|
||||
REACT_APP_ENABLE_DEBUG=true
|
||||
|
||||
# 后端 API 地址(生产环境)
|
||||
# 使用单独的 API 域名,静态资源走 CDN,API 走专用域名
|
||||
REACT_APP_API_URL=https://api.valuefrontier.cn
|
||||
REACT_APP_API_URL=http://49.232.185.254:5001
|
||||
|
||||
# PostHog 分析配置(生产环境)
|
||||
# PostHog API Key(从 PostHog 项目设置中获取)
|
||||
@@ -39,10 +50,17 @@ 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
|
||||
# ========================================
|
||||
# Bytedesk 客服系统配置
|
||||
# ========================================
|
||||
# Bytedesk 服务器地址
|
||||
REACT_APP_BYTEDESK_API_URL=http://43.143.189.195
|
||||
|
||||
# 组织 ID(从管理后台获取)
|
||||
REACT_APP_BYTEDESK_ORG=df_org_uid
|
||||
|
||||
# 工作组 ID(从管理后台获取)
|
||||
REACT_APP_BYTEDESK_SID=df_wg_aftersales
|
||||
|
||||
# 客服类型(2=人工客服, 1=机器人)
|
||||
REACT_APP_BYTEDESK_TYPE=2
|
||||
|
||||
94
.eslintrc.js
@@ -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',
|
||||
],
|
||||
};
|
||||
14
.gitignore
vendored
@@ -22,10 +22,6 @@ node_modules/
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# 部署配置(包含密钥,不提交)
|
||||
.env.cos
|
||||
.env.deploy
|
||||
|
||||
# 日志
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
@@ -52,14 +48,6 @@ Thumbs.db
|
||||
*.md
|
||||
!README.md
|
||||
!CLAUDE.md
|
||||
|
||||
# 忽略 docs 目录(开发文档不提交到 Git)
|
||||
docs/
|
||||
!docs/**/*.md
|
||||
|
||||
src/assets/img/original-backup/
|
||||
|
||||
# 涨停分析静态数据(由 export_zt_data.py 生成,不提交到 Git)
|
||||
public/data/zt/
|
||||
|
||||
# 概念涨跌幅静态数据(由 export_concept_data.py 生成,不提交到 Git)
|
||||
public/data/concept/
|
||||
|
||||
13
ISSUE_TEMPLATE.md
Normal 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
|
||||
-->
|
||||
@@ -1 +0,0 @@
|
||||
17Fo4JhapMw6vtNa
|
||||
198
README.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# vf_react
|
||||
|
||||
前端
|
||||
|
||||
---
|
||||
|
||||
## 📚 重构记录
|
||||
|
||||
### 2025-10-30: EventList.js 组件化重构
|
||||
|
||||
#### 🎯 重构目标
|
||||
将 Community 社区页面的 `EventList.js` 组件(1095行)拆分为多个可复用的子组件,提高代码可维护性和复用性。
|
||||
|
||||
#### 📊 重构成果
|
||||
- **重构前**: 1095 行
|
||||
- **重构后**: 497 行
|
||||
- **减少**: 598 行 (-54.6%)
|
||||
|
||||
---
|
||||
|
||||
### 📁 新增目录结构
|
||||
|
||||
```
|
||||
src/views/Community/components/EventCard/
|
||||
├── index.js (60行) - EventCard 统一入口,智能路由紧凑/详细模式
|
||||
│
|
||||
├── ──────────────────────────────────────────────────────────
|
||||
│ 原子组件 (Atoms) - 7个基础UI组件
|
||||
├── ──────────────────────────────────────────────────────────
|
||||
│
|
||||
├── EventTimeline.js (60行) - 时间轴显示组件
|
||||
│ └── Props: createdAt, timelineStyle, borderColor, minHeight
|
||||
│
|
||||
├── EventImportanceBadge.js (100行) - 重要性等级标签 (S/A/B/C/D)
|
||||
│ └── Props: importance, showTooltip, showIcon, size
|
||||
│
|
||||
├── EventStats.js (60行) - 统计信息 (浏览/帖子/关注)
|
||||
│ └── Props: viewCount, postCount, followerCount, size, spacing
|
||||
│
|
||||
├── EventFollowButton.js (40行) - 关注按钮
|
||||
│ └── Props: isFollowing, followerCount, onToggle, size, showCount
|
||||
│
|
||||
├── EventPriceDisplay.js (130行) - 价格变动显示 (平均/最大/周)
|
||||
│ └── Props: avgChange, maxChange, weekChange, compact, inline
|
||||
│
|
||||
├── EventDescription.js (60行) - 描述文本 (支持展开/收起)
|
||||
│ └── Props: description, textColor, minLength, noOfLines
|
||||
│
|
||||
├── EventHeader.js (100行) - 事件标题头部
|
||||
│ └── Props: title, importance, onTitleClick, linkColor, compact
|
||||
│
|
||||
├── ──────────────────────────────────────────────────────────
|
||||
│ 组合组件 (Molecules) - 2个卡片组件
|
||||
├── ──────────────────────────────────────────────────────────
|
||||
│
|
||||
├── CompactEventCard.js (160行) - 紧凑模式事件卡片
|
||||
│ ├── 使用: EventTimeline, EventHeader, EventStats, EventFollowButton
|
||||
│ └── Props: event, index, isFollowing, followerCount, callbacks...
|
||||
│
|
||||
└── DetailedEventCard.js (170行) - 详细模式事件卡片
|
||||
├── 使用: EventTimeline, EventHeader, EventStats, EventFollowButton,
|
||||
│ EventPriceDisplay, EventDescription
|
||||
└── Props: event, isFollowing, followerCount, callbacks...
|
||||
```
|
||||
|
||||
**总计**: 10个文件,940行代码
|
||||
|
||||
---
|
||||
|
||||
### 🔧 重构的文件
|
||||
|
||||
#### `src/views/Community/components/EventList.js`
|
||||
|
||||
**移除的内容**:
|
||||
- ❌ `renderPriceChange` 函数 (~60行)
|
||||
- ❌ `renderCompactEvent` 函数 (~200行)
|
||||
- ❌ `renderDetailedEvent` 函数 (~300行)
|
||||
- ❌ `expandedDescriptions` state(展开状态管理移至子组件)
|
||||
- ❌ 冗余的 Chakra UI 导入
|
||||
|
||||
**保留的功能**:
|
||||
- ✅ WebSocket 实时推送
|
||||
- ✅ 浏览器原生通知
|
||||
- ✅ 关注状态管理 (followingMap, followCountMap)
|
||||
- ✅ 分页控制
|
||||
- ✅ 视图模式切换(紧凑/详细)
|
||||
- ✅ 推送权限管理
|
||||
|
||||
**新增引入**:
|
||||
```javascript
|
||||
import EventCard from './EventCard';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🏗️ 架构改进
|
||||
|
||||
#### 重构前(单体架构)
|
||||
```
|
||||
EventList.js (1095行)
|
||||
├── 业务逻辑 (WebSocket, 关注, 通知)
|
||||
├── renderCompactEvent (200行)
|
||||
│ └── 所有UI代码内联
|
||||
├── renderDetailedEvent (300行)
|
||||
│ └── 所有UI代码内联
|
||||
└── renderPriceChange (60行)
|
||||
```
|
||||
|
||||
#### 重构后(组件化架构)
|
||||
```
|
||||
EventList.js (497行) - 容器组件
|
||||
├── 业务逻辑 (WebSocket, 关注, 通知)
|
||||
└── 渲染逻辑
|
||||
└── EventCard (智能路由)
|
||||
├── CompactEventCard (紧凑模式)
|
||||
│ ├── EventTimeline
|
||||
│ ├── EventHeader (compact)
|
||||
│ ├── EventStats
|
||||
│ └── EventFollowButton
|
||||
└── DetailedEventCard (详细模式)
|
||||
├── EventTimeline
|
||||
├── EventHeader (detailed)
|
||||
├── EventStats
|
||||
├── EventFollowButton
|
||||
├── EventPriceDisplay
|
||||
└── EventDescription
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✨ 优势
|
||||
|
||||
1. **可维护性** ⬆️
|
||||
- 每个组件职责单一(单一职责原则)
|
||||
- 代码行数减少 54.6%
|
||||
- 组件边界清晰,易于理解
|
||||
|
||||
2. **可复用性** ⬆️
|
||||
- 原子组件可在其他页面复用
|
||||
- 例如:EventImportanceBadge 可用于任何需要显示事件等级的地方
|
||||
|
||||
3. **可测试性** ⬆️
|
||||
- 小组件更容易编写单元测试
|
||||
- 可独立测试每个组件的渲染和交互
|
||||
|
||||
4. **性能优化** ⬆️
|
||||
- React 可以更精确地追踪变化
|
||||
- 减少不必要的重渲染
|
||||
- 每个子组件可独立优化(useMemo, React.memo)
|
||||
|
||||
5. **开发效率** ⬆️
|
||||
- 新增功能时只需修改对应的子组件
|
||||
- 代码审查更高效
|
||||
- 降低了代码冲突的概率
|
||||
|
||||
---
|
||||
|
||||
### 📦 依赖工具函数
|
||||
|
||||
本次重构使用了之前提取的工具函数:
|
||||
|
||||
```
|
||||
src/utils/priceFormatters.js (105行)
|
||||
├── getPriceChangeColor(value) - 获取价格变化文字颜色
|
||||
├── getPriceChangeBg(value) - 获取价格变化背景颜色
|
||||
├── getPriceChangeBorderColor(value) - 获取价格变化边框颜色
|
||||
├── formatPriceChange(value) - 格式化价格为字符串
|
||||
└── PriceArrow({ value }) - 价格涨跌箭头组件
|
||||
|
||||
src/constants/animations.js (72行)
|
||||
├── pulseAnimation - 脉冲动画(S/A级标签)
|
||||
├── fadeIn - 渐入动画
|
||||
├── slideInUp - 从下往上滑入
|
||||
├── scaleIn - 缩放进入
|
||||
└── spin - 旋转动画(Loading)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🚀 下一步优化计划
|
||||
|
||||
Phase 1 已完成,后续可继续优化:
|
||||
|
||||
- **Phase 2**: 拆分 StockDetailPanel.js (1067行 → ~250行)
|
||||
- **Phase 3**: 拆分 InvestmentCalendar.js (827行 → ~200行)
|
||||
- **Phase 4**: 拆分 MidjourneyHeroSection.js (813行 → ~200行)
|
||||
- **Phase 5**: 拆分 UnifiedSearchBox.js (679行 → ~180行)
|
||||
|
||||
---
|
||||
|
||||
### 🔗 相关提交
|
||||
|
||||
- `feat: 拆分 EventList.js/提取价格相关工具函数到 utils/priceFormatters.js`
|
||||
- `feat(EventList): 创建事件卡片原子组件`
|
||||
- `feat(EventList): 创建事件卡片组合组件`
|
||||
- `refactor(EventList): 使用组件化架构替换内联渲染函数`
|
||||
|
||||
---
|
||||
BIN
__pycache__/config.cpython-311.pyc
Executable file
BIN
__pycache__/wechat_pay.cpython-310.pyc
Normal file
BIN
__pycache__/wechat_pay_config.cpython-310.pyc
Normal file
BIN
about_us.docx
@@ -1 +0,0 @@
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkBfIjOiu8vfmOSq1BXcjDsAqQ+xtwGO0aCn0VrhVzc0T70nDchaW/TJ4nW8qlRMBlgfTi00jDGFiW4ND9JHc4aES8yiDSNeaBW4gLQhC1isvpOndyu4YgDc+2lMfghv9+6D8uFl9VS8Vk6o7D+5SiE7F8aBn49rrLyvsmdN5i6eLxIuY9NM58/o0xVG5f3ktGqfFKzhtclPbt8ej39EgziCiNFbIk2FnZp9dB56vtmCKht4t3STDpM0RfC8YlQ2WpGu9okLJYSy1rfynhh0hlOy/9y4cYl50wthoQVxH/Hm9abiTMo2u6xWESreavtdDZ8ByKVltnUrRvzDQ4tVkYwIDAQAB
|
||||
@@ -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=
|
||||
@@ -1 +0,0 @@
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjyULL5tuD2cjfNvgn9fvVfn+WbLhoP3wk3bcYb9AabsZc1GCBlnG579Socc3U9dG5IR7C3KHD4kYHnH4tbK2pEWmmfjbVKXWguLqL5K3Dggnl5KVOlVVjrcsmLPqTj7JdeO0AQolmgdr/o6TlhQdsqINQAK5F5wWwIwwcSoJsWZ6zlPPX/Q/eMIN4zGgK2taMhx656zSxsYE5OKRYkTJhVrMktxQdwbUzoFSID++dTpjxF4w5k5qeVW/1WZaaswMFWh2IcJ5oyc+VjTRqZvtQt4gB0Fa0EblfmSJaozhoWHwzwF+1qtv7gp/TcMYj/Ah2+tY0UPxucEcTqY/i7PPfwIDAQAB
|
||||
118
alipay_config.py
@@ -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)
|
||||
523
alipay_pay.py
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
225
apns_push.py
@@ -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}")
|
||||
"""
|
||||
6352
app_vx_copy1.py
8126
app_vx_raw.py
Normal file
9
argon-pro-react-native/.gitignore
vendored
@@ -1,9 +0,0 @@
|
||||
node_modules/**/*
|
||||
.expo/*
|
||||
npm-debug.*
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
*.jks
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgLshiAgsJoiJegXNC
|
||||
55cF2MHBnQCi2AaObrf/qgEavcmgCgYIKoZIzj0DAQehRANCAAQoIgTclBUyCDU2
|
||||
gFaphqK1I4n1VAkEad144GMKxrdjwfAXbOenkDkUis/6LBEMoOI8tBTcwP1qlY7s
|
||||
V7zdIhb4
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,220 +0,0 @@
|
||||
# [Argon PRO React Native](https://creativetimofficial.github.io/argon-pro-react-native/docs/#) [](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/)
|
||||
|
||||
|
||||
 [](https://github.com/creativetimofficial/ct-argon-pro-react-native/issues?q=is%3Aopen+is%3Aissue) [](https://github.com/creativetimofficial/ct-argon-pro-react-native/issues?q=is%3Aissue+is%3Aclosed)
|
||||
|
||||
|
||||

|
||||
|
||||
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 |
|
||||
| --- | --- | --- |
|
||||
| [](https://www.creative-tim.com/product/argon-design-system) | [](https://www.creative-tim.com/product/argon-design-system-react) | [](https://www.creative-tim.com/product/argon-design-system-angular)
|
||||
|
||||
## Demo
|
||||
|
||||
| Home Screen | Profile Screen | Onboarding Screen | Register Screen |
|
||||
| --- | --- | --- | --- |
|
||||
| [](https://demos.creative-tim.com/argon-pro-react-native/) | [](https://demos.creative-tim.com/argon-pro-react-native/) | [](https://demos.creative-tim.com/argon-pro-react-native/) | [](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 |
|
||||
| --- | --- | --- |
|
||||
| [](https://www.creative-tim.com/product/argon-dashboard-pro) | [](https://www.creative-tim.com/product/argon-dashboard-pro-react) | [](https://www.creative-tim.com/product/vue-argon-dashboard-pro)
|
||||
|
||||
| Node.js | Nuxt | Laravel |
|
||||
| --- | --- | --- |
|
||||
| [](https://www.creative-tim.com/product/argon-dashboard-pro-nodejs) | [](https://www.creative-tim.com/product/nuxt-argon-dashboard-pro) | [](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>
|
||||
|
||||
@@ -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": "价值前沿 - 智能投资助手"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 271 KiB |
|
Before Width: | Height: | Size: 778 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 42 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 x="22" y="11" fill="#444444" width="2" height="6"/> <path data-color="color-2" fill="#444444" d="M13.241,15.73C12.847,15.91,12.43,16,12,16s-0.847-0.09-1.24-0.269L4,12.658V18 c0,2.626,4.024,4,8,4s8-1.374,8-4v-5.341L13.241,15.73z"/> <path fill="#444444" d="M23.414,7.09l-11-5c-0.263-0.119-0.564-0.119-0.827,0l-11,5C0.229,7.252,0,7.607,0,8s0.229,0.748,0.586,0.91 l11,5C11.718,13.97,11.859,14,12,14s0.282-0.03,0.414-0.09l11-5C23.771,8.748,24,8.393,24,8S23.771,7.252,23.414,7.09z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 677 B |
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g class="nc-icon-wrapper" fill="#444444">
|
||||
<path d="M20 24c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm-8-8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 16c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm24-16c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-8 16c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm8-8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm-8-8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm-8-8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 639 B |
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g class="nc-icon-wrapper" fill="#444444">
|
||||
<path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 276 B |
@@ -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"><polygon fill="#444444" points="17,1.382 13,3.382 13,22.618 17,20.618 "/> <polygon data-color="color-2" fill="#444444" points="11,3.382 7,1.382 7,20.618 11,22.618 "/> <path fill="#444444" d="M5,1.434L0.485,4.143C0.185,4.323,0,4.648,0,5v19l5-3.234V1.434z"/> <path data-color="color-2" fill="#444444" d="M23.515,4.143L19,1.434v19.332L24,24V5C24,4.648,23.815,4.323,23.515,4.143z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 572 B |
@@ -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="M11,9H1a1,1,0,0,0,0,2H11a1,1,0,0,0,0-2Z" fill="#444444"/> <path d="M11,1H1A1,1,0,0,0,1,3H11a1,1,0,0,0,0-2Z" fill="#444444"/> <path d="M11,5H1A1,1,0,0,0,1,7H11a1,1,0,0,0,0-2Z" fill="#444444" data-color="color-2"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 415 B |
@@ -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"><polygon points="6 5.882 2.148 2.03 0.074 4.104 6 10.03 11.926 4.104 9.852 2.03 6 5.882" fill="#444444"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 299 B |
@@ -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"><polygon points="7.92 0 1.92 6 7.92 12 10.02 9.9 6.12 6 10.02 2.1 7.92 0" fill="#444444"></polygon></g></svg>
|
||||
|
Before Width: | Height: | Size: 293 B |
@@ -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"><polygon points="1.98 2.1 5.88 6 1.98 9.9 4.08 12 10.08 6 4.08 0 1.98 2.1" fill="#444444"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 285 B |
@@ -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="M8,2c0.6,0,1.1,0.2,1.5,0.7l0.7,0.7l1.5-1.3L11,1.3C10.2,0.5,9.1,0,8,0C5.8,0,4,1.8,4,4v1.5 C2.8,6.6,2,8.2,2,10c0,3.3,2.7,6,6,6s6-2.7,6-6s-2.7-6-6-6C7.3,4,6.6,4.1,6,4.4V4C6,2.9,6.9,2,8,2z M8,7c1.1,0,2,0.9,2,2 c0,0.7-0.4,1.4-1,1.7V13H7v-2.3c-0.6-0.3-1-1-1-1.7C6,7.9,6.9,7,8,7z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 493 B |
@@ -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.4,6.1c-1.1-0.5-2.2-0.8-3.4-0.8c-1.6,0-3,0.5-4.3,0.9c-0.8,0.3-1.6,0.5-2.2,0.5c-0.4,0-0.6-0.1-0.7-0.3 c0-0.1,0.1-0.6,0.2-0.9c0.3-0.9,0.6-2-0.2-3.1c-0.5-0.6-1.3-1-2.2-1c-0.9,0-1.7,0.3-2.5,0.8C1.9,4.4,0,8.1,0,12c0,6.6,5.4,12,12,12 c5.3,0,10.1-3.6,11.6-8.8C23.7,14.6,25.1,8.4,20.4,6.1z M3,12c0-1.1,0.9-2,2-2s2,0.9,2,2s-0.9,2-2,2S3,13.1,3,12z M7.5,19 c-1.1,0-2-0.9-2-2s0.9-2,2-2s2,0.9,2,2S8.6,19,7.5,19z M13,21c-1.1,0-2-0.9-2-2s0.9-2,2-2s2,0.9,2,2S14.1,21,13,21z M17,15 c-1.7,0-3-1.3-3-3s1.3-3,3-3s3,1.3,3,3S18.7,15,17,15z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 741 B |
@@ -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 data-color="color-2" fill="#444444" d="M10,8H8v2H6V8H4V6h2V4h2v2h2V8z"/> <path fill="#444444" d="M7,14c-3.86,0-7-3.14-7-7s3.14-7,7-7s7,3.14,7,7S10.86,14,7,14z M7,2C4.243,2,2,4.243,2,7s2.243,5,5,5 s5-2.243,5-5S9.757,2,7,2z"/> <path data-color="color-2" fill="#444444" d="M15.707,14.293L13.314,11.9c-0.411,0.529-0.885,1.003-1.414,1.414l2.393,2.393 C14.488,15.902,14.744,16,15,16s0.512-0.098,0.707-0.293C16.098,15.316,16.098,14.684,15.707,14.293z"/> </g></svg>
|
||||
|
Before Width: | Height: | Size: 647 B |
@@ -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="M13.9,0.5C13.7,0.2,13.4,0,13,0H3C2.6,0,2.3,0.2,2.1,0.5C0,4.5,0,4.7,0,5c0,1.1,0.9,2,2,2v8c0,0.6,0.4,1,1,1 h10c0.6,0,1-0.4,1-1V7c1.1,0,2-0.9,2-2C16,4.7,16,4.5,13.9,0.5z M10,14v-4H6v4H4V6.7C4.3,6.9,4.6,7,5,7c0.6,0,1.1-0.3,1.5-0.7 C6.9,6.7,7.4,7,8,7s1.1-0.3,1.5-0.7C9.9,6.7,10.4,7,11,7c0.4,0,0.7-0.1,1-0.3V14H10z"></path></g></svg>
|
||||
|
Before Width: | Height: | Size: 535 B |
@@ -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="M23.58.424A1,1,0,0,0,22.819.13C8.791.862,3.609,13.358,3.559,13.484a1,1,0,0,0,.22,1.08l5.657,5.657a1,1,0,0,0,1.085.218c.125-.051,12.554-5.291,13.348-19.253A1,1,0,0,0,23.58.424Zm-8.166,10.99a2,2,0,1,1,0-2.828A2,2,0,0,1,15.414,11.414Z" fill="#444444"/> <path data-color="color-2" d="M1.113,18.844a2.844,2.844,0,1,1,4.022,4.022C4.024,23.977,0,24,0,24S0,19.954,1.113,18.844Z" fill="#444444"/> <path id="color-2" d="M10.357,2.341A8.911,8.911,0,0,0,2.522,4.825a9.084,9.084,0,0,0-1.384,1.8,1,1,0,0,0,.155,1.215l1.989,1.99A26.623,26.623,0,0,1,10.357,2.341Z" fill="#444444"/> <path id="color-3" d="M21.659,13.643a8.911,8.911,0,0,1-2.484,7.835,9.084,9.084,0,0,1-1.8,1.384,1,1,0,0,1-1.215-.155l-1.99-1.989A26.623,26.623,0,0,0,21.659,13.643Z" fill="#444444"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 949 B |
@@ -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="M8,0C3.6,0,0,3.6,0,8c0,4.4,3.6,8,8,8s8-3.6,8-8C16,3.6,12.4,0,8,0z M8,10c-1.1,0-2-0.9-2-2c0-1.1,0.9-2,2-2 s2,0.9,2,2C10,9.1,9.1,10,8,10z M8,2c0.9,0,1.8,0.2,2.6,0.6L9.044,4.156c-0.761-0.207-1.327-0.207-2.089,0L5.4,2.6 C6.2,2.2,7.1,2,8,2z M2,8c0-0.9,0.2-1.8,0.6-2.6l1.556,1.556c-0.207,0.761-0.207,1.327,0,2.089L2.6,10.6C2.2,9.8,2,8.9,2,8z M8,14 c-0.9,0-1.8-0.2-2.6-0.6l1.556-1.556c0.761,0.207,1.327,0.207,2.089,0L10.6,13.4C9.8,13.8,8.9,14,8,14z M13.4,10.6l-1.556-1.556 c0.207-0.761,0.207-1.327,0-2.089L13.4,5.4C13.8,6.2,14,7.1,14,8C14,8.9,13.8,9.8,13.4,10.6z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 776 B |
@@ -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="M6.5,11h11c3,0,5.5-2.5,5.5-5.5S20.5,0,17.5,0h-11C3.5,0,1,2.5,1,5.5S3.5,11,6.5,11z M6.5,2 C8.4,2,10,3.6,10,5.5S8.4,9,6.5,9S3,7.4,3,5.5S4.6,2,6.5,2z"></path> <path data-color="color-2" fill="#444444" d="M17.5,13h-11c-3,0-5.5,2.5-5.5,5.5S3.5,24,6.5,24h11c3,0,5.5-2.5,5.5-5.5S20.5,13,17.5,13z M17.5,22c-1.9,0-3.5-1.6-3.5-3.5s1.6-3.5,3.5-3.5s3.5,1.6,3.5,3.5S19.4,22,17.5,22z"></path></g></svg>
|
||||
|
Before Width: | Height: | Size: 596 B |
@@ -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="M11,12H1c-0.553,0-1-0.447-1-1V1c0-0.552,0.447-1,1-1h10c0.553,0,1,0.448,1,1v10C12,11.553,11.553,12,11,12z "></path> <path data-color="color-2" fill="#444444" d="M15,16H4v-2h10V4h2v11C16,15.553,15.553,16,15,16z"></path></g></svg>
|
||||
|
Before Width: | Height: | Size: 435 B |
|
Before Width: | Height: | Size: 298 KiB |
@@ -1,15 +0,0 @@
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ["babel-preset-expo"],
|
||||
plugins: [
|
||||
[
|
||||
"module-resolver",
|
||||
{
|
||||
extensions: [".tsx", ".ts", ".js", ".json"],
|
||||
},
|
||||
],
|
||||
"react-native-reanimated/plugin",
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from "galio-framework";
|
||||
|
||||
import argonTheme from "../constants/Theme";
|
||||
|
||||
class ArButton extends React.Component {
|
||||
render() {
|
||||
const { small, shadowless, children, color, style, fontSize, ...props } = this.props;
|
||||
|
||||
const colorStyle = color && argonTheme.COLORS[color.toUpperCase()];
|
||||
|
||||
const buttonStyles = [
|
||||
small && styles.smallButton,
|
||||
color && { backgroundColor: colorStyle },
|
||||
!shadowless && styles.shadow,
|
||||
{...style}
|
||||
];
|
||||
|
||||
return (
|
||||
<Button
|
||||
style={buttonStyles}
|
||||
shadowless
|
||||
textStyle={{ fontSize: fontSize || 12, fontWeight: '700' }}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArButton.propTypes = {
|
||||
small: PropTypes.bool,
|
||||
shadowless: PropTypes.bool,
|
||||
color: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.oneOf(['default', 'primary', 'secondary', 'info', 'error', 'success', 'warning'])
|
||||
])
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
smallButton: {
|
||||
width: 75,
|
||||
height: 28
|
||||
},
|
||||
shadow: {
|
||||
shadowColor: 'black',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowRadius: 4,
|
||||
shadowOpacity: 0.1,
|
||||
elevation: 2,
|
||||
},
|
||||
});
|
||||
|
||||
export default ArButton;
|
||||
@@ -1,141 +0,0 @@
|
||||
import React from "react";
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
StyleSheet,
|
||||
Image,
|
||||
TouchableWithoutFeedback
|
||||
} from "react-native";
|
||||
import { Block, Text, theme } from "galio-framework";
|
||||
|
||||
import { argonTheme } from "../constants";
|
||||
|
||||
const Card = ({
|
||||
item,
|
||||
horizontal,
|
||||
full,
|
||||
style,
|
||||
ctaColor,
|
||||
imageStyle,
|
||||
ctaRight
|
||||
}) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const imageStyles = [
|
||||
full ? styles.fullImage : styles.horizontalImage,
|
||||
imageStyle
|
||||
];
|
||||
const cardContainer = [styles.card, styles.shadow, style];
|
||||
const imgContainer = [
|
||||
styles.imageContainer,
|
||||
horizontal ? styles.horizontalStyles : styles.verticalStyles,
|
||||
styles.shadow
|
||||
];
|
||||
|
||||
return (
|
||||
<Block row={horizontal} card flex style={cardContainer}>
|
||||
<TouchableWithoutFeedback
|
||||
onPress={() => navigation.navigate("Product", { product: item })}
|
||||
>
|
||||
<Block flex style={imgContainer}>
|
||||
<Image source={{ uri: item.image }} style={imageStyles} />
|
||||
</Block>
|
||||
</TouchableWithoutFeedback>
|
||||
<TouchableWithoutFeedback
|
||||
onPress={() => navigation.navigate("Product", { product: item })}
|
||||
>
|
||||
<Block flex space="between" style={styles.cardDescription}>
|
||||
<Block flex>
|
||||
<Text
|
||||
style={{ fontFamily: 'open-sans-regular' }}
|
||||
size={14}
|
||||
style={styles.cardTitle}
|
||||
color={argonTheme.COLORS.TEXT}
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
{item.body ? (
|
||||
<Block flex left>
|
||||
<Text style={{ fontFamily: 'open-sans-regular' }} size={12} color={argonTheme.COLORS.TEXT}>
|
||||
{item.body}
|
||||
</Text>
|
||||
</Block>
|
||||
) : (
|
||||
<Block />
|
||||
)}
|
||||
</Block>
|
||||
<Block right={ctaRight ? true : false}>
|
||||
<Text
|
||||
style={{ fontFamily: 'open-sans-bold' }}
|
||||
size={12}
|
||||
muted={!ctaColor}
|
||||
color={ctaColor || argonTheme.COLORS.ACTIVE}
|
||||
bold
|
||||
>
|
||||
{item.cta}
|
||||
</Text>
|
||||
</Block>
|
||||
</Block>
|
||||
</TouchableWithoutFeedback>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
Card.propTypes = {
|
||||
item: PropTypes.object,
|
||||
horizontal: PropTypes.bool,
|
||||
full: PropTypes.bool,
|
||||
ctaColor: PropTypes.string,
|
||||
imageStyle: PropTypes.any,
|
||||
ctaRight: PropTypes.bool
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
backgroundColor: theme.COLORS.WHITE,
|
||||
marginVertical: theme.SIZES.BASE,
|
||||
borderWidth: 0,
|
||||
minHeight: 114,
|
||||
marginBottom: 4,
|
||||
},
|
||||
cardTitle: {
|
||||
// flex: 1,
|
||||
// flexWrap: "wrap",
|
||||
paddingBottom: 6
|
||||
},
|
||||
cardDescription: {
|
||||
padding: theme.SIZES.BASE / 2
|
||||
},
|
||||
imageContainer: {
|
||||
borderRadius: 3,
|
||||
elevation: 1,
|
||||
overflow: "hidden"
|
||||
},
|
||||
image: {
|
||||
// borderRadius: 3,
|
||||
},
|
||||
horizontalImage: {
|
||||
height: 122,
|
||||
width: "auto"
|
||||
},
|
||||
horizontalStyles: {
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0
|
||||
},
|
||||
verticalStyles: {
|
||||
borderBottomRightRadius: 0,
|
||||
borderBottomLeftRadius: 0
|
||||
},
|
||||
fullImage: {
|
||||
height: 215
|
||||
},
|
||||
shadow: {
|
||||
shadowColor: "#8898AA",
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowRadius: 6,
|
||||
shadowOpacity: 0.1,
|
||||
elevation: 2
|
||||
}
|
||||
});
|
||||
|
||||
export default Card;
|
||||
@@ -1,143 +0,0 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, TouchableOpacity, Linking } from "react-native";
|
||||
import { Block, Text, theme } from "galio-framework";
|
||||
|
||||
import Icon from "./Icon";
|
||||
import argonTheme from "../constants/Theme";
|
||||
|
||||
class DrawerItem extends React.Component {
|
||||
renderIcon = () => {
|
||||
const { title, focused } = this.props;
|
||||
|
||||
switch (title) {
|
||||
case "Home":
|
||||
return (
|
||||
<Icon
|
||||
name="shop"
|
||||
family="ArgonExtra"
|
||||
size={14}
|
||||
color={focused ? "white" : argonTheme.COLORS.PRIMARY}
|
||||
/>
|
||||
);
|
||||
case "Elements":
|
||||
return (
|
||||
<Icon
|
||||
name="map-big"
|
||||
family="ArgonExtra"
|
||||
size={14}
|
||||
color={focused ? "white" : argonTheme.COLORS.ERROR}
|
||||
/>
|
||||
);
|
||||
case "Articles":
|
||||
return (
|
||||
<Icon
|
||||
name="spaceship"
|
||||
family="ArgonExtra"
|
||||
size={14}
|
||||
color={focused ? "white" : argonTheme.COLORS.PRIMARY}
|
||||
/>
|
||||
);
|
||||
case "Profile":
|
||||
return (
|
||||
<Icon
|
||||
name="chart-pie-35"
|
||||
family="ArgonExtra"
|
||||
size={14}
|
||||
color={focused ? "white" : argonTheme.COLORS.WARNING}
|
||||
/>
|
||||
);
|
||||
case "Account":
|
||||
return (
|
||||
<Icon
|
||||
name="calendar-date"
|
||||
family="ArgonExtra"
|
||||
size={14}
|
||||
color={focused ? "white" : argonTheme.COLORS.INFO}
|
||||
/>
|
||||
);
|
||||
case "Settings":
|
||||
return (
|
||||
<Icon
|
||||
name="calendar-date"
|
||||
family="ArgonExtra"
|
||||
size={14}
|
||||
color={focused ? "white" : argonTheme.COLORS.DEFAULT}
|
||||
/>
|
||||
);
|
||||
case "Getting Started":
|
||||
return (
|
||||
<Icon
|
||||
name="spaceship"
|
||||
family="ArgonExtra"
|
||||
size={14}
|
||||
color={focused ? "white" : "rgba(0,0,0,0.5)"}
|
||||
/>
|
||||
);
|
||||
case "Log out":
|
||||
return <Icon />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { focused, title, navigation, navigateTo } = this.props;
|
||||
|
||||
const containerStyles = [
|
||||
styles.defaultStyle,
|
||||
focused ? [styles.activeStyle, styles.shadow] : null,
|
||||
];
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{ height: 60 }}
|
||||
onPress={() =>
|
||||
title == "Getting Started"
|
||||
? Linking.openURL(
|
||||
"https://demos.creative-tim.com/argon-pro-react-native/docs/"
|
||||
).catch((err) => console.error("An error occurred", err))
|
||||
: navigation.navigate(navigateTo)
|
||||
}
|
||||
>
|
||||
<Block flex row style={containerStyles}>
|
||||
<Block middle flex={0.1} style={{ marginRight: 5 }}>
|
||||
{this.renderIcon()}
|
||||
</Block>
|
||||
<Block row center flex={0.9}>
|
||||
<Text
|
||||
style={{ fontFamily: "open-sans-regular" }}
|
||||
size={15}
|
||||
bold={focused ? true : false}
|
||||
color={focused ? "white" : "rgba(0,0,0,0.5)"}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</Block>
|
||||
</Block>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
defaultStyle: {
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: 2,
|
||||
},
|
||||
activeStyle: {
|
||||
backgroundColor: argonTheme.COLORS.ACTIVE,
|
||||
borderRadius: 4,
|
||||
},
|
||||
shadow: {
|
||||
shadowColor: theme.COLORS.BLACK,
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowRadius: 8,
|
||||
shadowOpacity: 0.1,
|
||||
},
|
||||
});
|
||||
|
||||
export default DrawerItem;
|
||||
@@ -1,255 +0,0 @@
|
||||
import React from 'react';
|
||||
import { TouchableOpacity, StyleSheet, Platform, Dimensions, Keyboard } from 'react-native';
|
||||
import { Button, Block, NavBar, Text, theme } from 'galio-framework';
|
||||
import { CommonActions } from '@react-navigation/native';
|
||||
|
||||
import Icon from './Icon';
|
||||
import Input from './Input';
|
||||
import Tabs from './Tabs';
|
||||
import argonTheme from '../constants/Theme';
|
||||
|
||||
const { height, width } = Dimensions.get('window');
|
||||
const iPhoneX = () => Platform.OS === 'ios' && (height === 812 || width === 812 || height === 896 || width === 896);
|
||||
|
||||
const BellButton = ({isWhite, style, navigation}) => (
|
||||
<TouchableOpacity style={[styles.button, style]} onPress={() => navigation.navigate('Notifications')}>
|
||||
<Icon
|
||||
family="ArgonExtra"
|
||||
size={16}
|
||||
name="bell"
|
||||
color={argonTheme.COLORS[isWhite ? 'WHITE' : 'ICON']}
|
||||
/>
|
||||
<Block middle style={styles.notify} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const BasketButton = ({isWhite, style, navigation}) => (
|
||||
<TouchableOpacity style={[styles.button, style]} onPress={() => navigation.navigate('Cart')}>
|
||||
<Icon
|
||||
family="ArgonExtra"
|
||||
size={16}
|
||||
name="basket"
|
||||
color={argonTheme.COLORS[isWhite ? 'WHITE' : 'ICON']}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const SearchButton = ({isWhite, style, navigation}) => (
|
||||
<TouchableOpacity style={[styles.button, style]} onPress={() => navigation.navigate('Search')}>
|
||||
<Icon
|
||||
size={16}
|
||||
family="Galio"
|
||||
name="search-zoom-in"
|
||||
color={theme.COLORS[isWhite ? 'WHITE' : 'ICON']}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
class Header extends React.Component {
|
||||
handleLeftPress = () => {
|
||||
const { back, navigation, scene } = this.props;
|
||||
return (back ? navigation.dispatch(CommonActions.goBack()) : navigation.openDrawer());
|
||||
}
|
||||
renderRight = () => {
|
||||
const { white, title, navigation } = this.props;
|
||||
// const { routeName } = navigation.state;
|
||||
|
||||
if (title === 'Title') {
|
||||
return [
|
||||
<BellButton key='chat-title' navigation={navigation} isWhite={white} />,
|
||||
<BasketButton key='basket-title' navigation={navigation} isWhite={white} />
|
||||
]
|
||||
}
|
||||
|
||||
switch (title) {
|
||||
case 'Home':
|
||||
case 'Deals':
|
||||
case 'Categories':
|
||||
case 'Category':
|
||||
case 'Profile':
|
||||
case 'Product':
|
||||
case 'Search':
|
||||
case 'Settings':
|
||||
return ([
|
||||
<BellButton key='chat-categories' navigation={navigation} isWhite={white}/>,
|
||||
<BasketButton key='basket-categories' navigation={navigation} isWhite={white}/>
|
||||
]);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
renderSearch = () => {
|
||||
const { navigation } = this.props;
|
||||
return (
|
||||
<Input
|
||||
right
|
||||
color="black"
|
||||
style={styles.search}
|
||||
placeholder="What are you looking for?"
|
||||
placeholderTextColor={'#8898AA'}
|
||||
onFocus={() => {Keyboard.dismiss(); navigation.navigate('Search');}}
|
||||
iconContent={<Icon size={16} color={theme.COLORS.MUTED} name="search-zoom-in" family="ArgonExtra" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
renderOptions = () => {
|
||||
const { navigation, optionLeft, optionRight } = this.props;
|
||||
|
||||
return (
|
||||
<Block row style={styles.options}>
|
||||
<Button shadowless style={[styles.tab, styles.divider]} onPress={() => navigation.navigate('Beauty')}>
|
||||
<Block row middle>
|
||||
<Icon name="diamond" family="ArgonExtra" style={{ paddingRight: 8 }} color={argonTheme.COLORS.ICON} />
|
||||
<Text style={{ fontFamily: 'open-sans-regular' }} size={16} style={styles.tabTitle}>{optionLeft || 'Beauty'}</Text>
|
||||
</Block>
|
||||
</Button>
|
||||
<Button shadowless style={styles.tab} onPress={() => navigation.navigate('Fashion')}>
|
||||
<Block row middle>
|
||||
<Icon size={16} name="bag-17" family="ArgonExtra" style={{ paddingRight: 8 }} color={argonTheme.COLORS.ICON}/>
|
||||
<Text style={{ fontFamily: 'open-sans-regular' }} size={16} style={styles.tabTitle}>{optionRight || 'Fashion'}</Text>
|
||||
</Block>
|
||||
</Button>
|
||||
</Block>
|
||||
);
|
||||
}
|
||||
renderTabs = () => {
|
||||
const { tabs, tabIndex, navigation } = this.props;
|
||||
const defaultTab = tabs && tabs[0] && tabs[0].id;
|
||||
|
||||
if (!tabs) return null;
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
data={tabs || []}
|
||||
initialIndex={tabIndex || defaultTab}
|
||||
onChange={id => navigation.setParams({ tabId: id })} />
|
||||
)
|
||||
}
|
||||
renderHeader = () => {
|
||||
const { search, options, tabs } = this.props;
|
||||
if (search || tabs || options) {
|
||||
return (
|
||||
<Block center>
|
||||
{search ? this.renderSearch() : null}
|
||||
{options ? this.renderOptions() : null}
|
||||
{tabs ? this.renderTabs() : null}
|
||||
</Block>
|
||||
);
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const { back, title, white, transparent, bgColor, iconColor, titleColor, navigation, ...props } = this.props;
|
||||
// const { routeName } = navigation.state;
|
||||
const noShadow = ['Search', 'Categories', 'Deals', 'Pro', 'Profile'].includes(title);
|
||||
const headerStyles = [
|
||||
!noShadow ? styles.shadow : null,
|
||||
transparent ? { backgroundColor: 'rgba(0,0,0,0)' } : null,
|
||||
];
|
||||
|
||||
const navbarStyles = [
|
||||
styles.navbar,
|
||||
bgColor && { backgroundColor: bgColor }
|
||||
];
|
||||
|
||||
return (
|
||||
<Block style={headerStyles}>
|
||||
<NavBar
|
||||
back={false}
|
||||
title={title}
|
||||
style={navbarStyles}
|
||||
transparent={transparent}
|
||||
right={this.renderRight()}
|
||||
rightStyle={{ alignItems: 'center' }}
|
||||
onLeftPress={this.handleLeftPress}
|
||||
left={
|
||||
<Icon
|
||||
name={back ? 'chevron-left' : "menu"} family="entypo"
|
||||
// name={back ? 'nav-left' : "menu-8"} family="ArgonExtra"
|
||||
size={back ? 20 : 20} onPress={this.handleLeftPress}
|
||||
color={iconColor || (white ? argonTheme.COLORS.WHITE : argonTheme.COLORS.ICON)}
|
||||
style={{ marginTop: 2 }}
|
||||
/>
|
||||
}
|
||||
leftStyle={{ paddingVertical: 12, flex: 0.2 }}
|
||||
titleStyle={[
|
||||
styles.title,
|
||||
{ color: argonTheme.COLORS[white ? 'WHITE' : 'HEADER'] },
|
||||
titleColor && { color: titleColor }
|
||||
]}
|
||||
{...props}
|
||||
/>
|
||||
{this.renderHeader()}
|
||||
</Block>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
padding: 12,
|
||||
position: 'relative',
|
||||
},
|
||||
title: {
|
||||
width: '100%',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
navbar: {
|
||||
paddingVertical: 0,
|
||||
paddingBottom: theme.SIZES.BASE * 1.5,
|
||||
paddingTop: iPhoneX ? theme.SIZES.BASE * 4 : theme.SIZES.BASE,
|
||||
zIndex: 5,
|
||||
},
|
||||
shadow: {
|
||||
backgroundColor: theme.COLORS.WHITE,
|
||||
shadowColor: 'black',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowRadius: 6,
|
||||
shadowOpacity: 0.2,
|
||||
elevation: 3,
|
||||
},
|
||||
notify: {
|
||||
backgroundColor: argonTheme.COLORS.LABEL,
|
||||
borderRadius: 4,
|
||||
height: theme.SIZES.BASE / 2,
|
||||
width: theme.SIZES.BASE / 2,
|
||||
position: 'absolute',
|
||||
top: 9,
|
||||
right: 12,
|
||||
},
|
||||
header: {
|
||||
backgroundColor: theme.COLORS.WHITE,
|
||||
},
|
||||
divider: {
|
||||
borderRightWidth: 0.3,
|
||||
borderRightColor: theme.COLORS.ICON,
|
||||
},
|
||||
search: {
|
||||
height: 48,
|
||||
width: width - 32,
|
||||
marginHorizontal: 16,
|
||||
borderWidth: 1,
|
||||
borderRadius: 3,
|
||||
borderColor: argonTheme.COLORS.BORDER
|
||||
},
|
||||
options: {
|
||||
marginBottom: 24,
|
||||
marginTop: 10,
|
||||
elevation: 4,
|
||||
},
|
||||
tab: {
|
||||
backgroundColor: theme.COLORS.TRANSPARENT,
|
||||
width: width * 0.35,
|
||||
borderRadius: 0,
|
||||
borderWidth: 0,
|
||||
height: 24,
|
||||
elevation: 0,
|
||||
},
|
||||
tabTitle: {
|
||||
lineHeight: 19,
|
||||
fontWeight: '400',
|
||||
color: argonTheme.COLORS.HEADER
|
||||
},
|
||||
});
|
||||
|
||||
export default Header;
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
import * as Font from 'expo-font';
|
||||
import { createIconSetFromIcoMoon } from '@expo/vector-icons';
|
||||
import { Icon } from 'galio-framework';
|
||||
|
||||
import argonConfig from '../assets/config/argon.json';
|
||||
const ArgonExtra = require('../assets/font/argon.ttf');
|
||||
const IconArgonExtra = createIconSetFromIcoMoon(argonConfig, 'ArgonExtra');
|
||||
|
||||
class IconExtra extends React.Component {
|
||||
state = {
|
||||
fontLoaded: false,
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
await Font.loadAsync({ ArgonExtra: ArgonExtra });
|
||||
this.setState({ fontLoaded: true });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { name, family, ...rest } = this.props;
|
||||
|
||||
if (name && family && this.state.fontLoaded) {
|
||||
if (family === 'ArgonExtra') {
|
||||
return <IconArgonExtra name={name} family={family} {...rest} />;
|
||||
}
|
||||
return <Icon name={name} family={family} {...rest} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default IconExtra;
|
||||
@@ -1,76 +0,0 @@
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Input } from "galio-framework";
|
||||
|
||||
import Icon from './Icon';
|
||||
import { argonTheme } from "../constants";
|
||||
|
||||
class ArInput extends React.Component {
|
||||
render() {
|
||||
const { shadowless, success, error } = this.props;
|
||||
|
||||
const inputStyles = [
|
||||
styles.input,
|
||||
!shadowless && styles.shadow,
|
||||
success && styles.success,
|
||||
error && styles.error,
|
||||
{...this.props.style}
|
||||
];
|
||||
|
||||
return (
|
||||
<Input
|
||||
placeholder="write something here"
|
||||
placeholderTextColor={argonTheme.COLORS.MUTED}
|
||||
style={inputStyles}
|
||||
color={argonTheme.COLORS.HEADER}
|
||||
iconContent={
|
||||
<Icon
|
||||
size={14}
|
||||
color={argonTheme.COLORS.ICON}
|
||||
name="link"
|
||||
family="AntDesign"
|
||||
/>
|
||||
}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArInput.defaultProps = {
|
||||
shadowless: false,
|
||||
success: false,
|
||||
error: false
|
||||
};
|
||||
|
||||
ArInput.propTypes = {
|
||||
shadowless: PropTypes.bool,
|
||||
success: PropTypes.bool,
|
||||
error: PropTypes.bool
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
input: {
|
||||
borderRadius: 4,
|
||||
borderColor: argonTheme.COLORS.BORDER,
|
||||
height: 44,
|
||||
backgroundColor: '#FFFFFF'
|
||||
},
|
||||
success: {
|
||||
borderColor: argonTheme.COLORS.INPUT_SUCCESS,
|
||||
},
|
||||
error: {
|
||||
borderColor: argonTheme.COLORS.INPUT_ERROR,
|
||||
},
|
||||
shadow: {
|
||||
shadowColor: argonTheme.COLORS.BLACK,
|
||||
shadowOffset: { width: 0, height: 0.5 },
|
||||
shadowRadius: 1,
|
||||
shadowOpacity: 0.13,
|
||||
elevation: 2,
|
||||
}
|
||||
});
|
||||
|
||||
export default ArInput;
|
||||
@@ -1,157 +0,0 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, TouchableWithoutFeedback } from "react-native";
|
||||
import PropTypes from "prop-types";
|
||||
import { Block, Text } from "galio-framework";
|
||||
import Icon from "./Icon";
|
||||
import { argonTheme } from "../constants";
|
||||
|
||||
export default class Notification extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
body,
|
||||
color,
|
||||
iconColor,
|
||||
iconFamily,
|
||||
iconName,
|
||||
iconSize,
|
||||
onPress,
|
||||
style,
|
||||
system,
|
||||
time,
|
||||
title,
|
||||
transparent
|
||||
} = this.props;
|
||||
|
||||
const iconContainer = [
|
||||
styles.iconContainer,
|
||||
{ backgroundColor: color || argonTheme.COLORS.PRIMARY },
|
||||
system && { width: 34, height: 34 },
|
||||
!system && styles.iconShadow
|
||||
];
|
||||
|
||||
const container = [
|
||||
styles.card,
|
||||
!transparent && { backgroundColor: argonTheme.COLORS.WHITE },
|
||||
!transparent && styles.cardShadow,
|
||||
system && { height: 78 },
|
||||
style
|
||||
];
|
||||
return (
|
||||
<Block style={container} middle>
|
||||
<TouchableWithoutFeedback onPress={onPress}>
|
||||
<Block row style={{ width: "95%" }}>
|
||||
<Block top flex={system ? 0.12 : 0.2} middle>
|
||||
<Block middle style={iconContainer}>
|
||||
<Icon
|
||||
name={iconName}
|
||||
family={iconFamily}
|
||||
size={iconSize || system ? 16 : 22}
|
||||
color={
|
||||
iconColor || system ? argonTheme.COLORS.DEFAULT : argonTheme.COLORS.WHITE
|
||||
}
|
||||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
<Block flex style={{ paddingRight: 3, paddingLeft: 12 }}>
|
||||
{system && (
|
||||
<Block row space="between" style={{ height: 18 }}>
|
||||
<Text color={argonTheme.COLORS.MUTED} style={{ fontFamily: 'open-sans-bold' }} size={13}>{title}</Text>
|
||||
<Block row style={{ marginTop: 3 }}>
|
||||
<Icon
|
||||
family="material-community"
|
||||
name="clock"
|
||||
size={12}
|
||||
color={argonTheme.COLORS.MUTED}
|
||||
/>
|
||||
<Text
|
||||
color={argonTheme.COLORS.MUTED}
|
||||
style={{
|
||||
fontFamily: "open-sans-regular",
|
||||
marginLeft: 3,
|
||||
marginTop: -3
|
||||
}}
|
||||
size={12}
|
||||
>
|
||||
{time}
|
||||
</Text>
|
||||
</Block>
|
||||
</Block>
|
||||
)}
|
||||
<Text
|
||||
color={argonTheme.COLORS.TEXT}
|
||||
size={system ? 13 : 14}
|
||||
style={{ fontFamily: system ? "open-sans-bold" : "open-sans-regular" }}
|
||||
>
|
||||
{body}
|
||||
</Text>
|
||||
</Block>
|
||||
{!system && (
|
||||
<Block row flex={0.2} style={{ marginTop: 3 }}>
|
||||
<Icon
|
||||
family="material-community"
|
||||
name="clock"
|
||||
size={12}
|
||||
color={argonTheme.COLORS.MUTED}
|
||||
/>
|
||||
<Text
|
||||
color={argonTheme.COLORS.MUTED}
|
||||
style={{
|
||||
fontFamily: "open-sans-regular",
|
||||
marginLeft: 3,
|
||||
marginTop: -2
|
||||
}}
|
||||
size={12}
|
||||
>
|
||||
{time}
|
||||
</Text>
|
||||
</Block>
|
||||
)}
|
||||
</Block>
|
||||
</TouchableWithoutFeedback>
|
||||
</Block>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Notification.propTypes = {
|
||||
body: PropTypes.string,
|
||||
color: PropTypes.string,
|
||||
iconColor: PropTypes.string,
|
||||
iconFamily: PropTypes.string,
|
||||
iconName: PropTypes.string,
|
||||
iconSize: PropTypes.number,
|
||||
onPress: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
system: PropTypes.bool,
|
||||
time: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
transparent: PropTypes.bool,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
iconContainer: {
|
||||
width: 46,
|
||||
height: 46,
|
||||
borderRadius: 23,
|
||||
marginTop: 2
|
||||
},
|
||||
iconShadow: {
|
||||
shadowColor: "black",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowRadius: 4,
|
||||
shadowOpacity: 0.1,
|
||||
elevation: 2
|
||||
},
|
||||
card: {
|
||||
zIndex: 2,
|
||||
height: 127,
|
||||
borderRadius: 6
|
||||
},
|
||||
cardShadow: {
|
||||
shadowColor: argonTheme.COLORS.BLACK,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowRadius: 4,
|
||||
shadowOpacity: 0.1,
|
||||
elevation: 2
|
||||
}
|
||||
});
|
||||
@@ -1,85 +0,0 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import ModalDropdown from 'react-native-modal-dropdown';
|
||||
import { Block, Text } from 'galio-framework';
|
||||
|
||||
import Icon from './Icon';
|
||||
import { argonTheme } from '../constants';
|
||||
|
||||
class DropDown extends React.Component {
|
||||
state = {
|
||||
value: 1,
|
||||
}
|
||||
|
||||
handleOnSelect = (index, value) => {
|
||||
const { onSelect } = this.props;
|
||||
|
||||
this.setState({ value: value });
|
||||
onSelect && onSelect(index, value);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onSelect, iconName, iconFamily, iconSize, iconColor, color, textStyle, style, ...props } = this.props;
|
||||
|
||||
const modalStyles = [
|
||||
styles.qty,
|
||||
color && { backgroundColor: color },
|
||||
style
|
||||
];
|
||||
|
||||
const textStyles = [
|
||||
styles.text,
|
||||
textStyle
|
||||
];
|
||||
|
||||
return (
|
||||
<ModalDropdown
|
||||
style={modalStyles}
|
||||
onSelect={this.handleOnSelect}
|
||||
dropdownStyle={styles.dropdown}
|
||||
dropdownTextStyle={{paddingLeft:16, fontSize:12}}
|
||||
{...props}>
|
||||
<Block flex row middle space="between">
|
||||
<Text size={12} style={textStyles}>{this.state.value}</Text>
|
||||
<Icon name={iconName || "nav-down"} family={iconFamily || "ArgonExtra"} size={iconSize || 10} color={iconColor || argonTheme.COLORS.WHITE} />
|
||||
</Block>
|
||||
</ModalDropdown>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropDown.propTypes = {
|
||||
onSelect: PropTypes.func,
|
||||
iconName: PropTypes.string,
|
||||
iconFamily: PropTypes.string,
|
||||
iconSize: PropTypes.number,
|
||||
color: PropTypes.string,
|
||||
textStyle: PropTypes.any,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
qty: {
|
||||
width: 100,
|
||||
backgroundColor: argonTheme.COLORS.DEFAULT,
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 10,
|
||||
paddingBottom:9.5,
|
||||
borderRadius: 4,
|
||||
shadowColor: "rgba(0, 0, 0, 0.1)",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowRadius: 4,
|
||||
shadowOpacity: 1,
|
||||
},
|
||||
text: {
|
||||
color: argonTheme.COLORS.WHITE,
|
||||
fontWeight: '600'
|
||||
},
|
||||
dropdown: {
|
||||
marginTop: 8,
|
||||
marginLeft: -16,
|
||||
width: 100,
|
||||
},
|
||||
});
|
||||
|
||||
export default DropDown;
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Switch, Platform } from 'react-native';
|
||||
|
||||
import argonTheme from '../constants/Theme';
|
||||
|
||||
class MkSwitch extends React.Component {
|
||||
render() {
|
||||
const { value, ...props } = this.props;
|
||||
const thumbColor = Platform.OS === 'ios' ? null :
|
||||
Platform.OS === 'android' && value ? argonTheme.COLORS.SWITCH_ON : argonTheme.COLORS.SWITCH_OFF;
|
||||
|
||||
return (
|
||||
<Switch
|
||||
value={value}
|
||||
thumbColor={thumbColor}
|
||||
ios_backgroundColor={argonTheme.COLORS.SWITCH_OFF}
|
||||
trackColor={{ false: argonTheme.COLORS.SWITCH_ON, true: argonTheme.COLORS.SWITCH_ON }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MkSwitch;
|
||||