From 4dd19fa9982fc4a22ad138d71ab1c1593df3d387 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Wed, 25 Mar 2026 11:49:52 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=B7=BB=E5=8A=A0=E5=A4=9A=E6=B8=A0?= =?UTF-8?q?=E9=81=93=E6=89=93=E5=8C=85=E8=84=9A=E6=9C=AC=E5=B9=B6=E6=B8=85?= =?UTF-8?q?=E7=90=86=E9=A1=B9=E7=9B=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 build_channels.sh 脚本,支持华为、小米、OPPO、vivo、荣耀和谷歌渠道的交互式选择打包 - 脚本提供构建前清理、日志记录、错误处理和构建结果汇总功能 - 删除已过时的 AUDIO_PLAYBACK_FIX.md 文档,该文档记录的音频播放问题已修复 - 在 .gitignore 中添加 .claude/ 目录,避免 Claude Code 生成的文件被提交 --- .gitignore | 3 + AUDIO_PLAYBACK_FIX.md | 166 -------------- build_channels.sh | 519 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 522 insertions(+), 166 deletions(-) delete mode 100644 AUDIO_PLAYBACK_FIX.md create mode 100755 build_channels.sh diff --git a/.gitignore b/.gitignore index da0b68625..f99e16618 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ # See https://www.dartlang.org/guides/libraries/private-files +# Claude Code +.claude/ + # Files and directories created by pub .dart_tool/ .packages diff --git a/AUDIO_PLAYBACK_FIX.md b/AUDIO_PLAYBACK_FIX.md deleted file mode 100644 index 26dc4b0bb..000000000 --- a/AUDIO_PLAYBACK_FIX.md +++ /dev/null @@ -1,166 +0,0 @@ -# 音频播放Bug修复报告 - -## 问题描述 -在会议详情页面(meeting detail page)中,点击播放按钮(▶️)时没有任何反应,音频无法播放。 -影响两种类型的音频: -1. 录音的音频(recorded audio) -2. 导入的音频(imported audio) - -## 根本原因分析 - -### 1. 缺少播放器准备状态检查 -**问题:** `MeetingDetailsController` 中的 `_loadPlayerController()` 方法没有错误处理和状态跟踪。 -- 播放器初始化失败时没有任何提示 -- UI层无法知道播放器是否准备好 -- 文件路径错误或文件不存在时静默失败 - -### 2. UI层缺少状态验证 -**问题:** `meeting_details_view.dart` 中的播放按钮点击事件没有检查播放器状态。 -- 直接调用 `startPlayer()` 而不验证播放器是否已初始化 -- 没有异常捕获机制 -- 用户点击后没有任何反馈 - -### 3. 缺少日志记录 -**问题:** 没有足够的日志来追踪播放器初始化和播放过程。 -- 无法诊断失败原因 -- 难以定位问题发生的具体环节 - -## 修复方案 - -### 修改1: MeetingDetailsController - 添加播放器准备状态 -**文件:** `lib/modules/meeting/controllers/meeting_details_controller.dart` - -**变更:** -1. 添加 `isPlayerReady` 状态变量来跟踪播放器是否准备好 -2. 重写 `_loadPlayerController()` 方法,添加完整的错误处理 -3. 添加文件路径验证和文件存在性检查 -4. 添加详细的日志记录 -5. 添加用户友好的错误提示 - -**关键代码:** -```dart -// 新增状态变量 -RxBool isPlayerReady = false.obs; - -// 改进的播放器加载方法 -Future _loadPlayerController() async { - try { - final filepath = meetingData.value.filepath; - - // 验证文件路径 - if (filepath.isEmpty) { - Logger.error('音频文件路径为空,无法加载播放器'); - isPlayerReady.value = false; - return; - } - - // 验证文件是否存在 - final file = File(filepath); - if (!await file.exists()) { - Logger.error('音频文件不存在: $filepath'); - isPlayerReady.value = false; - Get.snackbar('error'.tr, '音频文件不存在', ...); - return; - } - - Logger.d('MeetingDetails', '开始加载音频播放器: $filepath'); - - // 准备播放器 - await playerController.preparePlayer(...); - - // 标记播放器已准备好 - isPlayerReady.value = true; - - } catch (e, stackTrace) { - Logger.error('加载音频播放器失败: $e'); - isPlayerReady.value = false; - Get.snackbar('error'.tr, '音频播放器加载失败', ...); - } -} -``` - -### 修改2: MeetingDetailsView - 添加播放前验证 -**文件:** `lib/modules/meeting/views/meeting_details_view.dart` - -**变更:** -1. 在播放按钮点击事件中添加播放器准备状态检查 -2. 添加异常捕获和用户提示 -3. 使用 async/await 确保异步操作正确执行 - -**关键代码:** -```dart -GestureDetector( - onTap: () async { - if (controller.meetingData.value.filepath.isEmpty) { - controller.downloadAudio(); - } else { - // 检查播放器是否已准备好 - if (!controller.isPlayerReady.value) { - Get.snackbar('error'.tr, '音频播放器未准备好,请稍候', ...); - return; - } - - try { - if (controller.isPlay.value) { - await controller.playerController.pausePlayer(); - } else { - await controller.playerController.startPlayer(); - } - } catch (e) { - Get.snackbar('error'.tr, '播放失败: ${e.toString()}', ...); - } - } - }, - ... -) -``` - -## 修复效果 - -### 修复前 -- ❌ 点击播放按钮无响应 -- ❌ 没有错误提示 -- ❌ 无法诊断问题 -- ❌ 用户体验差 - -### 修复后 -- ✅ 播放器初始化失败时有明确提示 -- ✅ 文件不存在时提示用户 -- ✅ 播放器未准备好时阻止播放并提示 -- ✅ 播放失败时显示具体错误信息 -- ✅ 完整的日志记录便于调试 -- ✅ 录音音频和导入音频都能正常播放 - -## 测试建议 - -1. **测试录音音频播放** - - 创建新的录音 - - 保存录音 - - 进入会议详情页面 - - 点击播放按钮验证播放功能 - -2. **测试导入音频播放** - - 导入外部音频文件 - - 进入会议详情页面 - - 点击播放按钮验证播放功能 - -3. **测试错误场景** - - 删除音频文件后尝试播放(应提示文件不存在) - - 在播放器未准备好时点击播放(应提示等待) - -## 技术细节 - -### 使用的技术 -- **audio_waveforms**: Flutter音频播放库 -- **GetX**: 状态管理和依赖注入 -- **Logger**: 日志记录工具 - -### 关键改进点 -1. **状态管理**: 使用 `RxBool isPlayerReady` 响应式状态 -2. **错误处理**: try-catch 包裹所有异步操作 -3. **用户反馈**: Get.snackbar 提供即时反馈 -4. **日志记录**: Logger 记录关键操作和错误 - -## 总结 -此次修复通过添加完善的状态检查、错误处理和用户反馈机制,彻底解决了音频播放按钮无响应的问题。修复后的代码更加健壮,用户体验显著提升。 - diff --git a/build_channels.sh b/build_channels.sh new file mode 100755 index 000000000..1533ff958 --- /dev/null +++ b/build_channels.sh @@ -0,0 +1,519 @@ +#!/bin/bash +# ================================================================= +# Voitrans 多渠道 Android 打包脚本 +# 用法: +# ./build_channels.sh # 打包所有渠道 +# ./build_channels.sh huawei google # 只打包指定渠道 +# ./build_channels.sh --clean # 清理后再打包所有渠道 +# ./build_channels.sh --list # 列出所有支持的渠道 +# ================================================================= + +set -euo pipefail + +# ---------- 常量定义 ---------- +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ENV_FILE="${SCRIPT_DIR}/.env" +PUBSPEC_FILE="${SCRIPT_DIR}/pubspec.yaml" +OUTPUT_DIR="/Users/liuxiamin/work/build_outputs" +APP_NAME="Voitrans" +BUILD_DATE=$(date +"%m%d") + +# ---------- 渠道配置 ---------- +# 渠道名列表(保证顺序) +CHANNEL_NAMES="huawei xiaomi oppo vivo honor google" + +# 根据渠道名获取 .env 中的 Channel 值 +get_channel_env_value() { + case "$1" in + huawei) echo "CHINA_HUAWEI" ;; + xiaomi) echo "CHINA_XIAOMI" ;; + oppo) echo "CHINA_OPPO" ;; + vivo) echo "CHINA_VIVO" ;; + honor) echo "CHINA_HONOR" ;; + google) echo "GLOBAL_GOOGLE" ;; + *) echo "" ;; + esac +} + +# 根据渠道名获取构建类型 +get_build_type() { + case "$1" in + google) echo "aab" ;; + *) echo "apk" ;; + esac +} + +# 校验渠道名是否有效 +is_valid_channel() { + case "$1" in + huawei|xiaomi|oppo|vivo|honor|google) return 0 ;; + *) return 1 ;; + esac +} + +# 构建结果追踪 +SUCCESS_CHANNELS="" +FAILED_CHANNELS="" + +# ---------- 颜色输出 ---------- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# ---------- 辅助函数 ---------- +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[OK]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# 提取版本号(从 pubspec.yaml 的 version 字段,去掉构建号) +get_version() { + grep '^version:' "$PUBSPEC_FILE" | sed 's/version: *//' | cut -d'+' -f1 +} + +# 备份 .env +backup_env() { + cp "$ENV_FILE" "${ENV_FILE}.bak" + log_info "已备份 .env -> .env.bak" +} + +# 恢复 .env +restore_env() { + if [ -f "${ENV_FILE}.bak" ]; then + mv "${ENV_FILE}.bak" "$ENV_FILE" + log_info "已恢复 .env 文件" + fi +} + +# 修改 .env 中的 Channel 值(兼容 macOS sed) +set_channel() { + local channel_value="$1" + if [ "$(uname)" = "Darwin" ]; then + sed -i '' "s/^Channel=.*/Channel=${channel_value}/" "$ENV_FILE" + else + sed -i "s/^Channel=.*/Channel=${channel_value}/" "$ENV_FILE" + fi +} + +# 列出所有渠道 +list_channels() { + echo "" + echo "支持的渠道列表:" + echo " 序号 渠道名 .env值 构建格式" + echo " ---- -------- ---------------- --------" + local idx=1 + for ch in $CHANNEL_NAMES; do + local env_val + env_val=$(get_channel_env_value "$ch") + local build_type + build_type=$(get_build_type "$ch") + printf " [%d] %-10s %-18s %s\n" "$idx" "$ch" "$env_val" "$build_type" + idx=$((idx + 1)) + done + echo "" +} + +# 交互式选择渠道(上下键移动,空格勾选,Enter确认) +interactive_select() { + # 构建菜单项数组 + local items="全部打包" + for ch in $CHANNEL_NAMES; do + local bt + bt=$(get_build_type "$ch") + items="${items}|${ch} (${bt})" + done + + local total_items=7 # 全部打包 + 6个渠道 + local cursor=0 # 当前光标位置 + # 选中状态:0=未选 1=已选(用空格分隔的列表,索引0-6) + local selected="0 0 0 0 0 0 0" + + # 获取第n项的选中状态 + get_selected() { + echo "$selected" | cut -d' ' -f$(($1 + 1)) + } + + # 切换第n项的选中状态 + toggle_selected() { + local idx=$1 + local new_selected="" + local i=0 + for val in $selected; do + if [ $i -eq $idx ]; then + if [ "$val" = "0" ]; then + # 如果选中"全部打包",取消其他所有 + if [ $idx -eq 0 ]; then + selected="1 0 0 0 0 0 0" + return + fi + val="1" + else + val="0" + fi + fi + # 选中单个渠道时,取消"全部打包" + if [ $idx -ne 0 ] && [ $i -eq 0 ]; then + val="0" + fi + if [ -z "$new_selected" ]; then + new_selected="$val" + else + new_selected="$new_selected $val" + fi + i=$((i + 1)) + done + selected="$new_selected" + } + + # 绘制菜单 + draw_menu() { + # 移动光标到菜单起始位置 + local i=0 + printf "\033[${total_items}A" # 向上移动 + + local item_idx=0 + # 保存IFS并用|分割 + local OLD_IFS="$IFS" + IFS='|' + for item in $items; do + local marker=" " + local sel + sel=$(get_selected $item_idx) + if [ "$sel" = "1" ]; then + marker="✓" + fi + + if [ $item_idx -eq $cursor ]; then + # 高亮当前行 + printf "\033[2K ${GREEN}▸ [${marker}] ${item}${NC}\n" + else + printf "\033[2K [${marker}] ${item}\n" + fi + item_idx=$((item_idx + 1)) + done + IFS="$OLD_IFS" + } + + # 显示标题 + echo "" + echo "╔══════════════════════════════════════════╗" + echo "║ Voitrans 多渠道打包工具 ║" + echo "╚══════════════════════════════════════════╝" + echo "" + echo " ↑/↓ 移动 空格 勾选 Enter 确认 q 退出" + echo "" + + # 首次绘制菜单(先输出占位行) + local i=0 + while [ $i -lt $total_items ]; do + echo "" + i=$((i + 1)) + done + draw_menu + + # 读取键盘输入 + while true; do + # 读取单个字符(静默模式) + IFS= read -rsn1 key + + case "$key" in + $'\x1b') + # 读取转义序列的剩余部分 + read -rsn2 seq + case "$seq" in + '[A') # 上箭头 + if [ $cursor -gt 0 ]; then + cursor=$((cursor - 1)) + else + cursor=$((total_items - 1)) + fi + ;; + '[B') # 下箭头 + if [ $cursor -lt $((total_items - 1)) ]; then + cursor=$((cursor + 1)) + else + cursor=0 + fi + ;; + esac + ;; + ' ') # 空格:切换选中 + toggle_selected $cursor + ;; + '') # Enter:确认 + break + ;; + 'q'|'Q') # 退出 + echo "" + echo "已取消" + exit 0 + ;; + esac + draw_menu + done + + # 根据选中状态构建渠道列表 + if [ "$(get_selected 0)" = "1" ]; then + SELECTED_CHANNELS="$CHANNEL_NAMES" + else + SELECTED_CHANNELS="" + local idx=1 + for ch in $CHANNEL_NAMES; do + if [ "$(get_selected $idx)" = "1" ]; then + SELECTED_CHANNELS="${SELECTED_CHANNELS} ${ch}" + fi + idx=$((idx + 1)) + done + SELECTED_CHANNELS=$(echo "$SELECTED_CHANNELS" | sed 's/^ *//') + fi + + if [ -z "$SELECTED_CHANNELS" ]; then + log_error "未选择任何渠道" + exit 1 + fi + + echo "" + log_info "已选择: ${SELECTED_CHANNELS}" +} + +# ---------- 核心构建函数 ---------- +build_channel() { + local channel_name="$1" + local env_value + env_value=$(get_channel_env_value "$channel_name") + local build_type + build_type=$(get_build_type "$channel_name") + local version + version=$(get_version) + + local output_filename="${APP_NAME}_${BUILD_DATE}_${version}_${channel_name}.${build_type}" + + echo "" + echo "========================================" + log_info "开始构建渠道: ${channel_name} (${env_value})" + log_info "构建类型: ${build_type}" + log_info "输出文件: ${output_filename}" + echo "========================================" + + # 1. 修改 .env 中的 Channel 值 + set_channel "$env_value" + log_info "已设置 Channel=${env_value}" + + # 2. 执行 flutter build + local build_start + build_start=$(date +%s) + local build_result=0 + local source_file="" + + if [ "$build_type" = "apk" ]; then + flutter build apk \ + --obfuscate \ + --split-debug-info=./build/app/outputs/symbols \ + 2>&1 | tee "${OUTPUT_DIR}/build_${channel_name}.log" || build_result=$? + source_file="${SCRIPT_DIR}/build/app/outputs/flutter-apk/app-release.apk" + else + flutter build aab \ + --obfuscate \ + --split-debug-info=./build/app/outputs/symbols \ + 2>&1 | tee "${OUTPUT_DIR}/build_${channel_name}.log" || build_result=$? + source_file="${SCRIPT_DIR}/build/app/outputs/bundle/release/app-release.aab" + fi + + local build_end + build_end=$(date +%s) + local build_duration=$((build_end - build_start)) + + # 3. 检查构建结果 + if [ $build_result -ne 0 ]; then + log_error "渠道 ${channel_name} 构建失败!(耗时 ${build_duration}秒)" + FAILED_CHANNELS="${FAILED_CHANNELS} ${channel_name}" + return 1 + fi + + # 4. 复制并重命名输出文件 + if [ -f "$source_file" ]; then + cp "$source_file" "${OUTPUT_DIR}/${output_filename}" + local file_size + file_size=$(du -h "${OUTPUT_DIR}/${output_filename}" | cut -f1) + # 构建成功,删除日志文件 + rm -f "${OUTPUT_DIR}/build_${channel_name}.log" + log_success "渠道 ${channel_name} 构建成功!(耗时 ${build_duration}秒, 大小 ${file_size})" + log_success "输出: ${OUTPUT_DIR}/${output_filename}" + SUCCESS_CHANNELS="${SUCCESS_CHANNELS} ${channel_name}:${build_duration}" + else + log_error "构建产物未找到: ${source_file}" + FAILED_CHANNELS="${FAILED_CHANNELS} ${channel_name}" + return 1 + fi +} + +# ---------- 主流程 ---------- +main() { + local total_start + total_start=$(date +%s) + + # 参数解析 + local do_clean=false + local channels_to_build="" + + for arg in "$@"; do + case "$arg" in + --list|-l) + list_channels + exit 0 + ;; + --clean|-c) + do_clean=true + ;; + --help|-h) + echo "用法: $0 [选项] [渠道名...]" + echo "" + echo "选项:" + echo " --list, -l 列出所有支持的渠道" + echo " --clean, -c 构建前先执行 flutter clean" + echo " --help, -h 显示帮助信息" + echo "" + echo "示例:" + echo " $0 # 打包所有渠道" + echo " $0 huawei google # 只打包华为和谷歌" + echo " $0 --clean # 清理后打包所有渠道" + exit 0 + ;; + *) + if ! is_valid_channel "$arg"; then + log_error "未知渠道: $arg" + list_channels + exit 1 + fi + channels_to_build="${channels_to_build} ${arg}" + ;; + esac + done + + # 如果没有指定渠道,进入交互选择模式 + if [ -z "$channels_to_build" ]; then + interactive_select + channels_to_build="$SELECTED_CHANNELS" + fi + # 去掉前导空格 + channels_to_build=$(echo "$channels_to_build" | sed 's/^ *//') + + local version + version=$(get_version) + + # 计算渠道总数 + local total=0 + for _ in $channels_to_build; do + total=$((total + 1)) + done + + # 显示构建计划 + echo "" + echo "╔══════════════════════════════════════════╗" + echo "║ Voitrans 多渠道打包工具 ║" + echo "╚══════════════════════════════════════════╝" + echo "" + log_info "构建渠道: ${channels_to_build}" + log_info "版本号: ${version}" + log_info "构建日期: ${BUILD_DATE}" + log_info "输出目录: ${OUTPUT_DIR}" + log_info "共 ${total} 个渠道" + + # 前置检查 + if ! command -v flutter &> /dev/null; then + log_error "flutter 命令未找到,请确保 Flutter SDK 已安装并配置 PATH" + exit 1 + fi + + if [ ! -f "$ENV_FILE" ]; then + log_error ".env 文件不存在: $ENV_FILE" + exit 1 + fi + + # 创建输出目录 + mkdir -p "$OUTPUT_DIR" + + # 可选:flutter clean + if [ "$do_clean" = true ]; then + log_info "执行 flutter clean..." + flutter clean + log_info "执行 flutter pub get..." + flutter pub get + fi + + # 备份 .env 并注册 trap(确保异常退出也能恢复) + backup_env + trap restore_env EXIT + + # 遍历构建 + local current=0 + + for channel in $channels_to_build; do + current=$((current + 1)) + echo "" + echo "████████████████████████████████████████" + echo " 进度: [${current}/${total}] 渠道: ${channel}" + echo "████████████████████████████████████████" + + build_channel "$channel" || true + done + + # ---------- 构建结果汇总 ---------- + local total_end + total_end=$(date +%s) + local total_duration=$((total_end - total_start)) + local total_min=$((total_duration / 60)) + local total_sec=$((total_duration % 60)) + + # 统计成功/失败数量 + local success_count=0 + local fail_count=0 + for _ in $SUCCESS_CHANNELS; do + success_count=$((success_count + 1)) + done + for _ in $FAILED_CHANNELS; do + fail_count=$((fail_count + 1)) + done + + echo "" + echo "╔══════════════════════════════════════════╗" + echo "║ 构 建 结 果 汇 总 ║" + echo "╠══════════════════════════════════════════╣" + + if [ $success_count -gt 0 ]; then + echo "║ 成功: ║" + for item in $SUCCESS_CHANNELS; do + local name="${item%%:*}" + local dur="${item##*:}" + printf "║ ✅ %-12s (%s秒) ║\n" "$name" "$dur" + done + fi + + if [ $fail_count -gt 0 ]; then + echo "║ 失败: ║" + for name in $FAILED_CHANNELS; do + printf "║ ❌ %-12s ║\n" "$name" + done + fi + + echo "╠══════════════════════════════════════════╣" + printf "║ 成功: %d/%d 总耗时: %d分%d秒 ║\n" \ + "$success_count" "$total" "$total_min" "$total_sec" + echo "╚══════════════════════════════════════════╝" + + # 列出输出目录中的安装包文件 + if [ $success_count -gt 0 ]; then + echo "" + log_info "输出文件列表:" + ls -lh "${OUTPUT_DIR}/${APP_NAME}_${BUILD_DATE}"_* 2>/dev/null || true + fi + + # 如果有失败的渠道,退出码为 1 + if [ $fail_count -gt 0 ]; then + exit 1 + fi +} + +main "$@"