Merge branch 'develop' of http://25.13.9.101:9000/K17_AITS/tra-app into develop

This commit is contained in:
杨航
2025-11-12 17:36:50 +08:00
48 changed files with 2817 additions and 406 deletions
+1 -9
View File
@@ -2,12 +2,4 @@
ENV = 'development'
# 'development'
VITE_APP_BASE_API_Url = ''
VITE_APP_BASE_H5_API_Url = 'http://25.18.122.65:7001'
# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.91:9786'
# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.78:9786'
# VITE_APP_BASE_H5_API_Url_TRAAPP = 'http://192.168.247.200'
# VITE_APP_BASE_H5_API_Url_TRASTUDY = 'http://25.64.32.154:9602'
VITE_APP_BASE_H5_API_Url_TRAEXAM = ''http://25.64.32.154:9604'
# VITE_APP_BASE_H5_API_Url_TRAASK = 'http://25.64.32.154:9605'
VITE_APP_BASE_API_Url = ''
+6
View File
@@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgZfELkUrcpqqAVlQg
UfKNlPbySAnafYL+vEndTe9/xoqgCgYIKoZIzj0DAQehRANCAATBz1/RZd+1AEgU
VfnoB1PNUMynS8FQ/zJdTcZ25qEuu6WVkWb82Ltmb1lIRSZpRWEbpQzL+7X6zQ95
yjRmUksW
-----END PRIVATE KEY-----
+182
View File
@@ -0,0 +1,182 @@
#!/usr/bin/env python3
import os
import subprocess
import sys
import shutil
from datetime import datetime
# 颜色定义
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
NC = '\033[0m' # 无颜色
def run_command(command, check=True, capture_output=False):
"""执行系统命令并返回结果"""
try:
if capture_output:
result = subprocess.run(
command,
shell=True,
check=check,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
return result
else:
subprocess.run(command, shell=True, check=check)
return None
except subprocess.CalledProcessError as e:
print(f"{RED}❌ 命令执行失败: {e.stderr}{NC}")
sys.exit(1)
def main():
# 检查参数
if len(sys.argv) < 4:
print(f"{RED}错误:参数不足!{NC}")
print(f"使用方法:{YELLOW}{sys.argv[0]} <target_branch> <marketing_version> <current_project_version>{NC}")
print(f"示例:{YELLOW}{sys.argv[0]} sit 1.0.0 1{NC}")
sys.exit(1)
# 解析参数(第一个参数为target_branch,原参数顺延)
target_branch = sys.argv[1]
marketing_version = sys.argv[2]
current_project_version = sys.argv[3]
# ==============================================
# 配置参数 - 请根据实际情况修改以下参数
# ==============================================
# 钥匙串配置
keychain_password = "1234567" # 钥匙串密码
keychain_path = f"{os.path.expanduser('~')}/Library/Keychains/login.keychain-db" # 钥匙串路径
# 项目路径配置(可根据需要结合target_branch调整路径)
script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
parent_dir = os.path.dirname(script_dir)
work_dir = os.path.join(parent_dir, "build", target_branch, 'Ios','HBuilder-Hello')
project_dir = os.path.abspath(work_dir)
project_name = "HBuilder-Hello.xcodeproj" # Xcode项目文件名
scheme_name = "HBuilder" # 构建方案名称
configuration = "Release" # 构建配置
# 构建输出配置
build_dir = f"{os.path.expanduser('~')}/Desktop/lx_uniapp/build" # 构建输出目录
archive_name = "HBuilder.xcarchive" # 归档文件名
ipa_dir_name = f"{target_branch}_ipa_production" # IPA输出目录名(带分支名)
derived_data_dir = f"{os.path.expanduser('~')}/Library/Developer/Xcode/DerivedData/*" # 衍生数据目录
# 签名配置
code_sign_identity = "Apple Distribution: Bank of Jilin Co., Ltd. (2EP6SRRN43)" # 签名标识
provisioning_profile = "e3487fe1-19dc-4fc7-90df-11caf27da0c6" # 配置文件UUID
development_team = "2EP6SRRN43" # 开发团队ID
code_signing_required = "YES" # 是否需要签名
# 导出配置
export_options_plist = os.path.join(script_dir, "build_ios_config", "ExportOptions.plist")
# 转换为绝对路径(跨平台兼容)
export_options_plist = os.path.abspath(export_options_plist)
# ==============================================
# 显示参数信息
print(f"\n{BLUE}===== 构建参数信息 ====={NC}")
print(f"▶ 目标分支: {YELLOW}{target_branch}{NC}")
print(f"▶ 市场版本号: {YELLOW}{marketing_version}{NC}")
print(f"▶ 内部版本号: {YELLOW}{current_project_version}{NC}")
print(f"▶ 项目目录: {YELLOW}{project_dir}{NC}\n")
# 0. 解锁钥匙串并授予权限
print(f"{BLUE}🔑 解锁钥匙串...{NC}")
run_command(f"security unlock-keychain -p '{keychain_password}' '{keychain_path}'")
# 1. 进入项目目录
print(f"{BLUE}📂 进入项目目录: {project_dir}{NC}")
try:
os.chdir(project_dir)
except OSError as e:
print(f"{RED}❌ 无法进入项目目录: {e}{NC}")
sys.exit(1)
# 2. 清理旧构建
print(f"{BLUE}🧹 清理旧构建...{NC}")
run_command(
f"xcodebuild -project '{project_name}' "
f"-scheme '{scheme_name}' "
f"-configuration '{configuration}' clean"
)
# 3. 删除上次构建内容
print(f"{BLUE}🗑️ 删除上次构建内容...{NC}")
# 拼接路径
ipa_dir_path = f"{build_dir}/{ipa_dir_name}"
archive_path = f"{build_dir}/{ipa_dir_name}/{archive_name}"
# 删除文件/目录
for path in [ipa_dir_path, derived_data_dir]:
if os.path.exists(path) or '*' in path: # 处理通配符
run_command(f"rm -rf '{path}'", check=False)
# 4. 归档项目
print(f"{BLUE}📦 开始归档项目...{NC}")
run_command(
f"xcodebuild -project '{project_name}' "
f"-scheme '{scheme_name}' "
f"-configuration '{configuration}' "
f"-archivePath '{archive_path}' "
f"MARKETING_VERSION='{marketing_version}' "
f"CURRENT_PROJECT_VERSION='{current_project_version}' "
f"-destination 'generic/platform=iOS' "
f"CODE_SIGN_IDENTITY='{code_sign_identity}' "
f"PROVISIONING_PROFILE='{provisioning_profile}' "
f"DEVELOPMENT_TEAM='{development_team}' "
f"CODE_SIGNING_REQUIRED='{code_signing_required}' "
f"OTHER_CFLAGS='' "
f"OTHER_CPLUSPLUS_FLAGS='' "
f"OTHER_SWIFT_FLAGS='' "
f"archive"
)
# 5. 导出IPA文件
print(f"{BLUE}📤 导出IPA文件...{NC}")
run_command(
f"xcodebuild -exportArchive "
f"-archivePath '{archive_path}' "
f"-exportPath '{build_dir}/{ipa_dir_name}' "
f"-exportOptionsPlist '{export_options_plist}'"
)
print(f"\n{GREEN}🎉 构建完成!IPA文件已导出至: {build_dir}/{ipa_dir_name}{NC}")
# 6. 复制并重命名IPA文件到共享目录
print(f"{BLUE}📋 复制IPA文件到共享目录...{NC}")
# 定义共享目录路径
share_dir = "/Users/Shared/lx_uniapp_build_for_share"
# 确保共享目录存在(不存在则创建)
os.makedirs(share_dir, exist_ok=True)
# 获取当前时间(格式:YYYYMMDDHHMM
current_time = datetime.now().strftime("%Y%m%d%H%M")
# 构建新文件名(jax-<分支名>-<版本号>-<当前时间>.ipa
new_ipa_name = f"jax-{target_branch}-{current_project_version}-{current_time}.ipa"
# 源IPA文件路径(基于之前的导出路径)
source_ipa = os.path.join(build_dir, ipa_dir_name, "HBuilder.ipa")
# 目标IPA文件路径
target_ipa = os.path.join(share_dir, new_ipa_name)
# 复制并覆盖文件
try:
shutil.copy2(source_ipa, target_ipa) # copy2 会保留文件元数据
print(f"{GREEN}✅ IPA文件已复制至共享目录:{target_ipa}{NC}")
except FileNotFoundError:
print(f"{RED}❌ 源IPA文件不存在:{source_ipa}{NC}")
except Exception as e:
print(f"{RED}❌ 复制IPA文件失败:{str(e)}{NC}")
if __name__ == "__main__":
main()
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>HBuilder.ipa</key>
<array>
<dict>
<key>architectures</key>
<array>
<string>arm64</string>
</array>
<key>buildNumber</key>
<string>114</string>
<key>certificate</key>
<dict>
<key>SHA1</key>
<string>D24C7C1BFC6616660E24A8893EF27E23D1C6109F</string>
<key>dateExpires</key>
<string>2026/7/18</string>
<key>type</key>
<string>Apple Distribution</string>
</dict>
<key>entitlements</key>
<dict>
<key>application-identifier</key>
<string>2EP6SRRN43.cn.com.jlbank.aits</string>
<key>beta-reports-active</key>
<true/>
<key>com.apple.developer.team-identifier</key>
<string>2EP6SRRN43</string>
<key>get-task-allow</key>
<false/>
</dict>
<key>name</key>
<string>HBuilder.app</string>
<key>profile</key>
<dict>
<key>UUID</key>
<string>e3487fe1-19dc-4fc7-90df-11caf27da0c6</string>
<key>dateExpires</key>
<string>2026/7/18</string>
<key>name</key>
<string>jiaixue-pro</string>
</dict>
<key>symbols</key>
<true/>
<key>team</key>
<dict>
<key>id</key>
<string>2EP6SRRN43</string>
<key>name</key>
<string></string>
</dict>
<key>versionNumber</key>
<string>0.2.1</string>
</dict>
</array>
</dict>
</plist>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>destination</key>
<string>export</string>
<key>generateAppStoreInformation</key>
<false/>
<key>manageAppVersionAndBuildNumber</key>
<true/>
<key>method</key>
<string>app-store-connect</string>
<key>provisioningProfiles</key>
<dict>
<key>cn.com.jlbank.aits</key>
<string>e3487fe1-19dc-4fc7-90df-11caf27da0c6</string>
</dict>
<key>signingCertificate</key>
<string>Apple Distribution: Bank of Jilin Co., Ltd. (2EP6SRRN43)</string>
<key>signingStyle</key>
<string>manual</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>2EP6SRRN43</string>
<key>testFlightInternalTestingOnly</key>
<false/>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
+88
View File
@@ -0,0 +1,88 @@
#!/bin/bash
# 脚本名称:sync_remote_branch.sh
# 功能描述:修复origin/sit引用问题,强制同步远程分支到本地develop
# 使用方法:./sync_remote_branch.sh <远程分支名>
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# 检查参数
if [ $# -eq 0 ]; then
echo -e "${RED}错误:未传入分支名称参数!${NC}"
echo -e "使用方法:${YELLOW}$0 <远程分支名>${NC}"
exit 1
fi
echo -e "▶ 先设置内网网络\n"
sudo route add -host 25.13.9.101 25.64.32.254
# 配置参数
TARGET_BRANCH="$1"
WORK_DIR="Desktop/lx_uniapp/build/git_work_code"
REMOTE_NAME="origin"
LOCAL_BRANCH="develop"
echo -e "\n${YELLOW}===== 开始执行分支同步脚本 =====${NC}"
echo -e "▶ 远程目标分支:${YELLOW}${TARGET_BRANCH}${NC}"
echo -e "▶ 本地工作目录:${YELLOW}${WORK_DIR}${NC}\n"
# 1. 进入工作目录
echo -e "${YELLOW}1. 检查并进入工作目录...${NC}"
if [ ! -d "$WORK_DIR" ]; then
echo -e "${RED}错误:工作目录不存在!${NC}"
exit 1
fi
cd "$WORK_DIR" || {
echo -e "${RED}错误:无法进入工作目录!${NC}"
exit 1
}
echo -e "✅ 成功进入工作目录:$(pwd)\n"
# 2. 检查Git仓库
echo -e "${YELLOW}2. 检查Git仓库...${NC}"
if [ ! -d ".git" ]; then
echo -e "${RED}错误:当前目录不是Git仓库!${NC}"
exit 1
fi
echo -e "✅ 确认是Git仓库\n"
# 3. 关键修复:获取远程分支完整信息(不限制深度)
echo -e "${YELLOW}3. 完整获取远程所有分支完整信息...${NC}"
# 先移除可能存在的浅层克隆限制
git config --unset core.depth
# 完整拉取所有分支信息
if ! git fetch "$REMOTE_NAME" "+refs/heads/*:refs/remotes/$REMOTE_NAME/*"; then
echo -e "${RED}错误:拉取远程分支信息失败!${NC}"
exit 1
fi
echo -e "✅ 远程分支信息拉取完成\n"
# 4. 再次检查远程分支是否存在
echo -e "${YELLOW}4. 验证远程 ${TARGET_BRANCH} 分支...${NC}"
if ! git show-ref --verify --quiet "refs/remotes/$REMOTE_NAME/$TARGET_BRANCH"; then
echo -e "${RED}错误:远程确实不存在 ${TARGET_BRANCH} 分支!${NC}"
echo -e "远程所有分支列表:"
git branch -r
exit 1
fi
echo -e "✅ 确认远程存在 ${YELLOW}${TARGET_BRANCH}${NC} 分支\n"
# 5. 同步到本地develop分支
echo -e "${YELLOW}5. 同步远程 ${TARGET_BRANCH} 到本地 ${LOCAL_BRANCH}...${NC}"
echo -e "${YELLOW}⚠️ 警告:将覆盖本地develop分支所有内容!${NC}"
# 确保本地有develop分支
git checkout -B "$LOCAL_BRANCH" >/dev/null 2>&1
# 关键命令:使用完整引用路径
if ! git reset --hard "refs/remotes/$REMOTE_NAME/$TARGET_BRANCH"; then
echo -e "${RED}错误:同步远程分支到本地失败!${NC}"
exit 1
fi
# 6. 完成提示
echo -e "\n${GREEN}===== 操作完成 =====${NC}"
echo -e "✅ 本地 ${YELLOW}${LOCAL_BRANCH}${NC} 已同步为远程 ${YELLOW}${TARGET_BRANCH}${NC} 最新状态"
echo -e "当前版本:"
git log -1 --pretty=format:"%h - %an, %ar : %s"
+101
View File
@@ -0,0 +1,101 @@
#!/opt/miniconda3/bin/python3
import os
import sys
import socket
import http.server
import socketserver
import subprocess
from urllib.parse import urlparse
# 配置基础路径和端口(推荐使用 1024-65535 之间的端口,避免系统预留端口)
BUILD_DIR = os.path.join(os.path.expanduser('~'), 'Desktop', 'lx_uniapp', 'build')
DEFAULT_PORT = 8000 # 若仍失败,可改为 8080、9000 等
def check_port_available(port):
"""检查端口是否真的可用(比 lsof 更可靠)"""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind(("", port)) # 尝试绑定端口,成功则可用
return True
except OSError:
return False
def free_port(port):
"""释放占用端口的进程"""
try:
result = subprocess.check_output(
f"lsof -i :{port} | grep LISTEN | awk '{{print $2}}'",
shell=True,
text=True,
stderr=subprocess.STDOUT # 忽略错误输出
)
pids = [pid.strip() for pid in result.split('\n') if pid.strip()]
if pids:
print(f"释放端口 {port} 占用的进程:{pids}")
for pid in pids:
subprocess.run(f"kill -9 {pid}", shell=True, check=True)
except subprocess.CalledProcessError:
pass # 无进程占用时正常
except Exception as e:
print(f"释放端口警告:{e}(可忽略)")
class IPARequestHandler(http.server.SimpleHTTPRequestHandler):
# 省略与之前相同的 do_GET 实现(保持不变)
def do_GET(self):
parsed_path = urlparse(self.path)
target_branch = parsed_path.path.strip('/')
if not target_branch:
self.send_response(400)
self.end_headers()
self.wfile.write("请指定分支名,例如: http://服务器IP:端口/sit".encode('utf-8'))
return
ipa_dir_name = f"{target_branch}_ipa_production"
ipa_path = os.path.join(BUILD_DIR, ipa_dir_name, "HBuilder.ipa")
if not os.path.exists(ipa_path):
self.send_response(404)
self.end_headers()
self.wfile.write(f"文件不存在: {ipa_path}".encode('utf-8'))
return
self.send_response(200)
self.send_header('Content-Type', 'application/octet-stream')
self.send_header('Content-Disposition', f'attachment; filename="HBuilder_{target_branch}.ipa"')
self.end_headers()
with open(ipa_path, 'rb') as f:
self.wfile.write(f.read())
if __name__ == '__main__':
# 1. 先尝试释放端口
free_port(DEFAULT_PORT)
# 2. 检查端口是否真的可用
if not check_port_available(DEFAULT_PORT):
print(f"端口 {DEFAULT_PORT} 仍被占用,尝试自动切换端口...")
# 自动切换到 8080 端口(若 8000 不可用)
DEFAULT_PORT = 8080
free_port(DEFAULT_PORT)
if not check_port_available(DEFAULT_PORT):
print(f"端口 {DEFAULT_PORT} 也被占用,请手动指定其他端口(如 9000)")
sys.exit(1)
# 3. 启动服务(增加错误捕获)
try:
# 禁用地址重用限制(解决 TIME_WAIT 状态导致的启动失败)
socketserver.TCPServer.allow_reuse_address = True
with socketserver.TCPServer(("", DEFAULT_PORT), IPARequestHandler) as httpd:
print(f"\nIPA 文件服务已启动,端口: {DEFAULT_PORT}")
print(f"访问格式: http://服务器IP:{DEFAULT_PORT}/目标分支名")
print(f"示例: http://25.64.32.152:{DEFAULT_PORT}/sit")
httpd.serve_forever()
except Exception as e:
print(f"服务启动失败:{str(e)}")
print("可能原因:权限不足(尝试非 1-1024 端口)或网络配置限制")
sys.exit(1)
+402
View File
@@ -0,0 +1,402 @@
#!/usr/bin/env python3
import jwt
import time
import requests
import os
import sys
import datetime
from pathlib import Path
# ==============================================
# 重试配置参数(可按需修改)
# ==============================================
MAX_RETRIES = 10 # 查找构建的最大重试次数
RETRY_DELAY = 30 # 查找构建的重试间隔(秒)
SUBMIT_INIT_WAIT = 60 # 提交前初始等待时间(秒)
SUBMIT_MAX_RETRIES = 20 # 提交审核的最大重试次数
SUBMIT_BASE_DELAY = 30 # 提交审核的初始重试间隔(秒)
SUBMIT_DELAY_INCREMENT = 10 # 每次重试间隔递增(秒)
# ==============================================
# 其他配置参数
# ==============================================
def get_chinese_date_time():
"""
获取当前时间,以中文格式返回(如:8月5日 上午8点24分)
包含月、日、上午/下午、时、分,不包含秒数
"""
now = datetime.datetime.now()
month = now.month
day = now.day
hour_24 = now.hour
minute = now.minute
if hour_24 < 12:
period = "上午"
hour_12 = hour_24 if hour_24 != 0 else 12
else:
period = "下午"
hour_12 = hour_24 if hour_24 == 12 else hour_24 - 12
return f"{month}{day}{period}{hour_12}{minute}"
# 获取参数
if len(sys.argv) >= 2 and sys.argv[1]:
env_name = sys.argv[1]
else:
env_name = 'develop'
if len(sys.argv) >= 3 and sys.argv[2]:
build_number_str = sys.argv[2]
else:
build_number_str = '68'
# 验证第一个参数(枚举值)
valid_envs = ['develop', 'sit', 'uat']
if env_name not in valid_envs:
print(f"错误:环境名称 '{env_name}' 无效")
print(f"有效环境名称:{', '.join(valid_envs)}")
sys.exit(1)
# 验证第二个参数(数字字符串)
if not build_number_str.isdigit():
print(f"错误:构建号 '{build_number_str}' 必须是纯数字字符串")
sys.exit(1)
# 参数验证通过,继续处理
TARGET_BUILD_NUMBER = build_number_str
if env_name == 'develop':
BETA_GROUP_NAME = "外部自动化测试"
BETA_GROUP_NAME = "UAT"
TARGET_VERSION = "0.3.1"
elif env_name == 'sit':
BETA_GROUP_NAME = "SIT"
TARGET_VERSION = "0.2.1"
elif env_name == 'uat':
BETA_GROUP_NAME = "UAT"
TARGET_VERSION = "0.3.1"
# 接收参数结束
# 配置参数
API_KEY = "ACRB9XFL9G"
API_ISSUER = "69a6de7f-3378-47e3-e053-5b8c7c11a4d1"
APP_NAME = "吉AI学"
API_ENDPOINT = "https://api.appstoreconnect.apple.com/v1"
if 'PYCHARM_HOSTED' in os.environ:
Auth_Key_Path = './'
else:
Auth_Key_Path = '/Users/jlbank/Desktop/lx_uniapp/auto_build'
private_key_path = Path(f'{Auth_Key_Path}/AuthKey_{API_KEY}.p8')
def generate_jwt_token(api_key, api_issuer, private_key_path):
"""生成 JWT 令牌"""
with open(private_key_path, "r") as f:
private_key = f.read()
current_time = int(time.time())
payload = {
"iss": api_issuer,
"exp": current_time + 20 * 60,
"iat": current_time,
"aud": "appstoreconnect-v1"
}
token = jwt.encode(
payload,
private_key,
algorithm="ES256",
headers={"kid": api_key}
)
return token
def find_specific_build(app_id, headers):
"""通过预发布版本ID查找指定构建版本,带重试机制"""
retries = 0
while retries < MAX_RETRIES:
try:
print(f"\n🔍 查找版本 {TARGET_VERSION} 构建号 {TARGET_BUILD_NUMBER} (尝试 {retries + 1}/{MAX_RETRIES})")
print(" 正在获取预发布版本ID...")
pre_release_url = f"{API_ENDPOINT}/preReleaseVersions"
pre_release_params = {
"filter[version]": TARGET_VERSION,
"filter[app]": app_id
}
pre_release_response = requests.get(pre_release_url, headers=headers, params=pre_release_params)
pre_release_response.raise_for_status()
pre_release_data = pre_release_response.json()
if not pre_release_data["data"]:
print(f"❓ 未找到版本为 {TARGET_VERSION} 的预发布版本")
retries += 1
if retries < MAX_RETRIES:
time.sleep(RETRY_DELAY)
continue
pre_release_id = pre_release_data["data"][0]["id"]
print(f" 找到预发布版本ID: {pre_release_id}")
builds_url = f"{API_ENDPOINT}/builds"
params = {
"filter[preReleaseVersion]": pre_release_id,
"sort": "-version",
"limit": "10",
"include": "buildBetaDetail,betaBuildLocalizations,preReleaseVersion,betaAppReviewSubmission,icons,buildBundles,betaGroups",
"limit[betaBuildLocalizations]": "40",
"limit[betaGroups]": "50"
}
response = requests.get(builds_url, headers=headers, params=params)
if response.status_code == 400:
print(f" 错误响应内容: {response.text}")
response.raise_for_status()
builds_data = response.json()
matching_builds = [
b for b in builds_data["data"]
if b["attributes"].get("version") == TARGET_BUILD_NUMBER
]
if not matching_builds:
print(f"❓ 未找到版本 {TARGET_VERSION} 构建号 {TARGET_BUILD_NUMBER} 的构建")
else:
valid_build = next((
b for b in matching_builds
if b["attributes"]["processingState"] == "VALID"
), None)
if valid_build:
return valid_build
else:
states = [b["attributes"]["processingState"] for b in matching_builds]
print(f"⏳ 找到匹配的构建但状态为: {', '.join(states)},等待处理完成...")
if retries < MAX_RETRIES - 1:
print(f"将在 {RETRY_DELAY} 秒后重试...")
time.sleep(RETRY_DELAY)
retries += 1
except Exception as e:
print(f"查询过程中出错: {str(e)}")
retries += 1
if retries < MAX_RETRIES:
time.sleep(RETRY_DELAY)
return None
# ==============================================
# 新增:带重试的提交审核函数
# ==============================================
def submit_for_review_with_retry(build_id, headers):
"""提交审核(带初始等待和动态递增间隔重试)"""
# 第一步:初始等待
print(f"\n⌛ 提交审核前,先等待 {SUBMIT_INIT_WAIT} 秒(确保构建完全处理完成)...")
time.sleep(SUBMIT_INIT_WAIT)
# 第二步:重试提交
for retry in range(SUBMIT_MAX_RETRIES):
current_delay = SUBMIT_BASE_DELAY + (retry * SUBMIT_DELAY_INCREMENT)
try:
print(f"\n📋 提交构建版本审核 (尝试 {retry + 1}/{SUBMIT_MAX_RETRIES})")
print(f" 当前重试间隔:{current_delay}")
submit_url = f"{API_ENDPOINT}/betaAppReviewSubmissions"
submit_payload = {
"data": {
"type": "betaAppReviewSubmissions",
"relationships": {
"build": {
"data": {"id": build_id, "type": "builds"}
}
}
}
}
submit_response = requests.post(submit_url, headers=headers, json=submit_payload)
submit_response.raise_for_status()
print("✅ 构建版本已成功提交审核")
return True # 提交成功,退出函数
except requests.exceptions.HTTPError as e:
# 处理已提交的情况(409冲突)
if submit_response.status_code == 409:
print("⚠️ 构建已提交审核,无需重复操作")
return True
else:
print(f"❌ 第 {retry + 1} 次提交审核失败: {e}")
print("错误详情:", submit_response.json())
# 最后一次重试失败则退出
if retry == SUBMIT_MAX_RETRIES - 1:
print(f"\n❌ 已达到最大重试次数 ({SUBMIT_MAX_RETRIES}),提交审核失败")
return False
# 非最后一次重试,等待后继续
print(f" 将在 {current_delay} 秒后进行第 {retry + 2} 次重试...")
time.sleep(current_delay)
except Exception as e:
print(f"❌ 第 {retry + 1} 次提交审核异常: {str(e)}")
# 最后一次重试失败则退出
if retry == SUBMIT_MAX_RETRIES - 1:
print(f"\n❌ 已达到最大重试次数 ({SUBMIT_MAX_RETRIES}),提交审核失败")
return False
# 非最后一次重试,等待后继续
print(f" 将在 {current_delay} 秒后进行第 {retry + 2} 次重试...")
time.sleep(current_delay)
return False
def main():
try:
# 1. 生成 JWT 令牌
if not private_key_path.exists():
raise FileNotFoundError(f"私钥文件不存在: {private_key_path}")
token = generate_jwt_token(API_KEY, API_ISSUER, private_key_path)
print("✅ JWT 令牌生成成功")
# 2. 配置请求头
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
# 3. 获取应用 ID
print(f"\n🔍 查找应用: {APP_NAME}")
apps_url = f"{API_ENDPOINT}/apps"
response = requests.get(apps_url, headers=headers)
response.raise_for_status()
apps_data = response.json()
app = next((a for a in apps_data["data"] if a["attributes"]["name"] == APP_NAME), None)
if not app:
raise ValueError(f"未找到应用: {APP_NAME}")
app_id = app["id"]
print(f"✅ 找到应用 ID: {app_id}")
# 4. 获取指定版本和构建号的构建版本(带重试)
valid_build = find_specific_build(app_id, headers)
if not valid_build:
raise ValueError(
f"经过 {MAX_RETRIES} 次尝试后,仍未找到可用的构建版本: {TARGET_VERSION} ({TARGET_BUILD_NUMBER})")
build_id = valid_build["id"]
build_version = TARGET_VERSION
build_number = valid_build["attributes"]["version"]
print(f"✅ 找到构建版本 ID: {build_id} (版本号: {build_version}, 构建号: {build_number})")
# 5. 获取外部测试群组 ID
print(f"\n🔍 查找测试群组: {BETA_GROUP_NAME}")
groups_url = f"{API_ENDPOINT}/betaGroups"
response = requests.get(groups_url, headers=headers)
response.raise_for_status()
groups_data = response.json()
beta_group = next((g for g in groups_data["data"] if
g["attributes"]["name"] == BETA_GROUP_NAME and
g["attributes"]["isInternalGroup"] is False), None)
if not beta_group:
raise ValueError(f"未找到外部测试群组: {BETA_GROUP_NAME}")
beta_group_id = beta_group["id"]
print(f"✅ 找到外部测试群组 ID: {beta_group_id}")
# 6. 添加构建到测试群组后,更新测试内容(betaBuildLocalizations
print("\n📝 更新测试版本的本地化信息(whatsNew)...")
# 7. 将构建版本添加到测试群组
print(f"\n🔗 将构建 {build_id} 添加到测试群组 {beta_group_id}...")
add_build_url = f"{API_ENDPOINT}/betaGroups/{beta_group_id}/relationships/builds"
add_build_payload = {
"data": [{"id": build_id, "type": "builds"}]
}
add_response = requests.post(add_build_url, headers=headers, json=add_build_payload)
try:
add_response.raise_for_status()
except requests.exceptions.HTTPError as e:
if add_response.status_code == 409:
print("⚠️ 构建已在测试群组中,跳过添加步骤")
else:
print(f"❌ 添加构建到测试群组失败: {e}")
print("错误详情:", add_response.json())
sys.exit(1)
else:
print("✅ 构建已成功添加到测试群组")
# 8. 更新测试内容(whatsNew
get_localizations_url = f"{API_ENDPOINT}/builds/{build_id}/betaBuildLocalizations"
response = requests.get(get_localizations_url, headers=headers)
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
print(f"❌ 获取本地化信息失败: {e}")
print("错误详情:", response.json())
sys.exit(1)
localizations = response.json().get("data", [])
print("\n📝 更新测试版本说明...")
if localizations:
localization_id = localizations[0]["id"]
update_url = f"{API_ENDPOINT}/betaBuildLocalizations/{localization_id}"
print(f"使用现有本地化信息 ID: {localization_id}")
else:
print("创建新的本地化信息...")
create_url = f"{API_ENDPOINT}/betaBuildLocalizations"
create_payload = {
"data": {
"type": "betaBuildLocalizations",
"attributes": {"locale": "zh-Hans", "whatsNew": ""},
"relationships": {
"build": {"data": {"id": build_id, "type": "builds"}}
}
}
}
create_response = requests.post(create_url, headers=headers, json=create_payload)
create_response.raise_for_status()
localization_id = create_response.json()["data"]["id"]
update_url = f"{API_ENDPOINT}/betaBuildLocalizations/{localization_id}"
now_time_text = get_chinese_date_time()
update_payload = {
"data": {
"id": localization_id,
"type": "betaBuildLocalizations",
"attributes": {
"whatsNew": f"当前版本: {TARGET_BUILD_NUMBER}\n提交时间:{now_time_text}\n自动化提交"
}
}
}
update_response = requests.patch(update_url, headers=headers, json=update_payload)
update_response.raise_for_status()
print("✅ 测试版本说明已更新")
# 9. 提交审核(调用新增的带重试函数)
submit_success = submit_for_review_with_retry(build_id, headers)
if not submit_success:
sys.exit(1)
except Exception as e:
print(e)
exit(1)
if __name__ == "__main__":
main()
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
# ==============================================
# 配置参数 - 请根据实际情况修改以下参数
# ==============================================
# 钥匙串配置
KEYCHAIN_PASSWORD="1234567" # 钥匙串密码
KEYCHAIN_PATH="$HOME/Library/Keychains/login.keychain-db" # 钥匙串路径
# 项目路径配置
PROJECT_DIR="$HOME/Desktop/lx_uniapp/build/git_work_code/Ios/HBuilder-Hello" # 项目目录
PROJECT_NAME="HBuilder-Hello.xcodeproj" # Xcode项目文件名
SCHEME_NAME="HBuilder" # 构建方案名称
CONFIGURATION="Release" # 构建配置
# 构建输出配置
BUILD_DIR="$HOME/Desktop/lx_uniapp/build" # 构建输出目录
ARCHIVE_NAME="HBuilder.xcarchive" # 归档文件名
IPA_DIR_NAME="ipa_production" # IPA输出目录名
DERIVED_DATA_DIR="$HOME/Library/Developer/Xcode/DerivedData/*" # 衍生数据目录
# 版本配置
MARKETING_VERSION="$1" # 市场版本号
CURRENT_PROJECT_VERSION="$2" # 项目内部版本号
# 签名配置
CODE_SIGN_IDENTITY="Apple Distribution: Bank of Jilin Co., Ltd. (2EP6SRRN43)" # 签名标识
PROVISIONING_PROFILE="e3487fe1-19dc-4fc7-90df-11caf27da0c6" # 配置文件UUID
DEVELOPMENT_TEAM="2EP6SRRN43" # 开发团队ID
CODE_SIGNING_REQUIRED="YES" # 是否需要签名
# 导出配置
EXPORT_OPTIONS_PLIST="$BUILD_DIR/ExportOptions_Production.plist" # 导出配置plist路径
# ==============================================
# 错误处理函数
error_exit() {
echo "$1"
exit 1
}
# 0. 解锁钥匙串并授予权限
echo "🔑 解锁钥匙串..."
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" || {
error_exit "钥匙串解锁失败"
}
# 1. 进入项目目录
echo "📂 进入项目目录: $PROJECT_DIR"
cd "$PROJECT_DIR" || {
error_exit "无法进入项目目录: $PROJECT_DIR"
}
# 2. 清理旧构建
echo "🧹 清理旧构建..."
xcodebuild -project "$PROJECT_NAME" -scheme "$SCHEME_NAME" -configuration "$CONFIGURATION" clean || {
error_exit "清理构建失败"
}
# 3. 删除上次构建内容
echo "🗑️ 删除上次构建内容..."
setopt rm_star_silent nullglob
rm -rf "$BUILD_DIR/$ARCHIVE_NAME" "$BUILD_DIR/$IPA_DIR_NAME" "$DERIVED_DATA_DIR"
unsetopt rm_star_silent nullglob # 恢复默认设置
# 4. 归档项目
echo "📦 开始归档项目..."
xcodebuild -project "$PROJECT_NAME" \
-scheme "$SCHEME_NAME" \
-configuration "$CONFIGURATION" \
-archivePath "$BUILD_DIR/$ARCHIVE_NAME" \
MARKETING_VERSION="$MARKETING_VERSION" \
CURRENT_PROJECT_VERSION="$CURRENT_PROJECT_VERSION" \
-destination 'generic/platform=iOS' \
CODE_SIGN_IDENTITY="$CODE_SIGN_IDENTITY" \
PROVISIONING_PROFILE="$PROVISIONING_PROFILE" \
DEVELOPMENT_TEAM="$DEVELOPMENT_TEAM" \
CODE_SIGNING_REQUIRED="$CODE_SIGNING_REQUIRED" \
OTHER_CFLAGS="" \
OTHER_CPLUSPLUS_FLAGS="" \
OTHER_SWIFT_FLAGS="" \
archive || {
error_exit "项目归档失败"
}
# 5. 导出IPA文件
echo "📤 导出IPA文件..."
xcodebuild -exportArchive \
-archivePath "$BUILD_DIR/$ARCHIVE_NAME" \
-exportPath "$BUILD_DIR/$IPA_DIR_NAME" \
-exportOptionsPlist "$EXPORT_OPTIONS_PLIST" || {
error_exit "IPA导出失败"
}
echo "🎉 构建完成!IPA文件已导出至: $BUILD_DIR/$IPA_DIR_NAME"
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env python3
import os
import sys
print(f"🔍 当前脚本使用的 Python 路径:{sys.executable}")
print(f"🔍 当前 Python 环境变量 PATH{os.environ.get('PATH')[:500]}...") # 只打印前500字符,避免过长
print(f"🔍 当前 Conda 环境:{os.environ.get('CONDA_DEFAULT_ENV', '未激活 Conda 环境')}\n")
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
import os
import subprocess
import sys
# 颜色定义
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
NC = '\033[0m' # 无颜色
def run_command(command, check=True, capture_output=False):
"""执行系统命令并返回结果"""
try:
if capture_output:
result = subprocess.run(
command,
shell=True,
check=check,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
return result
else:
subprocess.run(command, shell=True, check=check)
return None
except subprocess.CalledProcessError as e:
print(f"{RED}错误:命令执行失败: {e}{NC}")
sys.exit(1)
def main():
# 检查参数
if len(sys.argv) < 2:
print(f"{RED}错误:未传入分支名称参数!{NC}")
print(f"使用方法:{YELLOW}{sys.argv[0]} <远程分支名>{NC}")
sys.exit(1)
print(f"▶ 先设置内网网络\n")
run_command("sudo route add -host 25.13.9.101 25.64.32.254")
# 配置参数
target_branch = sys.argv[1]
# 关键修改:跨平台路径拼接(build + target_branch
# 1. 获取脚本所在目录(作为相对路径基准,可选,根据实际需求调整)
script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
parent_dir = os.path.dirname(script_dir)
# 2. 拼接路径:build/<target_branch>(相对脚本目录)
work_dir = os.path.join(parent_dir, "build", target_branch)
# 3. 转换为绝对路径(消除相对路径符号,更可靠)
work_dir = os.path.abspath(work_dir)
print(f"▶ 本地工作目录:{YELLOW}{work_dir}{NC}\n")
remote_name = "origin"
local_branch = "develop"
print(f"\n{YELLOW}===== 开始执行分支同步脚本 ====={NC}")
print(f"▶ 远程目标分支:{YELLOW}{target_branch}{NC}")
# 1. 进入工作目录
print(f"{YELLOW}1. 检查并进入工作目录...{NC}")
if not os.path.isdir(work_dir):
print(f"{RED}错误:工作目录不存在!{NC}")
sys.exit(1)
try:
os.chdir(work_dir)
except OSError as e:
print(f"{RED}错误:无法进入工作目录!{e}{NC}")
sys.exit(1)
print(f"✅ 成功进入工作目录:{os.getcwd()}\n")
# 2. 检查Git仓库
print(f"{YELLOW}2. 检查Git仓库...{NC}")
if not os.path.isdir(".git"):
print(f"{RED}错误:当前目录不是Git仓库!{NC}")
sys.exit(1)
print(f"✅ 确认是Git仓库\n")
# 3. 关键修复:获取远程分支完整信息
print(f"{YELLOW}3. 完整获取远程所有分支完整信息...{NC}")
# 完整拉取所有分支信息
run_command(f"git fetch {remote_name} '+refs/heads/*:refs/remotes/{remote_name}/*'")
print(f"✅ 远程分支信息拉取完成\n")
# 4. 验证远程分支是否存在
print(f"{YELLOW}4. 验证远程 {target_branch} 分支...{NC}")
result = run_command(
f"git show-ref --verify --quiet 'refs/remotes/{remote_name}/{target_branch}'",
check=False,
capture_output=True
)
if result.returncode != 0:
print(f"{RED}错误:远程确实不存在 {target_branch} 分支!{NC}")
print("远程所有分支列表:")
run_command("git branch -r")
sys.exit(1)
print(f"✅ 确认远程存在 {YELLOW}{target_branch}{NC} 分支\n")
# 5. 同步到本地develop分支
print(f"{YELLOW}5. 同步远程 {target_branch} 到本地 {local_branch}...{NC}")
print(f"{YELLOW}⚠️ 警告:将覆盖本地develop分支所有内容!{NC}")
# 确保本地有develop分支
run_command(f"git checkout -B {local_branch} >/dev/null 2>&1", check=False)
# 关键命令:使用完整引用路径
run_command(f"git reset --hard 'refs/remotes/{remote_name}/{target_branch}'")
# 6. 完成提示
print(f"\n{GREEN}===== 操作完成 ====={NC}")
print(f"✅ 本地 {YELLOW}{local_branch}{NC} 已同步为远程 {YELLOW}{target_branch}{NC} 最新状态")
print("当前版本:")
run_command("git log -1 --pretty=format:\"%h - %an, %ar : %s\"")
if __name__ == "__main__":
main()
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
import os
import subprocess
import sys
import tempfile
# 颜色定义
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
NC = '\033[0m' # 无颜色
def run_command(command, capture_output=False):
"""执行系统命令并返回结果"""
try:
result = subprocess.run(
command,
shell=True,
check=True,
stdout=subprocess.PIPE if capture_output else None,
stderr=subprocess.STDOUT if capture_output else None,
text=True
)
return result
except subprocess.CalledProcessError as e:
if capture_output:
return e # 返回错误信息供后续分析
print(f"{RED}❌ 命令执行失败: {e.output}{NC}")
sys.exit(e.returncode)
def main():
# 检查参数
if len(sys.argv) < 2:
print(f"{RED}错误:参数不足!{NC}")
print(f"使用方法:{YELLOW}{sys.argv[0]} <target_branch>{NC}")
print(f"示例:{YELLOW}{sys.argv[0]} sit{NC}")
sys.exit(1)
# 解析参数(第一个参数为target_branch
target_branch = sys.argv[1]
# ==============================================
# 配置变量 - 根据实际情况修改以下参数
# ==============================================
API_KEY = "ACRB9XFL9G" # App Store Connect API 密钥ID
API_ISSUER = "69a6de7f-3378-47e3-e053-5b8c7c11a4d1" # API 密钥对应的Issuer ID
APP_TYPE = "ios" # 应用类型(固定为ios
# 关键修改:根据target_branch拼接IPA路径
build_dir = f"{os.path.expanduser('~')}/Desktop/lx_uniapp/build" # 构建输出目录
ipa_dir_name = f"{target_branch}_ipa_production" # 带分支名的IPA目录
IPA_PATH = os.path.join(build_dir, ipa_dir_name, "HBuilder.ipa") # 固定IPA文件名
# 显示参数信息
print(f"{YELLOW}===== 上传配置信息 ====={NC}")
print(f"▶ 目标分支: {YELLOW}{target_branch}{NC}")
print(f"▶ IPA文件路径: {YELLOW}{IPA_PATH}{NC}\n")
# ==============================================
# 验证IPA文件是否存在
# ==============================================
if not os.path.isfile(IPA_PATH):
print(f"{RED}❌ 错误:未找到IPA文件!{NC}")
print(f" 查找路径:{IPA_PATH}")
print(f" 请确认该分支已成功构建IPA文件")
sys.exit(1)
# ==============================================
# 1. 验证IPA文件合法性
# ==============================================
print(f"{YELLOW}📋 开始验证IPA文件...{NC}")
print(f" 文件路径:{IPA_PATH}")
# 创建临时文件保存输出
with tempfile.NamedTemporaryFile(mode='w+', delete=False) as tmp_file:
tmp_filename = tmp_file.name
# 执行验证命令
validate_cmd = (
f"xcrun altool --validate-app "
f"--type '{APP_TYPE}' "
f"--file '{IPA_PATH}' "
f"--apiKey '{API_KEY}' "
f"--apiIssuer '{API_ISSUER}' > '{tmp_filename}' 2>&1"
)
# 执行命令但不立即退出(需要处理特定错误)
result = run_command(validate_cmd, capture_output=True)
# 读取并显示验证输出
with open(tmp_filename, 'r') as f:
validate_output = f.read()
print(validate_output)
os.unlink(tmp_filename) # 清理临时文件
# 处理验证结果
if isinstance(result, subprocess.CalledProcessError):
# 检查是否是版本号重复错误
if "The bundle version must be higher than the previously uploaded version" in validate_output:
# 提取之前的版本号
previous_version = None
if "previousBundleVersion = " in validate_output:
previous_version = validate_output.split("previousBundleVersion = ")[1].split()[0]
elif "version: " in validate_output:
previous_version = validate_output.split("version: ")[1].split("")[0]
print(f"{RED}❌ 错误:版本号重复{NC}")
if previous_version:
print(f"{RED} 已上传的最新版本号为: {previous_version}{NC}")
try:
print(f"{RED} 请将版本号修改为 {int(previous_version) + 1} 或更高后重试{NC}")
except ValueError:
print(f"{RED} 请将版本号修改为更高版本后重试{NC}")
else:
print(f"{RED} 请将版本号修改为更高版本后重试{NC}")
sys.exit(result.returncode)
else:
print(f"{RED}❌ IPA文件验证失败,请查看上方错误信息{NC}")
sys.exit(result.returncode)
else:
print(f"{GREEN}✅ IPA文件验证成功,符合上传要求{NC}")
# ==============================================
# 2. 上传IPA文件到App Store Connect
# ==============================================
print(f"\n{YELLOW}🚀 开始上传IPA文件...{NC}")
print(f" 文件路径:{IPA_PATH}")
upload_cmd = (
f"xcrun altool --upload-app "
f"--type '{APP_TYPE}' "
f"--file '{IPA_PATH}' "
f"--apiKey '{API_KEY}' "
f"--apiIssuer '{API_ISSUER}'"
)
run_command(upload_cmd)
# 上传成功提示
print(f"\n{GREEN}🎉 上传成功!可在App Store Connect中查看构建版本{NC}")
print(f" 提示:构建版本处理需要几分钟,请耐心等待")
if __name__ == "__main__":
main()
+92
View File
@@ -0,0 +1,92 @@
#!/bin/bash
set -e # 脚本执行过程中若有命令失败则立即退出
# ==============================================
# 颜色定义
# ==============================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # 无颜色(重置)
# ==============================================
# 配置变量 - 根据实际情况修改以下参数
# ==============================================
API_KEY="ACRB9XFL9G" # App Store Connect API 密钥ID
API_ISSUER="69a6de7f-3378-47e3-e053-5b8c7c11a4d1" # API 密钥对应的Issuer ID
IPA_PATH="$HOME/Desktop/lx_uniapp/build/ipa_production/HBuilder.ipa" # IPA文件路径
APP_TYPE="ios" # 应用类型(固定为ios
# ==============================================
# 验证IPA文件是否存在
# ==============================================
if [ ! -f "$IPA_PATH" ]; then
echo -e "${RED}❌ 错误:未找到IPA文件,请检查路径是否正确${NC}"
echo -e " 查找路径:$IPA_PATH"
exit 1
fi
# ==============================================
# 1. 验证IPA文件合法性
# ==============================================
echo -e "\n${YELLOW}📋 开始验证IPA文件...${NC}"
echo -e " 文件路径:$IPA_PATH"
# 创建临时文件保存输出
TMP_FILE=$(mktemp)
# 执行验证命令,直接输出到终端,同时重定向到临时文件
xcrun altool --validate-app \
--type "$APP_TYPE" \
--file "$IPA_PATH" \
--apiKey "$API_KEY" \
--apiIssuer "$API_ISSUER" > "$TMP_FILE" 2>&1
# 直接打印临时文件内容(确保错误信息显示)
cat "$TMP_FILE"
# 获取退出码
VALIDATE_EXIT_CODE=$?
# 从临时文件读取内容用于分析
VALIDATE_OUTPUT=$(cat "$TMP_FILE")
rm "$TMP_FILE" # 清理临时文件
# 验证结果处理
if [ $VALIDATE_EXIT_CODE -eq 0 ]; then
echo -e "${GREEN}✅ IPA文件验证成功,符合上传要求${NC}"
else
# 检查是否是版本号重复错误
if echo "$VALIDATE_OUTPUT" | grep -q "The bundle version must be higher than the previously uploaded version"; then
# 提取之前的版本号
PREVIOUS_VERSION=$(echo "$VALIDATE_OUTPUT" | grep -oE 'previousBundleVersion = [0-9]+' | awk '{print $3}')
if [ -z "$PREVIOUS_VERSION" ]; then
PREVIOUS_VERSION=$(echo "$VALIDATE_OUTPUT" | grep -oE 'version: [0-9]+' | sed "s/version: //;s///")
fi
echo -e "${RED}❌ 错误:版本号重复${NC}"
echo -e "${RED} 已上传的最新版本号为: $PREVIOUS_VERSION${NC}"
echo -e "${RED} 请将版本号修改为 $((PREVIOUS_VERSION + 1)) 或更高后重试${NC}"
else
echo -e "${RED}❌ IPA文件验证失败,请查看上方错误信息${NC}"
fi
exit $VALIDATE_EXIT_CODE
fi
# ==============================================
# 2. 上传IPA文件到App Store Connect
# ==============================================
echo -e "\n${YELLOW}🚀 开始上传IPA文件...${NC}"
echo -e " 文件路径:$IPA_PATH"
xcrun altool --upload-app \
--type "$APP_TYPE" \
--file "$IPA_PATH" \
--apiKey "$API_KEY" \
--apiIssuer "$API_ISSUER"
# 上传成功提示
if [ $? -eq 0 ]; then
echo -e "\n${GREEN}🎉 上传成功!可在App Store Connect中查看构建版本${NC}"
echo -e " 提示:构建版本处理需要几分钟,请耐心等待"
else
echo -e "\n${RED}❌ 上传失败,请根据错误信息修复后重试${NC}"
exit 1
fi
+6
View File
@@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgZfELkUrcpqqAVlQg
UfKNlPbySAnafYL+vEndTe9/xoqgCgYIKoZIzj0DAQehRANCAATBz1/RZd+1AEgU
VfnoB1PNUMynS8FQ/zJdTcZ25qEuu6WVkWb82Ltmb1lIRSZpRWEbpQzL+7X6zQ95
yjRmUksW
-----END PRIVATE KEY-----
+88
View File
@@ -0,0 +1,88 @@
#!/bin/bash
# 脚本名称:sync_remote_branch.sh
# 功能描述:修复origin/sit引用问题,强制同步远程分支到本地develop
# 使用方法:./sync_remote_branch.sh <远程分支名>
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# 检查参数
if [ $# -eq 0 ]; then
echo -e "${RED}错误:未传入分支名称参数!${NC}"
echo -e "使用方法:${YELLOW}$0 <远程分支名>${NC}"
exit 1
fi
echo -e "▶ 先设置内网网络\n"
sudo route add -host 25.13.9.101 25.64.32.254
# 配置参数
TARGET_BRANCH="$1"
WORK_DIR="Desktop/lx_uniapp/build/git_work_code"
REMOTE_NAME="origin"
LOCAL_BRANCH="develop"
echo -e "\n${YELLOW}===== 开始执行分支同步脚本 =====${NC}"
echo -e "▶ 远程目标分支:${YELLOW}${TARGET_BRANCH}${NC}"
echo -e "▶ 本地工作目录:${YELLOW}${WORK_DIR}${NC}\n"
# 1. 进入工作目录
echo -e "${YELLOW}1. 检查并进入工作目录...${NC}"
if [ ! -d "$WORK_DIR" ]; then
echo -e "${RED}错误:工作目录不存在!${NC}"
exit 1
fi
cd "$WORK_DIR" || {
echo -e "${RED}错误:无法进入工作目录!${NC}"
exit 1
}
echo -e "✅ 成功进入工作目录:$(pwd)\n"
# 2. 检查Git仓库
echo -e "${YELLOW}2. 检查Git仓库...${NC}"
if [ ! -d ".git" ]; then
echo -e "${RED}错误:当前目录不是Git仓库!${NC}"
exit 1
fi
echo -e "✅ 确认是Git仓库\n"
# 3. 关键修复:获取远程分支完整信息(不限制深度)
echo -e "${YELLOW}3. 完整获取远程所有分支完整信息...${NC}"
# 先移除可能存在的浅层克隆限制
git config --unset core.depth
# 完整拉取所有分支信息
if ! git fetch "$REMOTE_NAME" "+refs/heads/*:refs/remotes/$REMOTE_NAME/*"; then
echo -e "${RED}错误:拉取远程分支信息失败!${NC}"
exit 1
fi
echo -e "✅ 远程分支信息拉取完成\n"
# 4. 再次检查远程分支是否存在
echo -e "${YELLOW}4. 验证远程 ${TARGET_BRANCH} 分支...${NC}"
if ! git show-ref --verify --quiet "refs/remotes/$REMOTE_NAME/$TARGET_BRANCH"; then
echo -e "${RED}错误:远程确实不存在 ${TARGET_BRANCH} 分支!${NC}"
echo -e "远程所有分支列表:"
git branch -r
exit 1
fi
echo -e "✅ 确认远程存在 ${YELLOW}${TARGET_BRANCH}${NC} 分支\n"
# 5. 同步到本地develop分支
echo -e "${YELLOW}5. 同步远程 ${TARGET_BRANCH} 到本地 ${LOCAL_BRANCH}...${NC}"
echo -e "${YELLOW}⚠️ 警告:将覆盖本地develop分支所有内容!${NC}"
# 确保本地有develop分支
git checkout -B "$LOCAL_BRANCH" >/dev/null 2>&1
# 关键命令:使用完整引用路径
if ! git reset --hard "refs/remotes/$REMOTE_NAME/$TARGET_BRANCH"; then
echo -e "${RED}错误:同步远程分支到本地失败!${NC}"
exit 1
fi
# 6. 完成提示
echo -e "\n${GREEN}===== 操作完成 =====${NC}"
echo -e "✅ 本地 ${YELLOW}${LOCAL_BRANCH}${NC} 已同步为远程 ${YELLOW}${TARGET_BRANCH}${NC} 最新状态"
echo -e "当前版本:"
git log -1 --pretty=format:"%h - %an, %ar : %s"
+382
View File
@@ -0,0 +1,382 @@
import jwt
import time
import requests
import os
import sys
import datetime
from pathlib import Path
def get_chinese_date_time():
"""
获取当前时间,以中文格式返回(如:8月5日 上午8点24分)
包含月、日、上午/下午、时、分,不包含秒数
"""
# 获取当前时间
now = datetime.datetime.now()
# 获取月份和日期
month = now.month
day = now.day
# 获取24小时制小时数和分钟数
hour_24 = now.hour
minute = now.minute
# 确定上午/下午
if hour_24 < 12:
period = "上午"
# 转换为12小时制(0点特殊处理为12点)
hour_12 = hour_24 if hour_24 != 0 else 12
else:
period = "下午"
# 转换为12小时制(12点保持12点,13点及以后减12)
hour_12 = hour_24 if hour_24 == 12 else hour_24 - 12
# 格式化返回字符串
return f"{month}{day}{period}{hour_12}{minute}"
# 获取参数
# 处理第一个参数(环境),默认值为'develop'
if len(sys.argv) >= 2 and sys.argv[1]:
env_name = sys.argv[1]
else:
env_name = 'develop'
# 处理第二个参数(构建号),默认值为'2'
if len(sys.argv) >= 3 and sys.argv[2]:
build_number_str = sys.argv[2]
else:
build_number_str = '68'
# 验证第一个参数(枚举值)
valid_envs = ['develop', 'sit', 'uat']
if env_name not in ['develop', 'sit', 'uat']:
print(f"错误:环境名称 '{env_name}' 无效有效")
print(f"有效环境名称:{', '.join(valid_envs)}")
sys.exit(1)
# 验证第二个参数(数字字符串)
if not build_number_str.isdigit():
print(f"错误:构建号 '{build_number_str}' 必须是纯数字字符串")
sys.exit(1)
# 参数验证通过,继续处理
TARGET_BUILD_NUMBER = build_number_str
if env_name == 'develop':
BETA_GROUP_NAME = "外部自动化测试" # 测试群组
BETA_GROUP_NAME = "UAT" # 测试群组
TARGET_VERSION = "0.3.1"
elif env_name == 'sit':
BETA_GROUP_NAME = "SIT"
TARGET_VERSION = "0.2.1"
elif env_name == 'uat':
BETA_GROUP_NAME = "UAT"
TARGET_VERSION = "0.3.1"
# 接收参数结束
# 配置参数
API_KEY = "ACRB9XFL9G"
API_ISSUER = "69a6de7f-3378-47e3-e053-5b8c7c11a4d1"
APP_NAME = "吉AI学" # 应用名称
API_ENDPOINT = "https://api.appstoreconnect.apple.com/v1"
MAX_RETRIES = 10 # 最大重试次数
RETRY_DELAY = 30 # 重试间隔(秒)
if 'PYCHARM_HOSTED' in os.environ: # 说明是pycharm
Auth_Key_Path = './'
else:
Auth_Key_Path = '/Users/jlbank/Desktop/lx_uniapp/auto_build' # p8私钥文件位置
private_key_path = Path(f'{Auth_Key_Path}/AuthKey_{API_KEY}.p8')
def generate_jwt_token(api_key, api_issuer, private_key_path):
"""生成 JWT 令牌"""
with open(private_key_path, "r") as f:
private_key = f.read()
# 令牌有效期 20 分钟
current_time = int(time.time())
payload = {
"iss": api_issuer,
"exp": current_time + 20 * 60,
"iat": current_time,
"aud": "appstoreconnect-v1"
}
# 生成 JWT 令牌
token = jwt.encode(
payload,
private_key,
algorithm="ES256",
headers={"kid": api_key}
)
return token
def find_specific_build(app_id, headers):
"""通过预发布版本ID查找指定构建版本,带重试机制"""
retries = 0
while retries < MAX_RETRIES:
try:
print(f"\n🔍 查找版本 {TARGET_VERSION} 构建号 {TARGET_BUILD_NUMBER} (尝试 {retries + 1}/{MAX_RETRIES})")
# 1. 先获取目标版本对应的预发布版本ID
print(" 正在获取预发布版本ID...")
pre_release_url = f"{API_ENDPOINT}/preReleaseVersions"
pre_release_params = {
"filter[version]": TARGET_VERSION,
"filter[app]": app_id
}
pre_release_response = requests.get(pre_release_url, headers=headers, params=pre_release_params)
pre_release_response.raise_for_status()
pre_release_data = pre_release_response.json()
if not pre_release_data["data"]:
print(f"❓ 未找到版本为 {TARGET_VERSION} 的预发布版本")
retries += 1
if retries < MAX_RETRIES:
time.sleep(RETRY_DELAY)
continue
pre_release_id = pre_release_data["data"][0]["id"]
print(f" 找到预发布版本ID: {pre_release_id}")
# 2. 使用预发布版本ID筛选构建版本(修正参数错误)
builds_url = f"{API_ENDPOINT}/builds"
params = {
"filter[preReleaseVersion]": pre_release_id,
"sort": "-version",
"limit": "10",
# 移除了无效的ciBuildGroup关联
"include": "buildBetaDetail,betaBuildLocalizations,preReleaseVersion,betaAppReviewSubmission,icons,buildBundles,betaGroups",
# 移除了无效的ciBuildGroups字段
"limit[betaBuildLocalizations]": "40",
# 将betaGroups的limit调整为最大值50
"limit[betaGroups]": "50"
}
response = requests.get(builds_url, headers=headers, params=params)
if response.status_code == 400:
print(f" 错误响应内容: {response.text}")
response.raise_for_status()
builds_data = response.json()
# 在返回结果中筛选出符合构建号的构建
matching_builds = [
b for b in builds_data["data"]
if b["attributes"].get("version") == TARGET_BUILD_NUMBER
]
if not matching_builds:
print(f"❓ 未找到版本 {TARGET_VERSION} 构建号 {TARGET_BUILD_NUMBER} 的构建")
else:
# 检查是否有状态为VALID的构建
valid_build = next((
b for b in matching_builds
if b["attributes"]["processingState"] == "VALID"
), None)
if valid_build:
return valid_build
else:
# 显示找到的构建但状态不是VALID
states = [b["attributes"]["processingState"] for b in matching_builds]
print(f"⏳ 找到匹配的构建但状态为: {', '.join(states)},等待处理完成...")
# 重试前等待
if retries < MAX_RETRIES - 1:
print(f"将在 {RETRY_DELAY} 秒后重试...")
time.sleep(RETRY_DELAY)
retries += 1
except Exception as e:
print(f"查询过程中出错: {str(e)}")
retries += 1
if retries < MAX_RETRIES:
time.sleep(RETRY_DELAY)
return None
def main():
try:
# 1. 生成 JWT 令牌
if not private_key_path.exists():
raise FileNotFoundError(f"私钥文件不存在: {private_key_path}")
token = generate_jwt_token(API_KEY, API_ISSUER, private_key_path)
print("✅ JWT 令牌生成成功")
# 2. 配置请求头
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
# 3. 获取应用 ID
print(f"\n🔍 查找应用: {APP_NAME}")
apps_url = f"{API_ENDPOINT}/apps"
response = requests.get(apps_url, headers=headers)
response.raise_for_status()
apps_data = response.json()
app = next((a for a in apps_data["data"] if a["attributes"]["name"] == APP_NAME), None)
if not app:
raise ValueError(f"未找到应用: {APP_NAME}")
app_id = app["id"]
print(f"✅ 找到应用 ID: {app_id}")
# 4. 获取指定版本和构建号的构建版本(带重试)
valid_build = find_specific_build(app_id, headers)
if not valid_build:
raise ValueError(
f"经过 {MAX_RETRIES} 次尝试后,仍未找到可用的构建版本: {TARGET_VERSION} ({TARGET_BUILD_NUMBER})")
build_id = valid_build["id"]
build_version = TARGET_VERSION
build_number = valid_build["attributes"]["version"]
print(f"✅ 找到构建版本 ID: {build_id} (版本号: {build_version}, 构建号: {build_number})")
# 5. 获取外部测试群组 ID
print(f"\n🔍 查找测试群组: {BETA_GROUP_NAME}")
groups_url = f"{API_ENDPOINT}/betaGroups"
response = requests.get(groups_url, headers=headers)
response.raise_for_status()
groups_data = response.json()
beta_group = next((g for g in groups_data["data"] if
g["attributes"]["name"] == BETA_GROUP_NAME and
g["attributes"]["isInternalGroup"] is False), None) # 确保是外部群组
if not beta_group:
raise ValueError(f"未找到外部测试群组: {BETA_GROUP_NAME}")
beta_group_id = beta_group["id"]
print(f"✅ 找到外部测试群组 ID: {beta_group_id}")
# 6. 添加构建到测试群组后,更新测试内容(betaBuildLocalizations
print("\n📝 更新测试版本的本地化信息(whatsNew)...")
# 7. 将构建版本添加到测试群组
print(f"\n🔗 将构建 {build_id} 添加到测试群组 {beta_group_id}...")
add_build_url = f"{API_ENDPOINT}/betaGroups/{beta_group_id}/relationships/builds"
# 准备添加构建的请求数据
add_build_payload = {
"data": [{"id": build_id, "type": "builds"}]
}
# 发送添加请求
add_response = requests.post(add_build_url, headers=headers, json=add_build_payload)
try:
add_response.raise_for_status()
except requests.exceptions.HTTPError as e:
# 处理已存在的情况(409 Conflict 是正常现象,说明已添加)
if add_response.status_code == 409:
print("⚠️ 构建已在测试群组中,跳过添加步骤")
else:
print(f"❌ 添加构建到测试群组失败: {e}")
print("错误详情:", add_response.json())
sys.exit(1)
else:
print("✅ 构建已成功添加到测试群组")
# 8. 更新测试内容(whatsNew
# 先获取该构建的本地化信息 ID(如果已有)
# 构建 ID(build_id 是你要操作的构建版本 ID)
get_localizations_url = f"{API_ENDPOINT}/builds/{build_id}/betaBuildLocalizations"
response = requests.get(get_localizations_url, headers=headers)
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
print(f"❌ 获取本地化信息失败: {e}")
print("错误详情:", response.json())
sys.exit(1)
localizations = response.json().get("data", [])
print("\n📝 更新测试版本说明...")
if localizations:
# 使用现有本地化信息
localization_id = localizations[0]["id"]
update_url = f"{API_ENDPOINT}/betaBuildLocalizations/{localization_id}"
print(f"使用现有本地化信息 ID: {localization_id}")
else:
# 创建新的本地化信息(默认简体中文)
print("创建新的本地化信息...")
create_url = f"{API_ENDPOINT}/betaBuildLocalizations"
create_payload = {
"data": {
"type": "betaBuildLocalizations",
"attributes": {"locale": "zh-Hans", "whatsNew": ""},
"relationships": {
"build": {"data": {"id": build_id, "type": "builds"}}
}
}
}
create_response = requests.post(create_url, headers=headers, json=create_payload)
create_response.raise_for_status()
localization_id = create_response.json()["data"]["id"]
update_url = f"{API_ENDPOINT}/betaBuildLocalizations/{localization_id}"
now_time_text = get_chinese_date_time()
# 更新测试内容
update_payload = {
"data": {
"id": localization_id,
"type": "betaBuildLocalizations",
"attributes": {
"whatsNew": f"当前版本: {TARGET_BUILD_NUMBER}\n提交时间:{now_time_text}\n自动化提交"
}
}
}
update_response = requests.patch(update_url, headers=headers, json=update_payload)
update_response.raise_for_status()
print("✅ 测试版本说明已更新")
# 9. 提交审核(触发测试状态)- 修复API端点
print("\n📋 提交构建版本审核...")
# 正确的提交审核API端点
submit_url = f"{API_ENDPOINT}/betaAppReviewSubmissions"
submit_payload = {
"data": {
"type": "betaAppReviewSubmissions",
"relationships": {
"build": {
"data": {"id": build_id, "type": "builds"}
}
}
}
}
submit_response = requests.post(submit_url, headers=headers, json=submit_payload)
try:
submit_response.raise_for_status()
except requests.exceptions.HTTPError as e:
# 处理已提交的情况(409冲突)
if submit_response.status_code == 409:
print("⚠️ 构建已提交审核,无需重复操作")
else:
print(f"❌ 提交审核失败: {e}")
print("错误详情:", submit_response.json())
sys.exit(1)
else:
print("✅ 构建版本已成功提交审核")
except Exception as e:
print(e)
exit(1)
if __name__ == "__main__":
main()
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
# ==============================================
# 配置参数 - 请根据实际情况修改以下参数
# ==============================================
# 钥匙串配置
KEYCHAIN_PASSWORD="1234567" # 钥匙串密码
KEYCHAIN_PATH="$HOME/Library/Keychains/login.keychain-db" # 钥匙串路径
# 项目路径配置
PROJECT_DIR="$HOME/Desktop/lx_uniapp/build/git_work_code/Ios/HBuilder-Hello" # 项目目录
PROJECT_NAME="HBuilder-Hello.xcodeproj" # Xcode项目文件名
SCHEME_NAME="HBuilder" # 构建方案名称
CONFIGURATION="Release" # 构建配置
# 构建输出配置
BUILD_DIR="$HOME/Desktop/lx_uniapp/build" # 构建输出目录
ARCHIVE_NAME="HBuilder.xcarchive" # 归档文件名
IPA_DIR_NAME="ipa_production" # IPA输出目录名
DERIVED_DATA_DIR="$HOME/Library/Developer/Xcode/DerivedData/*" # 衍生数据目录
# 版本配置
MARKETING_VERSION="$1" # 市场版本号
CURRENT_PROJECT_VERSION="$2" # 项目内部版本号
# 签名配置
CODE_SIGN_IDENTITY="Apple Distribution: Bank of Jilin Co., Ltd. (2EP6SRRN43)" # 签名标识
PROVISIONING_PROFILE="e3487fe1-19dc-4fc7-90df-11caf27da0c6" # 配置文件UUID
DEVELOPMENT_TEAM="2EP6SRRN43" # 开发团队ID
CODE_SIGNING_REQUIRED="YES" # 是否需要签名
# 导出配置
EXPORT_OPTIONS_PLIST="$BUILD_DIR/ExportOptions_Production.plist" # 导出配置plist路径
# ==============================================
# 错误处理函数
error_exit() {
echo "$1"
exit 1
}
# 0. 解锁钥匙串并授予权限
echo "🔑 解锁钥匙串..."
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" || {
error_exit "钥匙串解锁失败"
}
# 1. 进入项目目录
echo "📂 进入项目目录: $PROJECT_DIR"
cd "$PROJECT_DIR" || {
error_exit "无法进入项目目录: $PROJECT_DIR"
}
# 2. 清理旧构建
echo "🧹 清理旧构建..."
xcodebuild -project "$PROJECT_NAME" -scheme "$SCHEME_NAME" -configuration "$CONFIGURATION" clean || {
error_exit "清理构建失败"
}
# 3. 删除上次构建内容
echo "🗑️ 删除上次构建内容..."
setopt rm_star_silent nullglob
rm -rf "$BUILD_DIR/$ARCHIVE_NAME" "$BUILD_DIR/$IPA_DIR_NAME" "$DERIVED_DATA_DIR"
unsetopt rm_star_silent nullglob # 恢复默认设置
# 4. 归档项目
echo "📦 开始归档项目..."
xcodebuild -project "$PROJECT_NAME" \
-scheme "$SCHEME_NAME" \
-configuration "$CONFIGURATION" \
-archivePath "$BUILD_DIR/$ARCHIVE_NAME" \
MARKETING_VERSION="$MARKETING_VERSION" \
CURRENT_PROJECT_VERSION="$CURRENT_PROJECT_VERSION" \
-destination 'generic/platform=iOS' \
CODE_SIGN_IDENTITY="$CODE_SIGN_IDENTITY" \
PROVISIONING_PROFILE="$PROVISIONING_PROFILE" \
DEVELOPMENT_TEAM="$DEVELOPMENT_TEAM" \
CODE_SIGNING_REQUIRED="$CODE_SIGNING_REQUIRED" \
OTHER_CFLAGS="" \
OTHER_CPLUSPLUS_FLAGS="" \
OTHER_SWIFT_FLAGS="" \
archive || {
error_exit "项目归档失败"
}
# 5. 导出IPA文件
echo "📤 导出IPA文件..."
xcodebuild -exportArchive \
-archivePath "$BUILD_DIR/$ARCHIVE_NAME" \
-exportPath "$BUILD_DIR/$IPA_DIR_NAME" \
-exportOptionsPlist "$EXPORT_OPTIONS_PLIST" || {
error_exit "IPA导出失败"
}
echo "🎉 构建完成!IPA文件已导出至: $BUILD_DIR/$IPA_DIR_NAME"
+92
View File
@@ -0,0 +1,92 @@
#!/bin/bash
set -e # 脚本执行过程中若有命令失败则立即退出
# ==============================================
# 颜色定义
# ==============================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # 无颜色(重置)
# ==============================================
# 配置变量 - 根据实际情况修改以下参数
# ==============================================
API_KEY="ACRB9XFL9G" # App Store Connect API 密钥ID
API_ISSUER="69a6de7f-3378-47e3-e053-5b8c7c11a4d1" # API 密钥对应的Issuer ID
IPA_PATH="$HOME/Desktop/lx_uniapp/build/ipa_production/HBuilder.ipa" # IPA文件路径
APP_TYPE="ios" # 应用类型(固定为ios
# ==============================================
# 验证IPA文件是否存在
# ==============================================
if [ ! -f "$IPA_PATH" ]; then
echo -e "${RED}❌ 错误:未找到IPA文件,请检查路径是否正确${NC}"
echo -e " 查找路径:$IPA_PATH"
exit 1
fi
# ==============================================
# 1. 验证IPA文件合法性
# ==============================================
echo -e "\n${YELLOW}📋 开始验证IPA文件...${NC}"
echo -e " 文件路径:$IPA_PATH"
# 创建临时文件保存输出
TMP_FILE=$(mktemp)
# 执行验证命令,直接输出到终端,同时重定向到临时文件
xcrun altool --validate-app \
--type "$APP_TYPE" \
--file "$IPA_PATH" \
--apiKey "$API_KEY" \
--apiIssuer "$API_ISSUER" > "$TMP_FILE" 2>&1
# 直接打印临时文件内容(确保错误信息显示)
cat "$TMP_FILE"
# 获取退出码
VALIDATE_EXIT_CODE=$?
# 从临时文件读取内容用于分析
VALIDATE_OUTPUT=$(cat "$TMP_FILE")
rm "$TMP_FILE" # 清理临时文件
# 验证结果处理
if [ $VALIDATE_EXIT_CODE -eq 0 ]; then
echo -e "${GREEN}✅ IPA文件验证成功,符合上传要求${NC}"
else
# 检查是否是版本号重复错误
if echo "$VALIDATE_OUTPUT" | grep -q "The bundle version must be higher than the previously uploaded version"; then
# 提取之前的版本号
PREVIOUS_VERSION=$(echo "$VALIDATE_OUTPUT" | grep -oE 'previousBundleVersion = [0-9]+' | awk '{print $3}')
if [ -z "$PREVIOUS_VERSION" ]; then
PREVIOUS_VERSION=$(echo "$VALIDATE_OUTPUT" | grep -oE 'version: [0-9]+' | sed "s/version: //;s///")
fi
echo -e "${RED}❌ 错误:版本号重复${NC}"
echo -e "${RED} 已上传的最新版本号为: $PREVIOUS_VERSION${NC}"
echo -e "${RED} 请将版本号修改为 $((PREVIOUS_VERSION + 1)) 或更高后重试${NC}"
else
echo -e "${RED}❌ IPA文件验证失败,请查看上方错误信息${NC}"
fi
exit $VALIDATE_EXIT_CODE
fi
# ==============================================
# 2. 上传IPA文件到App Store Connect
# ==============================================
echo -e "\n${YELLOW}🚀 开始上传IPA文件...${NC}"
echo -e " 文件路径:$IPA_PATH"
xcrun altool --upload-app \
--type "$APP_TYPE" \
--file "$IPA_PATH" \
--apiKey "$API_KEY" \
--apiIssuer "$API_ISSUER"
# 上传成功提示
if [ $? -eq 0 ]; then
echo -e "\n${GREEN}🎉 上传成功!可在App Store Connect中查看构建版本${NC}"
echo -e " 提示:构建版本处理需要几分钟,请耐心等待"
else
echo -e "\n${RED}❌ 上传失败,请根据错误信息修复后重试${NC}"
exit 1
fi
+1 -1
View File
@@ -15,7 +15,7 @@
"build:app-plus:sit": "uni build -p app-plus --mode sit",
"build:app-plus:uat": "uni build -p app-plus --mode uat",
"build:app-plus:prod": "uni build -p app-plus",
"build:h5:dev": "uni build -p app-plus --mode development_h5",
"build:h5:dev": "uni build -p h5 --mode development_h5",
"build:h5": "uni build"
},
"dependencies": {
+1 -1
View File
@@ -35,7 +35,7 @@ export const get_base_url = (url = '') => {
}
// #endif
// #ifdef APP-PLUS
return 'https://aitstest.jlbank.com.cn:7001' || ENV.VITE_APP_BASE_API_Url
return ENV.VITE_APP_BASE_API_Url
// #endif
} else { // 其他环境,包括生产环境,sit环境,uat环境
+9
View File
@@ -116,3 +116,12 @@ export const switchIcon = () : string => {
`.trim();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
// 左右切换箭头
export const arrowRightIcon = (color : string = '#F5212D') : string => {
const svgXml = `
<svg t="1762762748602" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7658" width="64" height="64" fill="${color}"><path d="M462.08 192a47.36 47.36 0 0 0 0 64l256 256-256 256a47.36 47.36 0 1 0 64 64l288.64-288.64A50.56 50.56 0 0 0 832 512a47.36 47.36 0 0 0-14.08-33.28L529.28 192a47.36 47.36 0 0 0-67.2 0z" p-id="7659"></path><path d="M206.08 192a47.36 47.36 0 0 0 0 64l256 256-256 256a47.36 47.36 0 1 0 64 64l291.84-286.08A50.56 50.56 0 0 0 576 512a47.36 47.36 0 0 0-14.08-33.28L273.28 192a47.36 47.36 0 0 0-67.2 0z" p-id="7660" fill="${color}"></path></svg>
`.trim();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
+1 -1
View File
@@ -110,7 +110,7 @@
<view class="reference_answer_title">参考答案:</view>
{{ subject_data.itemVos[0]?.itemAnswer ?? '' }}
</view>
<view class="analysis_note">试题解析:{{ subject_data?.analyContent }}</view>
<view class="analysis_note" v-if="props.mode !== 'pk_in'">试题解析:{{ subject_data?.analyContent }}</view>
</view>
</view>
</view>
+224 -9
View File
@@ -1,17 +1,232 @@
<template>
<view class="">
<uni-popup ref="popup" border-radius="10px 10px 0 0">
<view class="" style="background-color: aqua;">
<uni-popup ref="popup" background-color="rgba(10,10,10, 0.8)" >
<view class="popup-content" @touchmove.stop>
<view class="fl1"></view>
<view class="max-div">
<view class="popup_back_div" @click="close">
<image :src="optionErrorIcon('#C8BBB7')" class="img"></image>
</view>
<view class="title_div">
<image class="img" src="@/static/images/pointsAndRank/badge-wheat-left.png"></image>
<view class="title">恭喜你获得新徽章</view>
<image class="img" src="@/static/images/pointsAndRank/badge-wheat-right.png"></image>
</view>
<view class="points-div">
<view class="doct_icon" @click="doct_click(-1)">
<view class="doct_icon_div left">
<image :src="arrowRightIcon('#D88F50')" class="img" mode="widthFix"></image>
</view>
</view>
<swiper class="swiper">
<swiper-item class="swiper-item-div">
<image src="@/static/images/test/xz.png" class="img"></image>
</swiper-item>
<swiper-item class="swiper-item-div">
<image src="@/static/images/test/xz.png" class="img"></image>
<!-- // <image-preview :src="item.imgAddr" class="img" mode="widthFix"></image-preview> -->
</swiper-item>
</swiper>
<view class="doct_icon" @click="doct_click(1)">
<view class="doct_icon_div right">
<image :src="arrowRightIcon('#D88F50')" class="img" mode="widthFix"></image>
</view>
</view>
<!-- <view class="points-img-div">
<image-preview :src="nowImgSrc"></image-preview>
</view> -->
</view>
<view class="button-div">
<uv-button
class="button"
text="领取"
loadingText="签到中"
:custom-style="customStyle"
:color="buttonColor"
@click="checkIn"
></uv-button>
</view>
</view>
<view class="fl1"></view>
</view>
</uni-popup>
</view>
</template>
<script setup>
const popup = ref(null)
const open = () => {
}
defineExpose({open})
import { ref, computed } from 'vue';
import { optionErrorIcon, optionSuccessIcon } from '@/common/imgSvg';
import { addPointsByCheckIn, queryCheckIn } from '@/api/pointsAndRank.js';
import common from '@/common/common';
import { arrowRightIcon } from '@/common/imgSvg';
const popup = ref(null);
const nowImgSrc = ref('');
const badgesList = ref([]);
const pointsList = ref([]);
const buttonColor = 'linear-gradient( 270deg, #E0914A 0%, #EDBE7F 100%)';
const customStyle = {
borderRadius: '40rpx' //圆角
};
const emits = defineEmits(['checkInFun']);
// 退出签到
const close = () => {
popup.value.close();
};
const open = (points = [], badges = []) => {
if (!Array.isArray(badges) || badges.length === 0) {
console.error('badges 不是有效的数组或为空');
return;
}
badgesList.value = badges;
pointsList.value = points;
popup.value.open();
};
defineExpose({ open, close });
</script>
<style></style>
<style scoped lang="scss">
.points-div {
// width: 100%;
display: flex;
position: relative;
align-items: center;
justify-content: center;
.swiper {
height: 400rpx;
width: 520rpx;
.swiper-item-div {
/* 父容器只需居中对齐,无需设置背景图 */
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background-image: url('@/static/images/pointsAndRank/fs.png');
background-size: 110% 110%; /* 背景图比img大10% */
background-position: center; /* 背景图居中 */
background-repeat: no-repeat; /* 禁止背景图重复 */
.img {
/* 1. 缩放img(根据需要调整宽高,这里缩到原来的80%左右示例) */
width: 300rpx;
height: 270rpx;
transform: scale(0.8);
}
}
}
.doct_icon {
padding: 0%;
display: flex;
.doct_icon_div {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.left {
transform: rotate(180deg);
}
width: 8vw;
display: flex;
align-items: center;
flex-direction: column;
}
}
.button-div {
height: 92rpx;
width: 442rpx;
margin: 34rpx auto 0 auto;
.button {
width: 100%;
height: 100%;
}
}
.continuous-check-in-div {
display: flex;
border-bottom: 2rpx solid #efefef;
margin-bottom: 16rpx;
padding-top: 8rpx;
height: 128rpx;
.img {
width: 112rpx;
height: 102rpx;
}
.text {
margin-left: 16rpx;
font-weight: 400;
font-size: 30rpx;
height: 82rpx;
color: #333333;
display: flex;
align-items: flex-end;
}
.number {
font-weight: 600;
color: #0066ff;
font-family: Arial Black;
}
}
.title_div {
display: flex;
align-items: center;
justify-content: center;
margin-top: 54rpx;
margin-bottom: 24rpx;
height: 68rpx;
.img {
width: 26rpx;
height: 44rpx;
}
.title {
font-family: AlimamaShuHeiTi, AlimamaShuHeiTi;
font-weight: bold;
font-size: 36rpx;
color: #d88f50;
margin: 0rpx 30rpx;
text-align: center;
}
}
.popup_back_div {
width: 60rpx;
height: 60rpx;
position: absolute;
top: 20rpx;
right: 20rpx;
.img {
margin: 10rpx;
width: 40rpx;
height: 40rpx;
}
}
.popup-content {
width: 100vw;
height: 100vh;
z-index: 99999;
display: flex;
flex-direction: column;
align-items: center;
// padding: 40rpx 0;
}
.max-div {
// max-width: 400rpx;
// width: 80vw;
background-color: #fefbfa;
border-radius: 38rpx;
position: relative;
// padding: 26rpx;
overflow: hidden;
margin-top: var(--status-bar-height);
margin-bottom: 120rpx;
}
</style>
+19 -21
View File
@@ -95,7 +95,7 @@
// 注意:如果天数从1开始(不是0),需要减1再计算
return Math.floor((currentDay - 1) / 7);
});
const emits = defineEmits(['checkInFun'])
const emits = defineEmits(['checkInFun']);
// 切换显示状态
const switchStatus = () => {
expand.value = !expand.value;
@@ -109,14 +109,24 @@
const checkIn = () => {
if (checkInLoading.value) return;
checkInLoading.value = true;
addPointsByCheckIn({days:6})
addPointsByCheckIn({ days: continuousDay.value })
.then((res) => {
console.log('签到结果', res);
checkInLoading.value = false;
isSigned.value = true;
continuousDay.value = continuousDay.value + 1;
common.msg('签到成功,请明日继续!')
emits('checkInFun', true)
// common.msg('签到成功,请明日继续!')
emits(
'checkInFun',
true,
[{ changePoints: 5 }],
[
{
pointsBadgeImg: '/traoss/tras3/show/S3F0250181220722025110713501700000661177',
pointsBadgeName: '黄金徽章'
}
]
);
})
.catch((err) => {
checkInLoading.value = false;
@@ -195,11 +205,11 @@
return groupedList;
}
const open = (checkInToday,days) => {
isSigned.value = checkInToday==='Y'?true:false// 今日签到状态
// isSigned.value = false// 今日签到状态
continuousDay.value = days // 连续签到
const open = (checkInToday, days) => {
isSigned.value = checkInToday === 'Y' ? true : false; // 今日签到状态
isSigned.value = false; // 今日签到状态
continuousDay.value = days; // 连续签到
// 示例:X=10时,以"10天前"为第1天,生成35天列表
try {
const X = continuousDay.value - (isSigned.value ? 1 : 0);
@@ -207,18 +217,6 @@
console.log(`${continuousDay.value}`);
console.log(`${X}天前为第1天的35天分组日期:`);
dateGroups.value = generateGroupedDateList(X);
// dateGroups.value.forEach((group) => {
// console.log(`\n第${group.groupIndex + 1}组:`);
// console.log(
// group.days.map((day) => ({
// 日期: day.date,
// 天数序号: day.index, // 显示当前是第几天
// text: day.text,
// 类型: day.type
// }))
// );
// });
} catch (err) {
console.error(err.message);
}
+178 -3
View File
@@ -1,8 +1,183 @@
<template>
<view class="">
<uni-popup ref="popup">
<view class="popup-content" @touchmove.stop>
<view class="fl1"></view>
<view class="max-div">
<view class="popup_back_div" @click="clickFun(false)">
<image :src="optionErrorIcon('#C8BBB7')" class="img"></image>
</view>
<view class="title_div">
<image class="img" src="@/static/images/pointsAndRank/badge-wheat-left.png"></image>
<view class="title">恭喜你获得积分</view>
<image class="img" src="@/static/images/pointsAndRank/badge-wheat-right.png"></image>
</view>
<view class="points-div">
<view class="points-img-div">
<image class="img_100" src="@/static/images/pointsAndRank/like.png" mode=""></image>
</view>
<view class="points-num-div">+{{showData.num}}</view>
<view class="points-msg-div" v-show="showData.tip"> {{showData.tip}}</view>
</view>
<view class="button-div">
<uv-button
class="button"
text="开心收下"
:custom-style="customStyle"
:color="buttonColor"
@click="clickFun(true)"
></uv-button>
</view>
</view>
<view class="fl1"></view>
</view>
</uni-popup>
</view>
</template>
<script>
<script setup>
import { ref, computed } from 'vue';
import { optionErrorIcon, optionSuccessIcon } from '@/common/imgSvg';
import { addPointsByCheckIn, queryCheckIn } from '@/api/pointsAndRank.js';
import common from '@/common/common';
const showData = ref({
num: 1,
tip:''
});
const badgesList =ref([]);
const pointsList = ref([]);
const popup = ref(null);
const buttonColor = 'linear-gradient( 270deg, #F81758 0%, #FFA062 100%)';
const customStyle = {
borderRadius: '40rpx' //圆角
};
const emits = defineEmits(['confirm']);
const open = (points, badges=[]) => {
if (!Array.isArray(points) || points.length === 0) {
console.error('points 不是有效的数组或为空');
// 积分为空直接调用完成
emits(
'confirm',
status,
points,
badges
);
return;
}
badgesList.value = badges
// 解构出第一项和剩余元素(原数组不变)
const [firstItem, ...remainingPoints] = points;
console.log('firstItem', firstItem);
showData.value['num'] = firstItem['changePoints'];
pointsList.value = remainingPoints;
popup.value.open();
};
const close = () => {
popup.value.close();
};
// 点击
const clickFun = (status = true) => {
emits(
'confirm',
status,
pointsList.value,
badgesList.value
);
};
defineExpose({ open, close });
</script>
<style>
</style>
<style scoped lang="scss">
.points-div {
display: flex;
flex-direction: column;
align-items: center;
padding: 10rpx 0 0 0;
.points-img-div {
width: 320rpx;
height: 278rpx;
}
.points-num-div {
font-family: Arial, Arial;
font-weight: normal;
font-size: 108rpx;
color: #333333;
line-height: 108rpx;
text-align: center;
font-weight: 600;
}
.points-msg-div {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 30rpx;
color: #ae723b;
line-height: 40px;
text-align: center;
}
}
.button-div {
height: 92rpx;
width: 442rpx;
margin: 40rpx auto 20rpx auto;
.button {
width: 100%;
height: 100%;
}
}
.title_div {
display: flex;
align-items: center;
justify-content: center;
margin-top: 34rpx;
height: 68rpx;
.img {
width: 26rpx;
height: 44rpx;
}
.title {
font-family: AlimamaShuHeiTi, AlimamaShuHeiTi;
font-weight: bold;
font-size: 46rpx;
color: #d88f50;
margin: 0rpx 30rpx;
text-align: center;
}
}
.popup_back_div {
width: 60rpx;
height: 60rpx;
position: absolute;
top: 20rpx;
right: 20rpx;
.img {
margin: 10rpx;
width: 40rpx;
height: 40rpx;
}
}
.popup-content {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 0;
}
.max-div {
width: 80vw;
background-color: #fefbfa;
border-radius: 38rpx;
position: relative;
padding: 26rpx;
overflow: hidden;
margin-top: var(--status-bar-height);
margin-bottom: 120rpx;
}
</style>
+2 -1
View File
@@ -47,4 +47,5 @@ export const styleButton = {
backgroundColor: '#E2EDFF',
borderRadius: '8rpx'
}
}
}
export const pop_up_time = 500;
+330 -320
View File
@@ -1,337 +1,347 @@
<template>
<view class="main_div max_page">
<template v-if="!isMatchingSuccess">
<!-- 顶部一些按钮 -->
<view class="top-button-div">
<view class="back_div" @click="common.navigateBack()">
<view class="back-icon"></view>
</view>
<view class="fl1"></view>
<view class="top-icon-div" @click="click_top_icon(1)">
<image class="img1" :src="data_info.isAnony === 'N' ? pkAnonymousClose() : pkAnonymousOpen()"></image>
<view class="text">{{ data_info.isAnony === 'N' ? '不匿名' : '匿名' }}</view>
</view>
<view class="top-icon-div" @click="click_top_icon(2)">
<image class="img2" :src="pkRules()"></image>
<view class="text">规则说明</view>
</view>
<view class="top-icon-div" @click="click_top_icon(3)">
<image class="img3" :src="pkRankingList()"></image>
<view class="text">排行榜</view>
</view>
</view>
<view class="title-div" :class="{ hidden: challengeLoading }">
{{ data_info.comName }}
</view>
<view class="match-msg-div" :class="{ hidden: !challengeLoading }">匹配中...</view>
<view class="avatar-div">
<view class="avatar-surround-div">
<view class="avatar-img-div" :class="{ hidden: challengeLoading }">
<cached-avatar v-show="data_info.isAnony === 'N'" class="img"></cached-avatar>
<image v-show="data_info.isAnony === 'Y'" class="img" :src="defaultAnonymousAvatar"></image>
</view>
</view>
<view class="avatar-name-div">{{ data_info.isAnony === 'N' ? data_info.userName : '匿名' }}</view>
</view>
<view class="select-div" :class="{ hidden: challengeLoading }" @click="selectionInstitutionsRef.open(true)">
<view class="select-institution-div">
{{ data_info.orgName }}
<image class="img" :src="switchIcon()"></image>
</view>
<view class="select-institution-msg-div">*选择机构后会根据机构进行人员随机匹配挑战</view>
</view>
<view class="fl1"></view>
<!-- 匹配时间 -->
<view class="match-time-div" :class="{ hidden: !challengeLoading }">
{{ formatTime(practiceTime) }}
</view>
<view class="button-div" v-show="challengeButtonStatus">
<view
:class="{ start_button: !challengeLoading, cancel_button: challengeLoading, 'button-transition': true }"
@click="start_challenge"
>
{{ challengeLoading ? '取消匹配' : '开始挑战' }}
</view>
</view>
<view class="fl1"></view>
<view class="fl1"></view>
<view class="fl1"></view>
</template>
<template v-else>
<view class="matching-success-msg">匹配成功</view>
<view class="matching-success-animation">
<view class="left animate-left">
<image class="img" src="@/static/images/competition/matching-success-left.png" mode="widthFix"></image>
<view class="information-div-left">
<view class="avatar-img-div">
<cached-avatar v-show="data_info.isAnony === 'N'" class="img"></cached-avatar>
<image v-show="data_info.isAnony === 'Y'" class="img" :src="defaultAnonymousAvatar"></image>
</view>
<view class="name">
{{ data_info.isAnony === 'N' ? data_info.userName : '匿名' }}
</view>
<view class="org">
{{ data_info.isAnony === 'N' ? data_info.orgIdName : '机构不显示' }}
</view>
</view>
</view>
<view class="right animate-right">
<image class="img" src="@/static/images/competition/matching-success-right.png" mode="widthFix"></image>
<view class="information-div-right">
<view class="avatar-img-div">
<image-preview
v-if="data_info.pkIsAnony === 'N'"
class="img"
:src="data_info.pkimgAddr"
></image-preview>
<image v-else class="img" :src="defaultAnonymousAvatar" mode="aspectFill"></image>
</view>
<view class="name">
{{ data_info.pkIsAnony === 'N' ? data_info.pkUserName : '匿名' }}
</view>
<view class="org">
{{ data_info.pkIsAnony === 'N' ? data_info.pkOrgIdName : '机构不显示' }}
</view>
</view>
</view>
</view>
<view class="matching-success-text">准备</view>
</template>
<uni-popup ref="popupRef" class="popup">
<view class="sheet_popup_div">
<view class="title_div">双人对战规则说明</view>
<view class="note_div">
{{ data_info.ruleDesc }}
</view>
</view>
</uni-popup>
<selectionInstitutions ref="selectionInstitutionsRef" @submit="selectionInstitutionsSubmit"></selectionInstitutions>
<uv-picker ref="anonyPickerRef" :columns="[anonyOptions]" keyName="label" @confirm="anonyConfirm"></uv-picker>
</view>
<view class="main_div max_page">
<template v-if="!isMatchingSuccess">
<!-- 顶部一些按钮 -->
<view class="top-button-div">
<view class="back_div" @click="common.navigateBack()">
<view class="back-icon"></view>
</view>
<view class="fl1"></view>
<view class="top-icon-div" @click="click_top_icon(1)">
<image class="img1" :src="data_info.isAnony === 'N' ? pkAnonymousClose() : pkAnonymousOpen()"></image>
<view class="text">{{ data_info.isAnony === 'N' ? '不匿名' : '匿名' }}</view>
</view>
<view class="top-icon-div" @click="click_top_icon(2)">
<image class="img2" :src="pkRules()"></image>
<view class="text">规则说明</view>
</view>
<view class="top-icon-div" @click="click_top_icon(3)">
<image class="img3" :src="pkRankingList()"></image>
<view class="text">排行榜</view>
</view>
</view>
<view class="title-div" :class="{ hidden: challengeLoading }">
{{ data_info.comName }}
</view>
<view class="match-msg-div" :class="{ hidden: !challengeLoading }">匹配中...</view>
<view class="avatar-div">
<view class="avatar-surround-div">
<view class="avatar-img-div" :class="{ hidden: challengeLoading }">
<cached-avatar v-show="data_info.isAnony === 'N'" class="img"></cached-avatar>
<image v-show="data_info.isAnony === 'Y'" class="img" :src="defaultAnonymousAvatar"></image>
</view>
</view>
<view class="avatar-name-div">{{ data_info.isAnony === 'N' ? data_info.userName : '匿名' }}</view>
</view>
<view class="select-div" :class="{ hidden: challengeLoading }" @click="selectionInstitutionsRef.open(true)">
<view class="select-institution-div">
{{ data_info.orgName }}
<image class="img" :src="switchIcon()"></image>
</view>
<view class="select-institution-msg-div">*选择机构后会根据机构进行人员随机匹配挑战</view>
</view>
<view class="fl1"></view>
<!-- 匹配时间 -->
<view class="match-time-div" :class="{ hidden: !challengeLoading }">
{{ formatTime(practiceTime) }}
</view>
<view class="button-div" v-show="challengeButtonStatus">
<view
:class="{ start_button: !challengeLoading, cancel_button: challengeLoading, 'button-transition': true }"
@click="start_challenge"
>
{{ challengeLoading ? '取消匹配' : '开始挑战' }}
</view>
</view>
<view class="fl1"></view>
<view class="fl1"></view>
<view class="fl1"></view>
</template>
<template v-else>
<view class="matching-success-msg">匹配成功</view>
<view class="matching-success-animation">
<view class="left animate-left">
<image class="img" src="@/static/images/competition/matching-success-left.png" mode="widthFix"></image>
<view class="information-div-left">
<view class="avatar-img-div">
<cached-avatar v-show="data_info.isAnony === 'N'" class="img"></cached-avatar>
<image v-show="data_info.isAnony === 'Y'" class="img" :src="defaultAnonymousAvatar"></image>
</view>
<view class="name">
{{ data_info.isAnony === 'N' ? data_info.userName : '匿名' }}
</view>
<view class="org">
{{ data_info.isAnony === 'N' ? data_info.orgIdName : '机构不显示' }}
</view>
</view>
</view>
<view class="right animate-right">
<image class="img" src="@/static/images/competition/matching-success-right.png" mode="widthFix"></image>
<view class="information-div-right">
<view class="avatar-img-div">
<image-preview
v-if="data_info.pkIsAnony === 'N'"
class="img"
:src="data_info.pkImgAddr"
></image-preview>
<image v-else class="img" :src="defaultAnonymousAvatar" mode="aspectFill"></image>
</view>
<view class="name">
{{ data_info.pkIsAnony === 'N' ? data_info.pkUserName : '匿名' }}
</view>
<view class="org">
{{ data_info.pkIsAnony === 'N' ? data_info.pkOrgIdName : '机构不显示' }}
</view>
</view>
</view>
</view>
<view class="matching-success-text">准备</view>
</template>
<uni-popup ref="popupRef" class="popup">
<view class="sheet_popup_div">
<view class="title_div">双人对战规则说明</view>
<view class="note_div">
{{ data_info.ruleDesc }}
</view>
</view>
</uni-popup>
<selectionInstitutions ref="selectionInstitutionsRef" @submit="selectionInstitutionsSubmit"></selectionInstitutions>
<uv-picker ref="anonyPickerRef" :columns="[anonyOptions]" keyName="label" @confirm="anonyConfirm"></uv-picker>
</view>
</template>
<script setup lang="ts">
import {ref, onMounted, reactive} from 'vue';
import common, {getUserInfo} from '@/common/common';
import {MatchTimeGenerator, formatTime} from './js/matching';
import {queryTraCompetitionInfoByPkUser} from '@/api/competition';
import {onLoad} from '@dcloudio/uni-app';
import {defaultAnonymousAvatar} from '@/enum';
import {pkAnonymousClose, pkAnonymousOpen, pkRankingList, pkRules, switchIcon} from '@/common/imgSvg';
import {queryTraUserAnonyByUserId, updateTraUserAnony} from '@/api/user';
import selectionInstitutions from '@/components/selection-institutions/index.vue';
import { ref, onMounted, reactive } from 'vue';
import common, { getUserInfo } from '@/common/common';
import { MatchTimeGenerator, formatTime } from './js/matching';
import { queryTraCompetitionInfoByPkUser } from '@/api/competition';
import { onLoad } from '@dcloudio/uni-app';
import { defaultAnonymousAvatar } from '@/enum';
import { pkAnonymousClose, pkAnonymousOpen, pkRankingList, pkRules, switchIcon } from '@/common/imgSvg';
import { queryTraUserAnonyByUserId, updateTraUserAnony } from '@/api/user';
import selectionInstitutions from '@/components/selection-institutions/index.vue';
import { nextTick } from 'process';
const data_info = reactive({
userId: '',
userName: '',
orgName: '全部', // 机构名称
orgId: 'A', // 机构的ID,全行是A
papersId: '', // 试卷id
isAnony: '', // 是否匿名 Y是N否
ruleDesc: '', // 规则说明
comName: '', // 标题
comId: '',
orgIdName: '',
pkIsAnony: '',
pkimgAddr: '',
pkUserName: '',
pkOrgIdName: ''
});
const data_info = reactive({
userId: '',
userName: '',
orgName: '全部', // 机构名称
orgId: 'A', // 机构的ID,全行是A
papersId: '', // 试卷id
isAnony: '', // 是否匿名 Y是N否
ruleDesc: '', // 规则说明
comName: '', // 标题
comId: '',
orgIdName: '', // 自己的机构名称
pkIsAnony: '',
pkImgAddr: '',
pkUserName: '',
pkOrgIdName: ''
});
const popupRef = ref(null);
const anonyPickerRef = ref(null);
const selectionInstitutionsRef = ref(null);
const challengeLoading = ref(false);
const isMatchingSuccess = ref(false); // 匹配成功状态
const challengeButtonStatus = ref(true); // 匹配按钮显示
const practiceTime = ref(0); // 匹配时间
let practiceTimeTimer: number | null = null; // 匹配时间计时器
let matchTimer: number | null = null; // 匹配成功定时器
const anonyOptions = [
{
value: 'Y',
label: '匿名'
},
{
value: 'N',
label: '不匿名'
}
];
// 开始挑战
const start_challenge = () => {
if (!challengeLoading.value) {
challengeLoading.value = true;
// 清除旧计时器
if (practiceTimeTimer) {
clearInterval(practiceTimeTimer);
}
// 开始计时
const practiceStartTime = new Date().getTime();
practiceTimeTimer = setInterval(() => {
const currentTime = Date.now();
practiceTime.value = Math.floor((currentTime - practiceStartTime) / 1000);
}, 1000);
const popupRef = ref(null);
const anonyPickerRef = ref(null);
const selectionInstitutionsRef = ref(null);
const challengeLoading = ref(false);
const isMatchingSuccess = ref(false); // 匹配成功状态
const challengeButtonStatus = ref(true); // 匹配按钮显示
const practiceTime = ref(0); // 匹配时间
let practiceTimeTimer: number | null = null; // 匹配时间计时器
let matchTimer: number | null = null; // 匹配成功定时器
const anonyOptions = [
{
value: 'Y',
label: '匿名'
},
{
value: 'N',
label: '不匿名'
}
];
// 开始挑战
const start_challenge = () => {
if (!challengeLoading.value) {
challengeLoading.value = true;
// 清除旧计时器
if (practiceTimeTimer) {
clearInterval(practiceTimeTimer);
}
// 开始计时
const practiceStartTime = new Date().getTime();
practiceTimeTimer = setInterval(() => {
const currentTime = Date.now();
practiceTime.value = Math.floor((currentTime - practiceStartTime) / 1000);
}, 1000);
// 生成匹配延迟时间
// const delayTime = 0;
const delayTime = MatchTimeGenerator.generateInSeconds();
// 生成匹配延迟时间
// const delayTime = 0;
const delayTime = MatchTimeGenerator.generateInSeconds();
// 匹配定时器
matchTimer = setTimeout(async () => {
try {
challengeButtonStatus.value = false;
const orgId = data_info.orgId
const papersId = data_info.papersId
const comId = data_info.comId
const pkDataBody = await queryTraCompetitionInfoByPkUser({
// orgId:510121,
// papersId:'COMPETITION_PAPERS0250640321542025101615442400000003107',
// comId:'TRA_COMPETITION_INFO0250640321542025101615442400000003099'
orgId, papersId, comId
});
const pkData = pkDataBody.body;
// 下面两行是给后端擦屁股,后端传回的数值为空
pkData.orgId = orgId
pkData.papersId = papersId
common.setPageCache('competition_pk', pkData);
// 1. 显示匹配成功状态
isMatchingSuccess.value = true;
uni.vibrateLong({
success: function () {
}
});
// 2. 执行匹配成功动画(等待1.5秒)
await new Promise((resolve) => setTimeout(resolve, 600));
// 3. 清除计时器
if (practiceTimeTimer) {
clearInterval(practiceTimeTimer);
practiceTimeTimer = null;
}
// 5. 跳转页面
common.navigateTo('/pages/competition/pk');
challengeLoading.value = true;
} catch (error) {
common.msg('队列繁忙,请稍后再试!');
// 出错处理
} finally {
resetChallengeState();
}
}, delayTime * 1000);
} else {
// 取消挑战
resetChallengeState();
}
};
// 匹配定时器
matchTimer = setTimeout(async () => {
try {
challengeButtonStatus.value = false;
const orgId = data_info.orgId;
const papersId = data_info.papersId;
const comId = data_info.comId;
const pkDataBody = await queryTraCompetitionInfoByPkUser({
// orgId:510121,
// papersId:'COMPETITION_PAPERS0250640321542025101615442400000003107',
// comId:'TRA_COMPETITION_INFO0250640321542025101615442400000003099'
orgId,
papersId,
comId
});
const pkData = pkDataBody.body;
// 下面两行是给后端擦屁股,后端传回的数值为空
pkData.orgId = orgId;
pkData.papersId = papersId;
pkData.userId = data_info.userId;
pkData.userName = data_info.userName;
data_info.orgIdName = pkData.orgIdName;
data_info.pkUserName = pkData.pkUserName;
data_info.pkOrgIdName = pkData.pkOrgIdName;
data_info.pkImgAddr = pkData.pkImgAddr;
data_info.pkIsAnony = pkData.pkIsAnony;
console.log('上一页数据', pkData);
common.setPageCache('competition_pk', pkData);
// 1. 显示匹配成功状态
isMatchingSuccess.value = true;
// 震动
uni.vibrateLong({
success: function () {}
});
// 2. 执行匹配成功动画(等待1.5秒)
await new Promise((resolve) => setTimeout(resolve, 600));
// 3. 清除计时器
if (practiceTimeTimer) {
clearInterval(practiceTimeTimer);
practiceTimeTimer = null;
}
// 5. 跳转页面
common.navigateTo('/pages/competition/pk');
challengeLoading.value = true;
} catch (error) {
common.msg('队列繁忙,请稍后再试!');
// 出错处理
} finally {
resetChallengeState();
}
}, delayTime * 100);
} else {
// 取消挑战
// resetChallengeState();
}
};
interface InstitutionItem {
orgName: string; // 机构名称,类型为字符串
orgId: string; // 机构ID,类型为字符串
parentId: string; // 父级ID,类型为字符串
children?: InstitutionItem[];
}
interface InstitutionItem {
orgName: string; // 机构名称,类型为字符串
orgId: string; // 机构ID,类型为字符串
parentId: string; // 父级ID,类型为字符串
children?: InstitutionItem[];
}
const selectionInstitutionsSubmit = (options: [InstitutionItem, ...InstitutionItem[]]) => {
// 缓存起来
common.setValue('cache_selection_institutions' + data_info.userId, options);
const lastItem = options.at(-1);
data_info.orgName = lastItem.orgName;
data_info.orgId = lastItem.orgId;
const selectionInstitutionsSubmit = (options: [InstitutionItem, ...InstitutionItem[]]) => {
// 缓存起来
common.setValue('cache_selection_institutions' + data_info.userId, options);
const lastItem = options.at(-1);
data_info.orgName = lastItem.orgName;
data_info.orgId = lastItem.orgId;
selectionInstitutionsRef.value.close();
};
const anonyConfirm = (e) => {
const originalIsAnony = data_info.isAnony;
const isAnony = e.value[0].value;
if (isAnony === data_info.isAnony) return;
data_info.isAnony = isAnony;
common.loading('修改中');
updateTraUserAnony({isAnony})
.then(() => {
setTimeout(() => common.msg('切换成功'), 400);
})
.catch(() => {
// 4. 请求失败:用初始值回滚
data_info.isAnony = originalIsAnony;
common.msg('切换失败,请重试'); // 可选:提示失败
})
.finally(() => {
common.hideLoading();
});
};
const click_top_icon = (type: number) => {
// 匿名切换
if (type === 1) {
anonyPickerRef.value.setIndexs([data_info.isAnony === 'Y' ? 0 : 1], true);
anonyPickerRef.value.open();
} else if (type === 2) {
// 打开规则说明
popupRef.value.open('bottom');
} else if (type === 3) {
// 去排行榜
common.navigateTo(`/pages/competition/ranking?papersId=${data_info.papersId}&orgId=${data_info.orgId}`)
}
};
// 统一重置状态的函数
const resetChallengeState = () => {
if (matchTimer) {
clearTimeout(matchTimer);
matchTimer = null;
}
if (practiceTimeTimer) {
clearInterval(practiceTimeTimer);
practiceTimeTimer = null;
}
challengeButtonStatus.value = true;
challengeLoading.value = false;
isMatchingSuccess.value = false;
practiceTime.value = 0;
};
selectionInstitutionsRef.value.close();
};
const anonyConfirm = (e) => {
const originalIsAnony = data_info.isAnony;
const isAnony = e.value[0].value;
if (isAnony === data_info.isAnony) return;
data_info.isAnony = isAnony;
common.loading('修改中');
updateTraUserAnony({ isAnony })
.then(() => {
setTimeout(() => common.msg('切换成功'), 400);
})
.catch(() => {
// 4. 请求失败:用初始值回滚
data_info.isAnony = originalIsAnony;
common.msg('切换失败,请重试'); // 可选:提示失败
})
.finally(() => {
common.hideLoading();
});
};
const click_top_icon = (type: number) => {
// 匿名切换
if (type === 1) {
anonyPickerRef.value.setIndexs([data_info.isAnony === 'Y' ? 0 : 1], true);
anonyPickerRef.value.open();
} else if (type === 2) {
// 打开规则说明
popupRef.value.open('bottom');
} else if (type === 3) {
// 去排行榜
common.navigateTo(`/pages/competition/ranking?papersId=${data_info.papersId}&orgId=${data_info.orgId}`);
}
};
// 统一重置状态的函数
const resetChallengeState = () => {
if (matchTimer) {
clearTimeout(matchTimer);
matchTimer = null;
}
if (practiceTimeTimer) {
clearInterval(practiceTimeTimer);
practiceTimeTimer = null;
}
challengeButtonStatus.value = true;
challengeLoading.value = false;
isMatchingSuccess.value = false;
practiceTime.value = 0;
};
onLoad((e) => {
const lastData = common.getPageCache('competition_list');
console.log('lastData', lastData);
const userInfo = getUserInfo();
Object.assign(data_info, {
...lastData,
userId: userInfo.userId,
userName: userInfo.userName,
gender: userInfo.gender,
papersId: e.papersId,
comId: e.comId
});
const selectOptions = common.getValue('cache_selection_institutions' + data_info.userId);
if (Array.isArray(selectOptions) && selectOptions.length > 0) {
const lastItem = selectOptions.at(-1);
data_info.orgName = lastItem.orgName;
data_info.orgId = lastItem.orgId;
// const orgTreeInit = common.getValue('orgTree');
// if (Array.isArray(orgTreeInit) && orgTreeInit.length > 0) {
// console.log('selectOptions', selectOptions);
// }
}
});
onLoad((e) => {
const lastData = common.getPageCache('competition_list');
console.log('lastData', lastData);
const userInfo = getUserInfo();
console.log('userInfo', userInfo);
Object.assign(data_info, {
...lastData,
userId: userInfo.userId,
userName: userInfo.userName,
gender: userInfo.gender,
papersId: e.papersId,
comId: e.comId
});
const selectOptions = common.getValue('cache_selection_institutions' + data_info.userId);
if (Array.isArray(selectOptions) && selectOptions.length > 0) {
const lastItem = selectOptions.at(-1);
data_info.orgName = lastItem.orgName;
data_info.orgId = lastItem.orgId;
// const orgTreeInit = common.getValue('orgTree');
// if (Array.isArray(orgTreeInit) && orgTreeInit.length > 0) {
// console.log('selectOptions', selectOptions);
// }
}
});
</script>
<style scoped lang="scss">
// 匹配成功样式动画
@import './style/matching-success.scss';
@import './style/matching-loading.scss';
@import './style/matching-prelude.scss';
// 匹配成功样式动画
@import './style/matching-success.scss';
@import './style/matching-loading.scss';
@import './style/matching-prelude.scss';
/* 容器样式 */
.max_page {
width: 100%;
min-height: 100vh;
background-image: url('@/static/images/competition/bg.png');
background-color: #000; /* 图片高度不足时的背景色 */
background-size: 100% auto; /* 宽度填满,高度自适应 */
background-position: center top; /* 图片顶部居中显示 */
background-repeat: no-repeat; /* 不重复平铺 */
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
}
/* 容器样式 */
.max_page {
width: 100%;
min-height: 100vh;
background-image: url('@/static/images/competition/bg.png');
background-color: #000; /* 图片高度不足时的背景色 */
background-size: 100% auto; /* 宽度填满,高度自适应 */
background-position: center top; /* 图片顶部居中显示 */
background-repeat: no-repeat; /* 不重复平铺 */
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
}
</style>
+2 -1
View File
@@ -138,7 +138,6 @@
// 计算对方分数
const opponentScore = computed(() => {
// 筛选出所有 asrTm <= 当前时长的项
const validItems = pkData.traCompetitionPkTimeRecordVos.filter((item) => item.asrTm <= practiceTime.value);
if (validItems.length > 0) {
@@ -413,6 +412,8 @@
// console.log('倒计时时间', practiceRemainingTime.value);
// Pk时间到
if (practiceRemainingTime.value <= 0) {
}
}, 1000);
};
+5 -4
View File
@@ -57,7 +57,7 @@
</view>
<view class="pk-msg-right-div">
<view class="avatar-div">
<image-preview v-if="pkData.isAnonyPk === 'N'" class="img" :src="pkData.imgPk"></image-preview>
<image-preview v-if="pkData.pkIsAnony === 'N'" class="img" :src="pkData.imgPk"></image-preview>
<image v-else class="img" :src="defaultAnonymousAvatar" mode="aspectFill"></image>
</view>
<view class="name-div">{{ pkData.userNamePk }}</view>
@@ -123,7 +123,7 @@
userIdPk: '', // 用户Id String
userNamePk: '', // 用户姓名 String
imgPk: '', // 对方头像 String
isAnonyPk: 'Y', // 匿名 String
pkIsAnony: 'Y', // 匿名 String
correctNubPk: 0, // 正确数 int
qnsNubPk: 0, // 总数 int
timeTakenPk: 0, // 用时 int
@@ -147,7 +147,8 @@
const pkDataInit = common.getPageCache('competition_pk_to_resulf');
Object.assign(pkData, {
userName: pkDataInit.userName,
isAnony: pkDataInit.isAnony
isAnony: pkDataInit.isAnony,
pkIsAnony: pkDataInit.pkIsAnony,
});
});
onMounted(() => {
@@ -168,7 +169,7 @@
userIdPk: userResult.userIdPk, // 用户Id String
userNamePk: userResult.userNamePk, // 用户姓名 String
imgPk: userResult.imgPk, // 用户姓名 String
pkIsAnony: userResult.pkIsAnony, // 匿名 String
pkIsAnony: userResult.isAnonyPk, // 匿名 String
correctNubPk: userResult.correctNubPk, // 正确数 int
qnsNubPk: userResult.qnsNubPk || 0, // 总数 int
timeTakenPk: userResult.timeTakenPk || 0, // 用时 int
@@ -1,19 +1,22 @@
<template>
<view class="item">
<view class="details_div">
<view class="img_div">
<image-preview class="img" :src="item.ossAddr"></image-preview>
</view>
<view class="right_div">
<view class="title ellipsis-text">
{{ item.examName }}
{{ item.comName }}
</view>
<view class="prompt_div">
<view class="prompt_div_left">
<view class="top">{{ item.flagQuery === 'Y' ? item.highestGrade : '--' || 0 }}</view>
<view class="top">{{ item.highestGrade }}</view>
<view class="bottom">历史最高分</view>
</view>
<view class="prompt_div_line"></view>
<view class="prompt_div_right" @click="show_list_click">
<view class="click_text">
{{ item.examCnt || 0 }}
{{ item.comNub || 0 }}
<uv-icon
class="click_text_icon"
:class="{ click_text_icon_action: show_list_state }"
@@ -23,7 +26,7 @@
:bold="true"
></uv-icon>
</view>
<view class="bottom">考试记录</view>
<view class="bottom">竞赛记录</view>
</view>
</view>
</view>
@@ -33,11 +36,11 @@
<view class="list_content">
<view class="list_small_item">
<image src="@/static/images/courseRecord/time.png" class="icon_img"></image>
<view class="prompt">考试时间{{ item_list.examStartTm }}</view>
<view class="prompt">竞赛时间{{ item_list.startTm }}</view>
</view>
<view class="list_small_item">
<image src="@/static/images/courseRecord/duration.png" class="icon_img"></image>
<view class="prompt">考试得分{{ item_list.flagQuery === 'Y' ? item_list.examGrade : '--' }}</view>
<view class="prompt">竞赛得分{{ item_list.comGrade ?? '--' }}</view>
</view>
</view>
<view class="fl1"></view>
@@ -66,16 +69,22 @@
}
});
const item = computed(() => props.item);
const fetchFunction = (params) => queryTraCompetitionRecordInfoPaging({ examId: item.value.examId, ...params });
const fetchFunction = (params) => queryTraCompetitionRecordInfoPaging({ comId: item.value.comId, ...params });
const { status, data_list, show_list_state, list_div_height, show_list_click, getData, clear } = useItemList(fetchFunction);
//
const click_list_item = (list_item) => {
const papersName = item.value.examName;
const flagQueryDetail = list_item.flagQueryDetail === '' ? list_item.flagQuery : list_item.flagQueryDetail;
const pkData = {
userName: 'x',
isAnony: list_item.isAnony
}
common.setPageCache('competition_pk_to_resulf', pkData);
common.navigateTo(
`/pages/examination/result?execId=${list_item.execId}&papersName=${papersName}&mode=record&flagQuery=${list_item.flagQuery}&flagQueryDetail=${flagQueryDetail}`
`/pages/competition/result?&execId=${list_item.execId}&orgId=${list_item.orgId}&papersId=${list_item.papersId}`
);
};
defineExpose({clear})
</script>
-15
View File
@@ -28,21 +28,6 @@
<image-preview :src="item.imgAddr" class="img" mode="widthFix"></image-preview>
</view>
</swiper-item>
<!-- <swiper-item class="">
<view class="swiper-item-div">
<image src="@/static/images/mascot/ma_2.png" class="img" mode="widthFix"></image>
</view>
</swiper-item>
<swiper-item class="">
<view class="swiper-item-div">
<image src="@/static/images/mascot/ma_3.png" class="img" mode="widthFix"></image>
</view>
</swiper-item>
<swiper-item class="">
<view class="swiper-item-div">
<image src="@/static/images/mascot/ma_4.png" class="img" mode="widthFix"></image>
</view>
</swiper-item> -->
</swiper>
<view class="doct_icon right" @click="doct_click(1)">
<view class="doct_icon_div">
+33 -10
View File
@@ -48,8 +48,8 @@
<view class="work_number_div">工号{{ user_info.loginName }}</view>
<view class="fl1"></view>
</view>
<view class="sign-in-div" v-if="user_info.checkInToday==='N'" @click="checkInClick">签到</view>
<view class="already-sign-in-div" v-if="user_info.checkInToday==='Y'" @click="checkInClick">已签</view>
<view class="sign-in-div" v-if="user_info.checkInToday === 'N'" @click="checkInClick">签到</view>
<view class="already-sign-in-div" v-if="user_info.checkInToday === 'Y'" @click="checkInClick">已签</view>
</view>
<!-- 用户学习时间信息 -->
<view class="study_information">
@@ -64,7 +64,7 @@
></uv-count-to>
</view>
</view>
<view class="item-interval"></view>
<view class="item-interval" @click="test()"></view>
<view class="item" @click="common.navigateTo('/pages/pointsAndRank/badge')">
<view class="top">徽章墙</view>
<view class="bottom">
@@ -97,7 +97,7 @@
</view>
</view>
<view class="scroll-view-item">
<view class="item" @click="goTest()">
<view class="item" @click="goTest()">
<image class="icon" src="@/static/images/me/report.png"></image>
<view class="title">AI陪练记录</view>
</view>
@@ -159,6 +159,8 @@
@confirm="avatarOnConfirm"
></avatar-cropper>
<checkIn ref="checkInRef" @checkInFun="checkInFun"></checkIn>
<badge ref="badgeRef"></badge>
<points ref="pointsRef" @confirm="pointsConfirm"></points>
</view>
</template>
@@ -179,6 +181,9 @@
import useUpdateAvatar from '@/composables/useUpdateAvatar';
import { queryCurrentStatus, queryCheckIn } from '@/api/pointsAndRank.js';
import checkIn from '@/components/points-and-badge/check-in';
import badge from '@/components/points-and-badge/badge';
import points from '@/components/points-and-badge/points';
import {pop_up_time} from '@/enum';
const { avatarCropperUrl, click_update_avatar, avatarCropperStatus, avatarOnCancel, avatarOnConfirm } = useUpdateAvatar({
success: (userData) => {
user_info.imageAddr = userData.imageAddr;
@@ -220,8 +225,10 @@
const socketStore = useSocketStore();
const badgeNumRef = ref(null);
const checkInRef = ref(null);
const badgeRef = ref(null);
const pointsRef = ref(null);
const currentPointsRef = ref(null);
const user_info = reactive({
userName: '',
userId: '',
@@ -231,17 +238,33 @@
checkInDays: 0 //
});
const goTest = () => {
console.log('goTest');
// common.navigateTo('/pages/test/index');
common.navigateTo('/pages/test/index');
};
const test = () => {
badgeRef.value.open();
};
//
const checkInClick = () => {
checkInRef.value.open(user_info.checkInToday, user_info.checkInDays);
};
//
const checkInFun = (status) => {
if(status) {
getCheckInInfo()
const checkInFun = (status, pointsList = [], badgesList = []) => {
if (status) {
getCheckInInfo();
}
pointsRef.value.open(pointsList, badgesList);
};
//
const pointsConfirm = (status, pointsList = [], badgesList = []) => {
console.log('xxx', status, pointsList, badgesList);
pointsRef.value.close();
if (badgesList.length > 0) {
nextTick(() => {
setTimeout(() => {
badgeRef.value.open(pointsList, badgesList);
}, pop_up_time);
});
}
};
const click_loginout = () => {
+2
View File
@@ -165,6 +165,7 @@
display: flex;
align-items: center;
justify-content: center;
flex-wrap: nowrap;
margin: 30rpx 0;
.title {
margin: 0 16rpx;
@@ -173,6 +174,7 @@
color: #ae723b;
text-align: center;
font-style: normal;
white-space: nowrap;
}
.img {
width: 242rpx;
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB