update pay ui
This commit is contained in:
24
.env.cos.example
Normal file
24
.env.cos.example
Normal file
@@ -0,0 +1,24 @@
|
||||
# ============================================================================
|
||||
# 腾讯云 COS 部署配置
|
||||
# 复制此文件为 .env.cos 并填写你的配置
|
||||
# ============================================================================
|
||||
|
||||
# 腾讯云 API 密钥
|
||||
# 获取地址: https://console.cloud.tencent.com/cam/capi
|
||||
# 建议创建子账号并只授予 COS 相关权限
|
||||
COS_SECRET_ID=你的SecretId
|
||||
COS_SECRET_KEY=你的SecretKey
|
||||
|
||||
# COS 存储桶配置
|
||||
# 存储桶名称(包含 APPID 后缀)
|
||||
# 例如: valuefrontier-static-1234567890
|
||||
COS_BUCKET=你的存储桶名称
|
||||
|
||||
# 存储桶地域
|
||||
# 例如: ap-shanghai, ap-guangzhou, ap-beijing
|
||||
COS_REGION=ap-shanghai
|
||||
|
||||
# CDN 加速域名(可选)
|
||||
# 配置后部署完成会显示访问地址
|
||||
# 例如: www.valuefrontier.cn
|
||||
CDN_DOMAIN=www.valuefrontier.cn
|
||||
@@ -14,7 +14,8 @@ REACT_APP_ENABLE_MOCK=false
|
||||
REACT_APP_ENABLE_DEBUG=false
|
||||
|
||||
# 后端 API 地址(生产环境)
|
||||
REACT_APP_API_URL=http://49.232.185.254:5001
|
||||
# 使用独立的 API 域名,因为静态资源托管在 COS + CDN
|
||||
REACT_APP_API_URL=https://api.valuefrontier.cn
|
||||
|
||||
# PostHog 分析配置(生产环境)
|
||||
# PostHog API Key(从 PostHog 项目设置中获取)
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -22,6 +22,10 @@ node_modules/
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# 部署配置(包含密钥,不提交)
|
||||
.env.cos
|
||||
.env.deploy
|
||||
|
||||
# 日志
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
||||
@@ -108,6 +108,9 @@
|
||||
"test": "craco test --env=jsdom",
|
||||
"eject": "react-scripts eject",
|
||||
"deploy": "bash scripts/deploy-from-local.sh",
|
||||
"deploy:cos": "node scripts/deploy-to-cos.js",
|
||||
"deploy:cos:clean": "node scripts/deploy-to-cos.js --clear",
|
||||
"deploy:cdn": "npm run build && npm run deploy:cos",
|
||||
"deploy:setup": "bash scripts/setup-deployment.sh",
|
||||
"rollback": "bash scripts/rollback-from-local.sh",
|
||||
"lint:check": "eslint . --ext=js,jsx,ts,tsx; exit 0",
|
||||
@@ -126,6 +129,7 @@
|
||||
"@typescript-eslint/parser": "^8.46.4",
|
||||
"ajv": "^8.17.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"cos-nodejs-sdk-v5": "^2.15.4",
|
||||
"env-cmd": "^11.0.0",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"eslint-plugin-prettier": "3.4.0",
|
||||
|
||||
357
scripts/deploy-to-cos.js
Normal file
357
scripts/deploy-to-cos.js
Normal file
@@ -0,0 +1,357 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 腾讯云 COS 部署脚本
|
||||
* 将构建产物上传到 COS 存储桶,配合 CDN 实现静态资源加速
|
||||
*
|
||||
* 使用方法:
|
||||
* node scripts/deploy-to-cos.js
|
||||
*
|
||||
* 配置文件:
|
||||
* .env.cos (从 .env.cos.example 复制并填写)
|
||||
*/
|
||||
|
||||
const COS = require('cos-nodejs-sdk-v5');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// ============================================================================
|
||||
// 配置加载
|
||||
// ============================================================================
|
||||
|
||||
// 加载 .env.cos 配置
|
||||
function loadConfig() {
|
||||
const envPath = path.join(__dirname, '..', '.env.cos');
|
||||
|
||||
if (!fs.existsSync(envPath)) {
|
||||
console.error('\x1b[31m[错误]\x1b[0m 配置文件不存在: .env.cos');
|
||||
console.log('\n请先创建配置文件:');
|
||||
console.log(' 1. 复制 .env.cos.example 为 .env.cos');
|
||||
console.log(' 2. 填写腾讯云 SecretId、SecretKey 等信息\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const envContent = fs.readFileSync(envPath, 'utf-8');
|
||||
const config = {};
|
||||
|
||||
envContent.split('\n').forEach(line => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && !trimmed.startsWith('#')) {
|
||||
const [key, ...valueParts] = trimmed.split('=');
|
||||
if (key) {
|
||||
config[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 验证必需配置
|
||||
const required = ['COS_SECRET_ID', 'COS_SECRET_KEY', 'COS_BUCKET', 'COS_REGION'];
|
||||
const missing = required.filter(key => !config[key]);
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.error('\x1b[31m[错误]\x1b[0m 配置不完整,缺少:', missing.join(', '));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// COS 客户端
|
||||
// ============================================================================
|
||||
|
||||
let cos = null;
|
||||
let config = null;
|
||||
|
||||
function initCOS() {
|
||||
config = loadConfig();
|
||||
|
||||
cos = new COS({
|
||||
SecretId: config.COS_SECRET_ID,
|
||||
SecretKey: config.COS_SECRET_KEY,
|
||||
});
|
||||
|
||||
console.log('\x1b[32m[✓]\x1b[0m COS 客户端初始化成功');
|
||||
console.log(` 存储桶: ${config.COS_BUCKET}`);
|
||||
console.log(` 地域: ${config.COS_REGION}`);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 文件操作
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 递归获取目录下所有文件
|
||||
*/
|
||||
function getAllFiles(dirPath, arrayOfFiles = []) {
|
||||
const files = fs.readdirSync(dirPath);
|
||||
|
||||
files.forEach(file => {
|
||||
const fullPath = path.join(dirPath, file);
|
||||
if (fs.statSync(fullPath).isDirectory()) {
|
||||
getAllFiles(fullPath, arrayOfFiles);
|
||||
} else {
|
||||
arrayOfFiles.push(fullPath);
|
||||
}
|
||||
});
|
||||
|
||||
return arrayOfFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件的 Content-Type
|
||||
*/
|
||||
function getContentType(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const mimeTypes = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.js': 'application/javascript; charset=utf-8',
|
||||
'.json': 'application/json; charset=utf-8',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
'.webp': 'image/webp',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
'.ttf': 'font/ttf',
|
||||
'.eot': 'application/vnd.ms-fontobject',
|
||||
'.map': 'application/json',
|
||||
'.txt': 'text/plain; charset=utf-8',
|
||||
'.xml': 'application/xml',
|
||||
'.webmanifest': 'application/manifest+json',
|
||||
};
|
||||
|
||||
return mimeTypes[ext] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存控制头
|
||||
*/
|
||||
function getCacheControl(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
// HTML 文件不缓存(或短缓存)
|
||||
if (ext === '.html') {
|
||||
return 'no-cache, no-store, must-revalidate';
|
||||
}
|
||||
|
||||
// 带 hash 的静态资源长期缓存(JS/CSS 构建产物)
|
||||
// 文件名格式: main.abc123.js, styles.def456.css
|
||||
if (/\.[a-f0-9]{8,}\.(js|css)$/.test(fileName)) {
|
||||
return 'public, max-age=31536000, immutable'; // 1 年
|
||||
}
|
||||
|
||||
// 图片、字体缓存 30 天
|
||||
if (['.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.webp', '.woff', '.woff2', '.ttf', '.eot'].includes(ext)) {
|
||||
return 'public, max-age=2592000'; // 30 天
|
||||
}
|
||||
|
||||
// 其他文件缓存 1 天
|
||||
return 'public, max-age=86400';
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 上传功能
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 上传单个文件到 COS
|
||||
*/
|
||||
function uploadFile(localPath, remotePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const contentType = getContentType(localPath);
|
||||
const cacheControl = getCacheControl(localPath);
|
||||
|
||||
cos.putObject({
|
||||
Bucket: config.COS_BUCKET,
|
||||
Region: config.COS_REGION,
|
||||
Key: remotePath,
|
||||
Body: fs.createReadStream(localPath),
|
||||
ContentType: contentType,
|
||||
CacheControl: cacheControl,
|
||||
}, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空存储桶(可选)
|
||||
*/
|
||||
async function clearBucket() {
|
||||
console.log('\n\x1b[36m[清理]\x1b[0m 清空存储桶旧文件...');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
cos.getBucket({
|
||||
Bucket: config.COS_BUCKET,
|
||||
Region: config.COS_REGION,
|
||||
MaxKeys: 1000,
|
||||
}, async (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.Contents || data.Contents.length === 0) {
|
||||
console.log(' 存储桶为空,无需清理');
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const objects = data.Contents.map(item => ({ Key: item.Key }));
|
||||
console.log(` 发现 ${objects.length} 个文件`);
|
||||
|
||||
cos.deleteMultipleObject({
|
||||
Bucket: config.COS_BUCKET,
|
||||
Region: config.COS_REGION,
|
||||
Objects: objects,
|
||||
}, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
console.log(` 已删除 ${objects.length} 个文件`);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传整个 build 目录
|
||||
*/
|
||||
async function uploadBuildDir() {
|
||||
const buildDir = path.join(__dirname, '..', 'build');
|
||||
|
||||
if (!fs.existsSync(buildDir)) {
|
||||
console.error('\x1b[31m[错误]\x1b[0m build 目录不存在,请先运行 npm run build');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const files = getAllFiles(buildDir);
|
||||
console.log(`\n\x1b[36m[上传]\x1b[0m 共 ${files.length} 个文件待上传...\n`);
|
||||
|
||||
let uploaded = 0;
|
||||
let failed = 0;
|
||||
const errors = [];
|
||||
|
||||
// 并发上传(限制并发数)
|
||||
const concurrency = 10;
|
||||
const chunks = [];
|
||||
for (let i = 0; i < files.length; i += concurrency) {
|
||||
chunks.push(files.slice(i, i + concurrency));
|
||||
}
|
||||
|
||||
for (const chunk of chunks) {
|
||||
await Promise.all(chunk.map(async (filePath) => {
|
||||
const relativePath = path.relative(buildDir, filePath).replace(/\\/g, '/');
|
||||
|
||||
try {
|
||||
await uploadFile(filePath, relativePath);
|
||||
uploaded++;
|
||||
|
||||
// 进度显示
|
||||
const progress = Math.round((uploaded + failed) / files.length * 100);
|
||||
process.stdout.write(`\r 进度: ${progress}% (${uploaded}/${files.length})`);
|
||||
} catch (err) {
|
||||
failed++;
|
||||
errors.push({ file: relativePath, error: err.message });
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
console.log('\n');
|
||||
|
||||
if (failed > 0) {
|
||||
console.log('\x1b[33m[警告]\x1b[0m 部分文件上传失败:');
|
||||
errors.forEach(({ file, error }) => {
|
||||
console.log(` - ${file}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`\x1b[32m[✓]\x1b[0m 上传完成: 成功 ${uploaded},失败 ${failed}`);
|
||||
|
||||
return { uploaded, failed };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CDN 刷新(可选)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 刷新 CDN 缓存
|
||||
* 注意:需要额外配置 CDN API 权限
|
||||
*/
|
||||
async function refreshCDN() {
|
||||
if (!config.CDN_DOMAIN) {
|
||||
console.log('\n\x1b[33m[提示]\x1b[0m 未配置 CDN 域名,跳过 CDN 刷新');
|
||||
console.log(' 如需自动刷新 CDN,请在 .env.cos 中配置 CDN_DOMAIN');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('\n\x1b[36m[CDN]\x1b[0m 刷新 CDN 缓存...');
|
||||
console.log(` 域名: ${config.CDN_DOMAIN}`);
|
||||
console.log('\n\x1b[33m[提示]\x1b[0m 由于文件名包含 hash,通常不需要手动刷新 CDN');
|
||||
console.log(' 如需刷新,请到腾讯云 CDN 控制台操作:');
|
||||
console.log(' https://console.cloud.tencent.com/cdn/refresh');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 主流程
|
||||
// ============================================================================
|
||||
|
||||
async function main() {
|
||||
console.log('\n╔════════════════════════════════════════════════════════════════╗');
|
||||
console.log('║ 腾讯云 COS 部署工具 - ValueFrontier ║');
|
||||
console.log('╚════════════════════════════════════════════════════════════════╝\n');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// 1. 初始化 COS 客户端
|
||||
initCOS();
|
||||
|
||||
// 2. 可选:清空存储桶
|
||||
const clearFirst = process.argv.includes('--clear');
|
||||
if (clearFirst) {
|
||||
await clearBucket();
|
||||
}
|
||||
|
||||
// 3. 上传文件
|
||||
const { uploaded, failed } = await uploadBuildDir();
|
||||
|
||||
// 4. CDN 刷新提示
|
||||
await refreshCDN();
|
||||
|
||||
// 5. 完成
|
||||
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
|
||||
console.log('\n╔════════════════════════════════════════════════════════════════╗');
|
||||
console.log('║ 🎉 部署成功! ║');
|
||||
console.log('╚════════════════════════════════════════════════════════════════╝');
|
||||
console.log(`\n 耗时: ${duration} 秒`);
|
||||
console.log(` 上传: ${uploaded} 个文件`);
|
||||
|
||||
if (config.CDN_DOMAIN) {
|
||||
console.log(`\n 访问地址: https://${config.CDN_DOMAIN}`);
|
||||
}
|
||||
|
||||
console.log('\n');
|
||||
|
||||
} catch (err) {
|
||||
console.error('\n\x1b[31m[错误]\x1b[0m 部署失败:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行
|
||||
main();
|
||||
105
scripts/nginx-api.conf.example
Normal file
105
scripts/nginx-api.conf.example
Normal file
@@ -0,0 +1,105 @@
|
||||
# ============================================================================
|
||||
# Nginx 配置 - API 服务器
|
||||
# 部署 CDN 后,Nginx 只需处理 API 请求
|
||||
#
|
||||
# 使用方法:
|
||||
# 1. 复制此文件到服务器: /etc/nginx/sites-available/api.valuefrontier.cn
|
||||
# 2. 修改 SSL 证书路径
|
||||
# 3. sudo ln -s /etc/nginx/sites-available/api.valuefrontier.cn /etc/nginx/sites-enabled/
|
||||
# 4. sudo nginx -t && sudo systemctl reload nginx
|
||||
# ============================================================================
|
||||
|
||||
# API 服务器配置
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name api.valuefrontier.cn;
|
||||
|
||||
# SSL 证书(需要为 api.valuefrontier.cn 申请证书)
|
||||
ssl_certificate /etc/nginx/ssl/api.valuefrontier.cn.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/api.valuefrontier.cn.key;
|
||||
|
||||
# SSL 配置
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 1d;
|
||||
|
||||
# CORS 配置(允许 CDN 域名访问)
|
||||
add_header 'Access-Control-Allow-Origin' 'https://www.valuefrontier.cn' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization, X-Requested-With' always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||
|
||||
# 处理 OPTIONS 预检请求
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' 'https://www.valuefrontier.cn' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization, X-Requested-With' always;
|
||||
add_header 'Access-Control-Max-Age' 86400;
|
||||
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
|
||||
# API 代理到 Flask
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket 支持
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# 超时配置
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# 健康检查端点
|
||||
location /health {
|
||||
return 200 'ok';
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTP 重定向到 HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name api.valuefrontier.cn;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# 可选:如果你想保留原来的 www 域名也指向服务器(作为备用)
|
||||
# 取消下面的注释
|
||||
# ============================================================================
|
||||
# server {
|
||||
# listen 443 ssl http2;
|
||||
# server_name www.valuefrontier.cn valuefrontier.cn;
|
||||
#
|
||||
# # SSL 证书
|
||||
# ssl_certificate /etc/nginx/ssl/valuefrontier.cn.pem;
|
||||
# ssl_certificate_key /etc/nginx/ssl/valuefrontier.cn.key;
|
||||
#
|
||||
# root /var/www/valuefrontier.cn;
|
||||
# index index.html;
|
||||
#
|
||||
# # SPA 路由
|
||||
# location / {
|
||||
# try_files $uri $uri/ /index.html;
|
||||
# }
|
||||
#
|
||||
# # API 代理
|
||||
# location /api {
|
||||
# proxy_pass http://127.0.0.1:5001;
|
||||
# # ... 其他配置
|
||||
# }
|
||||
# }
|
||||
@@ -519,23 +519,34 @@ export default function SubscriptionContentNew() {
|
||||
console.log('[支付宝] 是否移动端跳转:', isMobileDevice);
|
||||
|
||||
if (isMobileDevice) {
|
||||
// 手机端:直接在当前页面跳转(可调起支付宝APP)
|
||||
toast({
|
||||
title: '订单创建成功',
|
||||
description: '正在跳转到支付宝...',
|
||||
status: 'success',
|
||||
duration: 2000,
|
||||
isClosable: true,
|
||||
});
|
||||
// 手机端:显示模态框,让用户手动点击跳转到支付宝
|
||||
// 原因:各种手机浏览器(Safari、华为、小米、微信内置等)对自动跳转有不同限制
|
||||
// 用户主动点击可以绑过这些限制,确保兼容性
|
||||
|
||||
// 保存订单信息到 sessionStorage,支付完成后返回时可以用来检查状态
|
||||
sessionStorage.setItem('alipay_order_id', data.data.id);
|
||||
sessionStorage.setItem('alipay_order_no', data.data.order_no);
|
||||
|
||||
// 延迟跳转,让用户看到提示
|
||||
setTimeout(() => {
|
||||
window.location.href = data.data.pay_url;
|
||||
}, 500);
|
||||
// 检测是否在微信内置浏览器中
|
||||
const isWechatBrowser = /MicroMessenger/i.test(navigator.userAgent);
|
||||
|
||||
// 显示模态框,让用户点击按钮跳转
|
||||
setPaymentOrder({
|
||||
...data.data,
|
||||
payment_method: 'alipay',
|
||||
is_mobile: true,
|
||||
is_wechat_browser: isWechatBrowser,
|
||||
});
|
||||
setPaymentCountdown(30 * 60);
|
||||
startAutoPaymentCheck(data.data.id, 'alipay');
|
||||
|
||||
toast({
|
||||
title: '订单创建成功',
|
||||
description: isWechatBrowser ? '请点击按钮,在浏览器中打开支付' : '请点击按钮打开支付宝',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
} else {
|
||||
// PC端:新窗口打开
|
||||
setPaymentOrder(data.data);
|
||||
@@ -1619,12 +1630,49 @@ export default function SubscriptionContentNew() {
|
||||
支付宝支付
|
||||
</Text>
|
||||
</HStack>
|
||||
{(paymentOrder as any).is_mobile ? (
|
||||
<>
|
||||
{(paymentOrder as any).is_wechat_browser ? (
|
||||
<>
|
||||
<Text color="orange.300" fontSize="sm" mb={2}>
|
||||
检测到您在微信中打开,请点击右上角「...」选择「在浏览器中打开」后再支付
|
||||
</Text>
|
||||
<Text color="rgba(255, 255, 255, 0.5)" fontSize="xs" mb={3}>
|
||||
或点击下方按钮尝试跳转
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text color="rgba(255, 255, 255, 0.7)" fontSize="sm" mb={3}>
|
||||
点击下方按钮打开支付宝完成支付
|
||||
</Text>
|
||||
)}
|
||||
<Button
|
||||
w="full"
|
||||
size="lg"
|
||||
bgGradient="linear-gradient(135deg, #1677FF, #0958d9)"
|
||||
color="white"
|
||||
fontWeight="bold"
|
||||
leftIcon={<Box as={AlipayCircleOutlined} fontSize="20px" />}
|
||||
onClick={() => {
|
||||
window.location.href = (paymentOrder as any).pay_url;
|
||||
}}
|
||||
_hover={{
|
||||
bgGradient: 'linear-gradient(135deg, #4096ff, #1677FF)',
|
||||
}}
|
||||
>
|
||||
打开支付宝付款
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text color="rgba(255, 255, 255, 0.7)" fontSize="sm">
|
||||
请在新打开的页面中完成支付
|
||||
</Text>
|
||||
<Text color="rgba(255, 255, 255, 0.5)" fontSize="xs" mt={1}>
|
||||
支付完成后点击下方按钮确认
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -17,19 +17,15 @@
|
||||
* const response = await fetch(getApiBase() + '/api/users');
|
||||
*/
|
||||
export const getApiBase = () => {
|
||||
// 生产环境使用相对路径
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 检查是否定义了 REACT_APP_API_URL(包括空字符串)
|
||||
// 使用 !== undefined 而不是 || 运算符,正确处理空字符串
|
||||
// 优先使用环境变量配置的 API URL
|
||||
// 生产环境配置为 https://api.valuefrontier.cn(CDN 部署后静态资源和 API 分离)
|
||||
// Mock 模式下配置为空字符串,让 MSW 拦截请求
|
||||
const apiUrl = process.env.REACT_APP_API_URL;
|
||||
if (apiUrl !== undefined) {
|
||||
return apiUrl; // Mock 模式下返回 '',其他情况返回配置的值
|
||||
return apiUrl;
|
||||
}
|
||||
|
||||
// 未配置时的默认后端地址
|
||||
// 未配置时的默认后端地址(开发环境)
|
||||
return 'http://49.232.185.254:5001';
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user