Skip to content
Toggle navigation
Toggle navigation
This project
Loading...
Sign in
ethan
/
appframe
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Network
Compare
Branches
Tags
Commit 02ea609e
authored
2026-07-06 17:45:07 +0800
by
ethanlamzs
Browse Files
Options
Browse Files
Tag
Download
Plain Diff
Merge branch 'feature-2607-pay' into feature-2604-menu-iosvip
2 parents
24c81ee0
333e28ac
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
905 additions
and
77 deletions
android/app/build.gradle.kts
android_build_brand.ps1
build_ios.sh
lib/bloc/login_phone_cubit.dart
lib/bloc/web_cubit.dart
lib/config/constant.dart
lib/config/locator.dart
lib/data/repositories/message/app_info_handler.dart
lib/data/repositories/message/xe_create_order_handler.dart
lib/data/repositories/user_auth_repository.dart
lib/main.dart
lib/ui/pages/login_phone_page.dart
lib/ui/pages/setting/setting_page.dart
lib/ui/pages/web_page.dart
lib/utils/video_util.dart
pubspec.yaml
android/app/build.gradle.kts
View file @
02ea609
...
...
@@ -102,7 +102,7 @@ dependencies {
// XiaoMi
//implementation("com.tencent.timpush:xiaomi:8.7.7201")
// OPPO
implementation("com.tencent.timpush:oppo:8.8.7357")
//
implementation("com.tencent.timpush:oppo:8.8.7357")
// vivo
//implementation("com.tencent.timpush:vivo:8.8.7357")
// Honor
...
...
android_build_brand.ps1
0 → 100644
View file @
02ea609
#################################################
# 脚本代码中不能含有中文,否则默认PowerShell环境可能会报错
#################################################
# 1. 读取脚本参数
#-------------------------------------------
param
(
# app打包环境:dev, prod
[
Parameter
(
Mandatory
=
$true
)]
[
ValidateSet
(
"dev"
,
"pro"
)]
[
string
]
$appenv
,
# App版本,例如:1.0.2512181
[
Parameter
(
Mandatory
=
$true
)]
[
string
]
$version
,
# 品牌, 例如: vivo, oppo, xiaomi, honor
[
Parameter
(
Mandatory
=
$true
)]
[
string
]
$brand
,
# 下载地址
[
string
]
$url
=
"https://bxe.obs.cn-north-4.myhuaweicloud.com/fronts/material/xehybrid/assets/basepkg/base-
$appenv
.zip"
,
# 下载保存文件的路径
[
string
]
$downloadPath
=
"C:
\U
sers
\t
ang-
\S
tudioProjects
\a
ppframe
\a
ssets
\b
ase-
$appenv
.zip"
,
# App路径
[
string
]
$appPath
=
"C:
\U
sers
\t
ang-
\S
tudioProjects
\a
ppframe"
)
# 2. 验证目录存在
#-------------------------------------------
if
(
-not
(
Test-Path
$appPath
))
{
Write-Error
"appPath not exists:
$appPath
"
exit
1
}
# 确保下载目录存在
$downloadDir
=
Split-Path
$downloadPath
-Parent
if
(
-not
(
Test-Path
$downloadDir
))
{
Write-Error
"the path for save file does not exists:
$appPath
"
exit
1
}
# 3. 下载文件(带错误处理)
#-------------------------------------------
try
{
Write-Host
"downloading from :
$url
"
-ForegroundColor Green
Write-Host
"save to :
$downloadPath
"
-ForegroundColor Green
Invoke-WebRequest -Uri
$url
-OutFile
$downloadPath
-ErrorAction Stop
if
(
Test-Path
$downloadPath
)
{
Write-Host
"download success!"
-ForegroundColor Green
}
else
{
Write-Error
"download failed!"
exit
1
}
}
catch
{
Write-Error
"downloading failed:
$_
"
exit
1
}
# 4. 执行 flutter build
#-------------------------------------------
try
{
Set-Location
-Path
$appPath
-ErrorAction Stop
Write-Host
"flutter building ..."
-ForegroundColor Green
Write-Host
"appenv:
$appenv
, version:
$version
, brand:
$brand
"
-ForegroundColor Cyan
flutter build apk
`
--release
`
--split-per-abi
`
--target-platform android-arm64
`
--dart-define
=
env
=
$appenv
`
--dart-define
=
version
=
$version
`
--dart-define
=
brand
=
$brand
if
(
$LASTEXITCODE
-eq 0
)
{
Write-Host
"build success!"
-ForegroundColor Green
}
else
{
Write-Error
"build failed!"
exit
$LASTEXITCODE
}
}
catch
{
Write-Error
"building failed:
$_
"
exit
1
}
# 5. 复制生成新命名的APK文件
# "build\app\outputs\flutter-apk\app-arm64-v8a-release.apk"
#-------------------------------------------
try
{
$apkOutputDir
=
Join-Path
$appPath
"build
\a
pp
\o
utputs
\f
lutter-apk"
$sourceApk
=
Join-Path
$apkOutputDir
"app-arm64-v8a-release.apk"
$newApkName
=
"bxeapp-
$appenv
-
$version
-
$brand
.apk"
$destApkPath
=
Join-Path
$apkOutputDir
$newApkName
if
(
Test-Path
$sourceApk
)
{
Copy-Item
-Path
$sourceApk
-Destination
$destApkPath
-Force
Write-Host
"Copied and renamed:
$newApkName
"
-ForegroundColor Yellow
}
else
{
Write-Warning
"Source APK not found:
$sourceApk
"
}
}
catch
{
Write-Error
"Failed to copy and rename APK:
$_
"
}
build_ios.sh
View file @
02ea609
...
...
@@ -54,3 +54,6 @@ flutter run --dart-define=env=$env --dart-define=version=$_main_ver$_ver -d 0000
#flutter run --dart-define=env=$env --dart-define=version=$_main_ver$_ver -d 00008030-001C75810E42402E
flutter build ios --build-number
=
$_ver
lib/bloc/login_phone_cubit.dart
View file @
02ea609
...
...
@@ -13,6 +13,7 @@ import 'package:shared_preferences/shared_preferences.dart';
class
LoginPhoneState
extends
Equatable
{
final
bool
agreed
;
final
bool
showAgreed
;
final
bool
showGuide
;
final
bool
allowSend
;
final
int
seconds
;
...
...
@@ -20,6 +21,7 @@ class LoginPhoneState extends Equatable {
const
LoginPhoneState
({
this
.
agreed
=
false
,
this
.
showAgreed
=
false
,
this
.
showGuide
=
false
,
this
.
allowSend
=
true
,
this
.
seconds
=
0
,
});
...
...
@@ -27,12 +29,14 @@ class LoginPhoneState extends Equatable {
LoginPhoneState
copyWith
({
bool
?
agreed
,
bool
?
showAgreed
,
bool
?
showGuide
,
bool
?
allowSend
,
int
?
seconds
,
})
{
return
LoginPhoneState
(
agreed:
agreed
??
this
.
agreed
,
showAgreed:
showAgreed
??
this
.
showAgreed
,
showGuide:
showGuide
??
this
.
showGuide
,
allowSend:
allowSend
??
this
.
allowSend
,
seconds:
seconds
??
this
.
seconds
,
);
...
...
@@ -42,6 +46,7 @@ class LoginPhoneState extends Equatable {
List
<
Object
?>
get
props
=>
[
agreed
,
showAgreed
,
showGuide
,
allowSend
,
seconds
,
];
...
...
@@ -103,7 +108,16 @@ class LoginPhoneCubit extends Cubit<LoginPhoneState> {
return
;
}
if
(
result
[
'code'
]
!=
0
)
{
Fluttertoast
.
showToast
(
msg:
result
[
'error'
],
gravity:
ToastGravity
.
TOP
,
backgroundColor:
Colors
.
red
);
final
err
=
result
[
'error'
]
??
''
;
// 手机号未注册时,打开指引界面
if
(
err
.
contains
(
'手机号尚未注册用户'
))
{
if
(!
state
.
showGuide
)
{
emit
(
state
.
copyWith
(
showGuide:
true
));
}
return
;
}
Fluttertoast
.
showToast
(
msg:
err
,
gravity:
ToastGravity
.
TOP
,
backgroundColor:
Colors
.
red
);
return
;
}
...
...
@@ -168,6 +182,10 @@ class LoginPhoneCubit extends Cubit<LoginPhoneState> {
emit
(
state
.
copyWith
(
showAgreed:
false
));
}
void
cancelShowGuide
()
{
emit
(
state
.
copyWith
(
showGuide:
false
));
}
void
goLoginMain
()
{
router
.
go
(
'/loginMain'
);
}
...
...
lib/bloc/web_cubit.dart
View file @
02ea609
...
...
@@ -22,6 +22,7 @@ import 'package:fluttertoast/fluttertoast.dart';
import
'package:path_provider/path_provider.dart'
;
import
'package:permission_handler/permission_handler.dart'
;
import
'package:shared_preferences/shared_preferences.dart'
;
import
'package:url_launcher/url_launcher.dart'
;
import
'package:uuid/uuid.dart'
;
import
'package:volume_controller/volume_controller.dart'
;
import
'package:webview_flutter/webview_flutter.dart'
;
...
...
@@ -34,6 +35,7 @@ class WebState extends Equatable {
final
bool
loaded
;
final
bool
isUpgrading
;
final
bool
suggestUpgrade
;
final
bool
suggestAppUpgrade
;
final
String
title
;
final
int
titleColor
;
...
...
@@ -82,6 +84,7 @@ class WebState extends Equatable {
this
.
loaded
=
false
,
this
.
isUpgrading
=
false
,
this
.
suggestUpgrade
=
false
,
this
.
suggestAppUpgrade
=
false
,
this
.
title
=
''
,
this
.
titleColor
=
0xFFFFFFFF
,
this
.
bgColor
=
0xFF7691FA
,
...
...
@@ -113,6 +116,7 @@ class WebState extends Equatable {
bool
?
loaded
,
bool
?
isUpgrading
,
bool
?
suggestUpgrade
,
bool
?
suggestAppUpgrade
,
String
?
title
,
int
?
titleColor
,
int
?
bgColor
,
...
...
@@ -143,6 +147,7 @@ class WebState extends Equatable {
loaded:
loaded
??
this
.
loaded
,
isUpgrading:
isUpgrading
??
this
.
isUpgrading
,
suggestUpgrade:
suggestUpgrade
??
this
.
suggestUpgrade
,
suggestAppUpgrade:
suggestAppUpgrade
??
this
.
suggestAppUpgrade
,
title:
title
??
this
.
title
,
titleColor:
titleColor
??
this
.
titleColor
,
bgColor:
bgColor
??
this
.
bgColor
,
...
...
@@ -176,6 +181,7 @@ class WebState extends Equatable {
loaded
,
isUpgrading
,
suggestUpgrade
,
suggestAppUpgrade
,
title
,
titleColor
,
bgColor
,
...
...
@@ -244,6 +250,7 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
// 遮罩界面
emit
(
state
.
copyWith
(
isUpgrading:
true
));
await
_downloadH5Zip
(
configVersion
,
downloadUrl
);
// 此处提前设置了新的H5业务包版本号,接下来启动本地服务的时候,解压对应版本的zip包
_setH5Version
(
configVersion
);
// 下载完成后取消遮罩,继续初始化其它数据
emit
(
state
.
copyWith
(
isUpgrading:
false
));
...
...
@@ -251,6 +258,7 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
// 后台下载,完成后提示用户
_downloadH5Zip
(
configVersion
,
downloadUrl
).
then
(
(
value
)
{
// 此处提前设置了新的H5业务包版本号,下次启动本地服务的时候,解压对应版本的zip包
_setH5Version
(
configVersion
);
emit
(
state
.
copyWith
(
suggestUpgrade:
true
));
},
...
...
@@ -300,10 +308,18 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
String
version
=
response
.
data
[
'version'
]
as
String
;
String
force
=
response
.
data
[
'force'
]
as
String
;
String
zip
=
response
.
data
[
'zip'
]
as
String
;
String
appversionCheck
=
response
.
data
[
'appversionCheck'
]
as
String
?
??
'0'
;
String
appversionAndroid
=
response
.
data
[
'appversionAndroid'
]
as
String
?
??
''
;
String
appversionIos
=
response
.
data
[
'appversionIos'
]
as
String
?
??
''
;
return
{
'version'
:
version
,
'force'
:
force
,
'zip'
:
'
$zip$version
.zip'
,
'appversionCheck'
:
appversionCheck
,
'appversionAndroid'
:
appversionAndroid
,
'appversionIos'
:
appversionIos
,
};
}
finally
{
dio
.
close
(
force:
true
);
...
...
@@ -704,34 +720,198 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
///
/// 升级提示
///
void
suggestUpgrade
(
BuildContext
ctx
)
{
showDialog
(
context:
ctx
,
Future
<
void
>
suggestUpgrade
(
BuildContext
context
)
async
{
// showDialog(
// context: ctx,
// barrierDismissible: false,
// builder: (BuildContext context) {
// return AlertDialog(
// title: Text('温馨提示'),
// content: Text('资源已更新为最新版本,是否现在进行加载?'),
// actions: <Widget>[
// TextButton(
// child: Text('取消'),
// onPressed: () {
// Navigator.of(context).pop();
// emit(state.copyWith(suggestUpgrade: false));
// },
// ),
// TextButton(
// child: Text('确定'),
// onPressed: () {
// Navigator.of(context).pop();
// emit(state.copyWith(suggestUpgrade: false));
// router.go('/reload');
// },
// ),
// ],
// );
// },
// );
final
result
=
await
showDialog
(
context:
context
,
barrierDismissible:
false
,
builder:
(
BuildContext
context
)
{
return
AlertDialog
(
title:
Text
(
'温馨提示'
),
content:
Text
(
'资源已更新为最新版本,是否现在进行加载?'
),
actions:
<
Widget
>[
TextButton
(
child:
Text
(
'取消'
),
onPressed:
()
{
Navigator
.
of
(
context
).
pop
();
emit
(
state
.
copyWith
(
suggestUpgrade:
false
));
},
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
all
(
Radius
.
circular
(
5
),
),
TextButton
(
child:
Text
(
'确定'
),
onPressed:
()
{
Navigator
.
of
(
context
).
pop
();
emit
(
state
.
copyWith
(
suggestUpgrade:
false
));
router
.
go
(
'/reload'
);
},
),
title:
Text
(
'资源包更新'
,
style:
TextStyle
(
fontSize:
17
,
color:
Color
(
0xFF000000
),
// fontWeight: FontWeight.bold,
),
textAlign:
TextAlign
.
center
,
),
content:
Text
.
rich
(
TextSpan
(
children:
[
TextSpan
(
text:
'资源更新包已准备就绪,是否立即加载?'
,
style:
TextStyle
(
color:
Color
(
0xFF666666
),
fontSize:
14
),
),
],
),
),
actions:
[
Table
(
children:
[
TableRow
(
children:
[
TableCell
(
child:
TextButton
(
onPressed:
()
{
Navigator
.
of
(
context
).
pop
();
},
style:
TextButton
.
styleFrom
(
foregroundColor:
Color
(
0xFF666666
),
textStyle:
TextStyle
(
fontSize:
17
),
padding:
EdgeInsets
.
zero
,
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
zero
,
),
),
child:
Text
(
'稍后再说'
),
),
),
TableCell
(
child:
TextButton
(
onPressed:
()
{
Navigator
.
of
(
context
).
pop
(
'OK'
);
},
style:
TextButton
.
styleFrom
(
foregroundColor:
Color
(
0xFF7691FA
),
textStyle:
TextStyle
(
fontSize:
17
),
minimumSize:
Size
.
fromHeight
(
40
),
padding:
EdgeInsets
.
zero
,
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
zero
,
),
),
child:
Text
(
'立即加载'
),
),
),
],
),
],
),
],
);
},
);
emit
(
state
.
copyWith
(
suggestUpgrade:
false
));
if
(
result
!=
null
)
{
router
.
go
(
'/reload'
);
}
}
Future
<
void
>
suggestAppUpdate
(
BuildContext
context
)
async
{
final
result
=
await
showDialog
(
context:
context
,
barrierDismissible:
false
,
builder:
(
BuildContext
context
)
{
return
AlertDialog
(
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
all
(
Radius
.
circular
(
5
),
),
),
title:
Text
(
'发现新版本'
,
style:
TextStyle
(
fontSize:
17
,
color:
Color
(
0xFF000000
),
// fontWeight: FontWeight.bold,
),
textAlign:
TextAlign
.
center
,
),
content:
Text
.
rich
(
TextSpan
(
children:
[
TextSpan
(
text:
'已有新版 APP 可供下载,建议您立即下载更新以获得更好体验。'
,
style:
TextStyle
(
color:
Color
(
0xFF666666
),
fontSize:
14
),
),
],
),
),
actions:
[
Table
(
children:
[
TableRow
(
children:
[
TableCell
(
child:
TextButton
(
onPressed:
()
{
Navigator
.
of
(
context
).
pop
();
},
style:
TextButton
.
styleFrom
(
foregroundColor:
Color
(
0xFF666666
),
textStyle:
TextStyle
(
fontSize:
17
),
padding:
EdgeInsets
.
zero
,
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
zero
,
),
),
child:
Text
(
'稍后再说'
),
),
),
TableCell
(
child:
TextButton
(
onPressed:
()
{
Navigator
.
of
(
context
).
pop
(
'OK'
);
},
style:
TextButton
.
styleFrom
(
foregroundColor:
Color
(
0xFF7691FA
),
textStyle:
TextStyle
(
fontSize:
17
),
minimumSize:
Size
.
fromHeight
(
40
),
padding:
EdgeInsets
.
zero
,
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
zero
,
),
),
child:
Text
(
'打开下载页面'
),
),
),
],
),
],
),
],
);
},
);
emit
(
state
.
copyWith
(
suggestAppUpgrade:
false
));
if
(
result
!=
null
)
{
final
uri
=
Uri
.
parse
(
'https://www.banxe.cn/apps/pro.html?t=
${DateTime.now().millisecondsSinceEpoch}
'
);
if
(
await
canLaunchUrl
(
uri
))
{
await
launchUrl
(
uri
,
mode:
LaunchMode
.
externalApplication
);
}
}
}
///
...
...
@@ -1242,6 +1422,74 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
///
bool
_hasBeenResumed
=
false
;
/// 版本检测并发控制标志
bool
_isCheckingVersion
=
false
;
/// H5资源已下载标志(本次生命周期内)
// bool _hasDownloadedH5 = false;
/// APP版本检测上次执行时间(2小时内只执行一次)
DateTime
?
_lastAppVersionCheckTime
;
///
/// 从后台恢复时进行版本检测
/// 参照 _init() 方法中的版本检测逻辑
/// 检测或下载中时不重复处理
///
Future
<
void
>
_checkVersionUpgrade
()
async
{
if
(
_isCheckingVersion
/*|| _hasDownloadedH5*/
)
{
return
;
}
_isCheckingVersion
=
true
;
try
{
var
curVersion
=
getIt
.
get
<
SharedPreferences
>().
getString
(
Constant
.
h5VersionKey
)
??
Constant
.
h5Version
;
var
versionConfig
=
await
_getVersionConfig
();
var
configVersion
=
versionConfig
[
'version'
]
as
String
;
var
downloadUrl
=
versionConfig
[
'zip'
]
as
String
;
var
appversionCheck
=
versionConfig
[
'appversionCheck'
]
as
String
;
var
appversionAndroid
=
versionConfig
[
'appversionAndroid'
]
as
String
;
var
appversionIos
=
versionConfig
[
'appversionIos'
]
as
String
;
if
(
appversionCheck
==
'1'
)
{
final
now
=
DateTime
.
now
();
if
(
_lastAppVersionCheckTime
==
null
||
now
.
difference
(
_lastAppVersionCheckTime
!).
inHours
>=
2
)
{
if
((
Platform
.
isAndroid
&&
appversionAndroid
!=
''
&&
appversionAndroid
!=
Constant
.
appVersion
)
||
(
Platform
.
isIOS
&&
appversionIos
!=
''
&&
appversionIos
!=
Constant
.
appVersion
))
{
emit
(
state
.
copyWith
(
suggestAppUpgrade:
true
));
}
_lastAppVersionCheckTime
=
now
;
// 重置标志
_isCheckingVersion
=
false
;
// 提示了APP升级后,不再进行H5版本检测
return
;
}
}
if
(
Constant
.
needUpgrade
&&
curVersion
!=
configVersion
)
{
// 闭包中提前捕获变量,确保回调执行时读取的是当前值
final
capturedConfigVersion
=
configVersion
;
_downloadH5Zip
(
capturedConfigVersion
,
downloadUrl
).
then
((
_
)
{
_setH5Version
(
capturedConfigVersion
);
// _hasDownloadedH5 = true;
emit
(
state
.
copyWith
(
suggestUpgrade:
true
));
}).
catchError
((
e
)
{
debugPrint
(
'后台H5资源下载失败
$e
'
);
}).
whenComplete
(()
{
_isCheckingVersion
=
false
;
});
}
else
{
// 不需要下载时,立即重置标志
_isCheckingVersion
=
false
;
}
}
catch
(
e
)
{
debugPrint
(
'后台H5版本检测失败
$e
'
);
}
}
@override
Future
<
void
>
didChangeAppLifecycleState
(
AppLifecycleState
lifecycleState
)
async
{
// 只针对iOS进行处理
...
...
@@ -1264,27 +1512,30 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
// 从后台恢复时,调用 appOnShow 指令
var
resp
=
{
'unique'
:
''
,
'cmd'
:
'appOnShow'
,
'data'
:
''
,
'errMsg'
:
''
};
_sendResponse
(
resp
);
// 代表从后台恢复,进行版本检测
_checkVersionUpgrade
();
}
// switch (state) {
// case AppLifecycleState.resumed:
// // 应用从后台回到前台时触发
//
print('
resumed--------------');
//
debugPrint('======> lifecycleState
resumed--------------');
// break;
// case AppLifecycleState.paused:
// // 应用进入后台时触发
//
print('
paused--------------');
//
debugPrint('======> lifecycleState
paused--------------');
// break;
// case AppLifecycleState.inactive:
// // 应用处于非活跃状态时触发
//
print('
inactive--------------');
//
debugPrint('======> lifecycleState
inactive--------------');
// break;
// case AppLifecycleState.detached:
// // 应用即将退出时触发
//
print('
detached--------------');
//
debugPrint('======> lifecycleState
detached--------------');
// break;
// case AppLifecycleState.hidden:
//
print('
hidden--------------');
//
debugPrint('======> lifecycleState
hidden--------------');
// break;
// }
}
...
...
lib/config/constant.dart
View file @
02ea609
...
...
@@ -50,12 +50,18 @@ class Constant {
/// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///
/// app 版本号规则
static
const
String
appVersion
=
EnvConfig
.
version
;
///
/// 实际取值来源于 pubspec.yaml 的 version 字段,
/// 在 main.dart 中通过 package_info_plus 读取后赋值;
/// EnvConfig.version 仅作为读取失败时的兜底默认值。
static
String
appVersion
=
EnvConfig
.
version
;
/// H5的起始终最低版本号规则
static
String
h5Version
=
'0.2.6'
;
/// 当前的H5业务包的版本
/// 从 key=h5VersionKey 的缓存中获取,获取不到时值时,说明是第一次打开APP,则从基础业务包中读取的。
static
String
h5Version
=
'0.0.0'
;
/// H5的版本号存储的key
/// 方便升级判断处理,存储H5的版本号。下载安装包之后,会提前设置版本号,启动本地服务时,解压对应版本的zip包。
static
const
String
h5VersionKey
=
'h5_version'
;
/// 用于显示的H5版本号存储的key
...
...
@@ -87,6 +93,9 @@ class Constant {
static
const
String
wxAppId
=
'wx8c32ea248f0c7765'
;
static
const
String
universalLink
=
'https://dev.banxiaoer.net/path/to/wechat/'
;
/// 微信支付商户号
static
const
String
wxPartnerId
=
'1514530791'
;
/// IM 相关
/// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///
...
...
lib/config/locator.dart
View file @
02ea609
...
...
@@ -41,6 +41,7 @@ import 'package:appframe/data/repositories/message/wechat_qr_bind_handler.dart';
import
'package:appframe/data/repositories/message/video_info_handler.dart'
;
import
'package:appframe/data/repositories/message/wifi_info_handler.dart'
;
import
'package:appframe/data/repositories/message/window_info_handler.dart'
;
import
'package:appframe/data/repositories/message/xe_create_order_handler.dart'
;
import
'package:appframe/data/repositories/phone_auth_repository.dart'
;
import
'package:appframe/data/repositories/user_auth_repository.dart'
;
import
'package:appframe/data/repositories/subs_repository.dart'
;
...
...
@@ -222,6 +223,9 @@ Future<void> setupLocator() async {
/// 设置屏幕模式
getIt
.
registerLazySingleton
<
MessageHandler
>(()
=>
ScreenHandler
(),
instanceName:
'setScreen'
);
/// 创建订单指令
getIt
.
registerLazySingleton
<
MessageHandler
>(()
=>
XeCreateOrderHandler
(),
instanceName:
'xeCreateOrder'
);
/// service
///
/// local server
...
...
lib/data/repositories/message/app_info_handler.dart
View file @
02ea609
import
'package:appframe/config/constant.dart'
;
import
'package:appframe/services/dispatcher.dart'
;
class
AppInfoHandler
extends
MessageHandler
{
@override
Future
<
Map
<
String
,
dynamic
>>
handleMessage
(
params
)
async
{
return
{
"version"
:
"0.1"
,
"theme"
:
"light"
};
return
{
"version"
:
Constant
.
appVersion
,
"theme"
:
"light"
,
"appshop"
:
''
,
};
}
}
lib/data/repositories/message/xe_create_order_handler.dart
0 → 100644
View file @
02ea609
import
'dart:async'
;
import
'dart:convert'
;
import
'package:appframe/config/constant.dart'
;
import
'package:appframe/config/locator.dart'
;
import
'package:appframe/data/repositories/user_auth_repository.dart'
;
import
'package:appframe/services/dispatcher.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:fluwx/fluwx.dart'
;
class
XeCreateOrderHandler
extends
MessageHandler
{
late
final
Fluwx
_fluwx
;
late
final
UserAuthRepository
_userAuthRepository
;
late
FluwxCancelable
_fluwxCancelable
;
// 支付结果 Completer,用于将异步回调转为 await
Completer
<
WeChatPaymentResponse
>?
_payCompleter
;
// 超时定时器
Timer
?
_timeoutTimer
;
XeCreateOrderHandler
()
{
_fluwx
=
getIt
.
get
<
Fluwx
>();
_userAuthRepository
=
getIt
.
get
<
UserAuthRepository
>();
}
@override
Future
<
dynamic
>
handleMessage
(
params
)
async
{
// 从 params 中提取支付参数
final
String
chargeCode
=
params
[
'chargeCode'
]
as
String
?
??
''
;
final
int
durationType
=
params
[
'durationType'
]
as
int
?
??
0
;
final
int
duration
=
params
[
'duration'
]
as
int
?
??
0
;
final
int
totalFee
=
params
[
'totalFee'
]
as
int
?
??
0
;
final
bool
renew
=
params
[
'renew'
]
as
bool
?
??
false
;
final
String
phone
=
params
[
'phone'
]
as
String
?
??
''
;
final
String
userCode
=
params
[
'userCode'
]
as
String
?
??
''
;
final
String
tbxStuId
=
params
[
'tbxStuId'
]
as
String
?
??
''
;
final
String
stuId
=
params
[
'stuId'
]
as
String
?
??
''
;
// ext 为 JSON 格式字符串
final
String
ext
=
params
[
'ext'
]
as
String
?
??
'{}'
;
// 解析 ext
final
Map
<
String
,
dynamic
>
extMap
=
jsonDecode
(
ext
);
final
useCoin
=
extMap
[
'useCoin'
]
as
bool
?
??
false
;
debugPrint
(
'PayHandler params: chargeCode=
$chargeCode
, durationType=
$durationType
, '
'duration=
$duration
, totalFee=
$totalFee
, renew=
$renew
, '
'phone=
$phone
, userCode=
$userCode
, tbxStuId=
$tbxStuId
, stuId=
$stuId
, '
'ext=
$ext
'
);
// 1. 检查微信是否安装
if
(!
await
_fluwx
.
isWeChatInstalled
)
{
throw
Exception
(
'设备上未安装微信App,不支持微信支付'
);
}
// 2. 请求后端获取预支付订单信息
final
deviceType
=
'android'
;
var
result
=
await
_userAuthRepository
.
createOrder
(
userCode
,
tbxStuId
,
chargeCode
,
duration
,
durationType
,
totalFee
,
renew
,
phone
,
stuId
,
ext
,
useCoin
?
1
:
0
,
deviceType
);
if
(
result
==
null
)
{
throw
Exception
(
'获取支付信息失败'
);
}
var
data
=
result
[
'data'
]
as
Map
<
String
,
dynamic
>?;
if
(
data
==
null
)
{
throw
Exception
(
'支付参数获取失败'
);
}
// 3. 从后端响应中提取预支付ID
final
prepayId
=
data
[
'prepayId'
]
as
String
?;
if
(
prepayId
==
null
||
prepayId
.
isEmpty
)
{
throw
Exception
(
'预支付订单ID获取失败'
);
}
// 4. 从后端响应中提取支付参数(nonceStr、timestamp、paySign 由后端生成)
final
appId
=
Constant
.
wxAppId
;
final
partnerId
=
Constant
.
wxPartnerId
;
final
packageValue
=
'Sign=WXPay'
;
final
nonceStr
=
data
[
'nonceStr'
]
as
String
;
final
timestamp
=
data
[
'timeStamp'
]
as
int
;
final
paySign
=
data
[
'paySign'
]
as
String
;
final
wxTradeNo
=
data
[
'wxTradeNo'
]
as
String
;
debugPrint
(
'prepayId:
$prepayId
'
);
debugPrint
(
'nonceStr:
$nonceStr
'
);
debugPrint
(
'timestamp:
$timestamp
'
);
debugPrint
(
'paySign:
$paySign
'
);
debugPrint
(
'appId:
$appId
'
);
debugPrint
(
'partnerId:
$partnerId
'
);
debugPrint
(
'packageValue:
$packageValue
'
);
debugPrint
(
'wxTradeNo:
$wxTradeNo
'
);
if
(
nonceStr
.
isEmpty
||
paySign
.
isEmpty
||
wxTradeNo
.
isEmpty
)
{
throw
Exception
(
'支付参数获取失败'
);
}
// 5. 注册支付结果监听器
_payCompleter
=
Completer
<
WeChatPaymentResponse
>();
_fluwxCancelable
=
_fluwx
.
addSubscriber
(
_responseListener
);
// 6. 启动超时兜底(60秒 * 10)
_timeoutTimer
=
Timer
(
const
Duration
(
seconds:
600
),
()
{
if
(
_payCompleter
!=
null
&&
!
_payCompleter
!.
isCompleted
)
{
_payCompleter
!.
completeError
(
TimeoutException
(
'支付超时'
));
}
});
// 7. 调用 fluwx.pay() 拉起微信支付
final
payResult
=
await
_fluwx
.
pay
(
which:
Payment
(
// 微信开放平台 AppID
appId:
appId
,
// 商户号(固定)
partnerId:
partnerId
,
// 预支付ID(后端返回)
prepayId:
prepayId
,
// 固定值 Sign=WXPay
packageValue:
packageValue
,
// 随机字符串(后端返回)
nonceStr:
nonceStr
,
// 时间戳(后端返回,秒级)
timestamp:
timestamp
,
// 签名(后端生成)
sign:
paySign
,
),
);
if
(!
payResult
)
{
_cleanup
();
throw
Exception
(
'拉起微信支付失败'
);
}
// 8. 等待支付结果回调
try
{
final
response
=
await
_payCompleter
!.
future
;
if
(
response
.
isSuccessful
)
{
debugPrint
(
'wechat pay success:
${response.errStr}
'
);
return
{
"orderUnique"
:
wxTradeNo
,
"orderState"
:
1
};
}
else
if
(
response
.
errCode
==
-
2
)
{
debugPrint
(
'wechat pay cancel:
${response.errStr}
'
);
// 用户取消支付
// throw Exception('已取消支付');
return
{
"orderUnique"
:
wxTradeNo
,
"orderState"
:
0
};
}
else
{
debugPrint
(
'wechat pay failed:
${response.errStr}
'
);
// 支付失败
throw
Exception
(
'支付失败'
);
}
}
catch
(
e
)
{
debugPrint
(
'wechat pay error:
$e
'
);
throw
Exception
(
'支付异常:
$e
'
);
}
finally
{
_cleanup
();
}
}
/// Fluwx 响应监听器
void
_responseListener
(
WeChatResponse
response
)
{
if
(
response
is
WeChatPaymentResponse
)
{
_timeoutTimer
?.
cancel
();
if
(
_payCompleter
!=
null
&&
!
_payCompleter
!.
isCompleted
)
{
_payCompleter
!.
complete
(
response
);
}
}
}
/// 清理资源
void
_cleanup
()
{
_fluwxCancelable
.
cancel
();
_timeoutTimer
?.
cancel
();
_timeoutTimer
=
null
;
_payCompleter
=
null
;
}
}
lib/data/repositories/user_auth_repository.dart
View file @
02ea609
...
...
@@ -129,4 +129,37 @@ class UserAuthRepository {
}
}
///
/// chargeCodes : bxe_vip
/// duration: durationType=1-天数|=2-月数|=3-yyyyMMdd|=4-学期数
/// durationType : 1.天|2.月|3.特定过期日期|4.学期|5.永久
/// totalFee: 经过计算后的实收价格,单位分
/// useCoin: 1.使用,0.不使用
///
Future
<
dynamic
>
createOrder
(
String
userId
,
String
tbxStuId
,
String
chargeCodes
,
int
duration
,
int
durationType
,
int
totalFee
,
bool
renew
,
String
phone
,
String
stuId
,
String
ext
,
int
useCoin
,
String
deviceType
)
async
{
try
{
Response
resp
=
await
_appService
.
post
(
'/api/v1/comm/user/buyvip'
,
{
"userId"
:
userId
,
"tbxStuId"
:
tbxStuId
,
"chargeCodes"
:
chargeCodes
,
"duration"
:
duration
,
"durationType"
:
durationType
,
"totalFee"
:
totalFee
,
"renew"
:
renew
,
"phone"
:
phone
,
"stuId"
:
stuId
,
"ext"
:
ext
,
"useCoin"
:
useCoin
,
"deviceType"
:
deviceType
},
);
return
resp
.
statusCode
==
HttpStatus
.
ok
?
resp
.
data
:
null
;
}
on
DioException
{
return
null
;
}
}
}
lib/main.dart
View file @
02ea609
...
...
@@ -8,6 +8,7 @@ import 'package:appframe/ui/widgets/ios_edge_swipe_detector.dart';
import
'package:archive/archive.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:package_info_plus/package_info_plus.dart'
;
import
'package:shared_preferences/shared_preferences.dart'
;
import
'app.dart'
;
...
...
@@ -16,12 +17,28 @@ void main() async {
WidgetsFlutterBinding
.
ensureInitialized
();
await
setupLocator
();
await
_initAppVersion
();
await
_initH5Version
();
await
_initIOSGesture
();
runApp
(
const
App
());
}
/// 从 pubspec.yaml 中读取并初始化 APP 版本号
///
/// pubspec.yaml 中的 version 字段作为全局唯一数据源,
/// 避免与构建参数、硬编码出现不一致。
Future
<
void
>
_initAppVersion
()
async
{
try
{
final
info
=
await
PackageInfo
.
fromPlatform
();
if
(
info
.
version
.
isNotEmpty
)
{
Constant
.
appVersion
=
info
.
version
;
}
}
catch
(
e
)
{
debugPrint
(
'读取 APP 版本号失败,使用默认值:
$e
'
);
}
}
Future
<
void
>
_initH5Version
()
async
{
// 1 读取保存的H5版本
var
h5Version
=
getIt
.
get
<
SharedPreferences
>().
getString
(
Constant
.
h5VersionKey
);
...
...
@@ -45,13 +62,17 @@ Future<void> _initH5Version() async {
var
h5Version
=
json
[
'version'
]
??
Constant
.
h5Version
;
Constant
.
h5Version
=
h5Version
;
getIt
.
get
<
SharedPreferences
>().
setString
(
Constant
.
h5ShowVersionKey
,
h5Version
);
var
sharedPreferences
=
getIt
.
get
<
SharedPreferences
>();
sharedPreferences
.
setString
(
Constant
.
h5VersionKey
,
h5Version
);
sharedPreferences
.
setString
(
Constant
.
h5ShowVersionKey
,
h5Version
);
return
;
}
}
}
catch
(
e
)
{
Constant
.
h5Version
=
'0.0.0'
;
getIt
.
get
<
SharedPreferences
>().
setString
(
Constant
.
h5ShowVersionKey
,
'0.0.0'
);
var
sharedPreferences
=
getIt
.
get
<
SharedPreferences
>();
sharedPreferences
.
setString
(
Constant
.
h5VersionKey
,
'0.0.0'
);
sharedPreferences
.
setString
(
Constant
.
h5ShowVersionKey
,
'0.0.0'
);
debugPrint
(
e
.
toString
());
}
}
...
...
lib/ui/pages/login_phone_page.dart
View file @
02ea609
...
...
@@ -123,6 +123,8 @@ class LoginPhonePage extends StatelessWidget {
listener:
(
context
,
state
)
{
if
(
state
.
showAgreed
)
{
_showAgreementDialog
(
context
,
context
.
read
<
LoginPhoneCubit
>());
}
else
if
(
state
.
showGuide
)
{
_showGuideDialog
(
context
,
context
.
read
<
LoginPhoneCubit
>());
}
},
),
...
...
@@ -438,4 +440,73 @@ class LoginPhonePage extends StatelessWidget {
loginPhoneCubit
.
cancelAgreed
();
}
}
Future
<
void
>
_showGuideDialog
(
BuildContext
context
,
LoginPhoneCubit
loginPhoneCubit
)
async
{
final
result
=
await
showDialog
(
context:
context
,
barrierDismissible:
false
,
builder:
(
BuildContext
context
)
{
return
AlertDialog
(
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
all
(
Radius
.
circular
(
5
),
),
),
title:
Text
(
'手机号登录指引'
,
style:
TextStyle
(
fontSize:
17
,
color:
Color
(
0xFF000000
),
// fontWeight: FontWeight.bold,
),
textAlign:
TextAlign
.
center
,
),
content:
Text
.
rich
(
TextSpan
(
children:
[
TextSpan
(
// text: '感谢使用班小二APP,为保护您的个人权益,请仔细阅读并充分理解',
text:
'输入的手机号尚未绑定用户,需先通过微信方式登录,并成功绑定手机号之后,才可使用手机号进行登录。点击查看'
,
style:
TextStyle
(
color:
Color
(
0xFF666666
),
fontSize:
14
),
),
TextSpan
(
text:
'《手机号登录指引》'
,
style:
TextStyle
(
color:
Color
(
0xFF7691FA
),
fontSize:
14
),
recognizer:
TapGestureRecognizer
()
..
onTap
=
()
{
router
.
push
(
'/link'
,
extra:
{
'url'
:
'https://bxr.banxiaoer.net/apps/phoneGuide.html'
,
'title'
:
'手机号登录指引'
},
);
},
),
TextSpan
(
text:
',了解绑定步骤。'
,
style:
TextStyle
(
color:
Color
(
0xFF666666
),
fontSize:
14
),
),
],
),
),
actions:
[
TextButton
(
onPressed:
()
{
Navigator
.
of
(
context
).
pop
(
'OK'
);
},
style:
TextButton
.
styleFrom
(
foregroundColor:
Color
(
0xFF7691FA
),
textStyle:
TextStyle
(
fontSize:
17
),
minimumSize:
Size
.
fromHeight
(
40
),
padding:
EdgeInsets
.
zero
,
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
zero
,
),
),
child:
Text
(
'关闭'
),
),
],
);
},
);
loginPhoneCubit
.
cancelShowGuide
();
}
}
lib/ui/pages/setting/setting_page.dart
View file @
02ea609
...
...
@@ -6,6 +6,7 @@ import 'package:appframe/config/env_config.dart';
import
'package:appframe/config/routes.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:url_launcher/url_launcher.dart'
;
class
SettingPage
extends
StatelessWidget
{
const
SettingPage
({
super
.
key
});
...
...
@@ -95,7 +96,10 @@ class SettingPage extends StatelessWidget {
onTap:
()
{
router
.
push
(
'/link'
,
extra:
{
'url'
:
'https://bxr.banxiaoer.net/apps/versions.html'
,
'title'
:
'版本记录'
},
extra:
{
'url'
:
'https://bxr.banxiaoer.net/apps/versions.html?t=
${DateTime.now().millisecondsSinceEpoch}
'
,
'title'
:
'版本记录'
},
);
},
contentPadding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
4
),
...
...
@@ -111,7 +115,7 @@ class SettingPage extends StatelessWidget {
router
.
push
(
'/link'
,
extra:
{
'url'
:
'https://bxr.banxiaoer.net/apps/useragreement.html'
,
'url'
:
'https://bxr.banxiaoer.net/apps/useragreement.html
?t=
${DateTime.now().millisecondsSinceEpoch}
'
,
'title'
:
'用户协议'
},
);
...
...
@@ -129,7 +133,7 @@ class SettingPage extends StatelessWidget {
router
.
push
(
'/link'
,
extra:
{
'url'
:
'https://bxr.banxiaoer.net/apps/privacysettings.html'
,
'url'
:
'https://bxr.banxiaoer.net/apps/privacysettings.html
?t=
${DateTime.now().millisecondsSinceEpoch}
'
,
'title'
:
'隐私设置'
},
);
...
...
@@ -146,7 +150,10 @@ class SettingPage extends StatelessWidget {
onTap:
()
{
router
.
push
(
'/link'
,
extra:
{
'url'
:
'https://bxr.banxiaoer.net/apps/produce.html'
,
'title'
:
'关于'
},
extra:
{
'url'
:
'https://bxr.banxiaoer.net/apps/produce.html?t=
${DateTime.now().millisecondsSinceEpoch}
'
,
'title'
:
'关于'
},
);
},
contentPadding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
4
),
...
...
@@ -158,6 +165,31 @@ class SettingPage extends StatelessWidget {
),
),
const
SizedBox
(
height:
8
),
Card
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
2
),
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
6
),
),
child:
Column
(
children:
[
ListTile
(
leading:
const
Icon
(
Icons
.
download
,
size:
20
),
title:
const
Text
(
'下载与更新'
,
style:
TextStyle
(
fontSize:
14
)),
onTap:
()
async
{
final
uri
=
Uri
.
parse
(
'https://www.banxe.cn/apps/pro.html?t=
${DateTime.now().millisecondsSinceEpoch}
'
);
if
(
await
canLaunchUrl
(
uri
))
{
await
launchUrl
(
uri
,
mode:
LaunchMode
.
externalApplication
);
}
},
contentPadding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
4
),
trailing:
const
Icon
(
Icons
.
arrow_forward_ios
,
size:
14
),
dense:
true
,
visualDensity:
VisualDensity
.
compact
,
),
],
),
),
const
SizedBox
(
height:
8
),
...
customerService
,
Card
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
2
),
...
...
@@ -235,7 +267,7 @@ class SettingPage extends StatelessWidget {
const
SizedBox
(
height:
4
),
],
const
Text
(
'V1.0'
,
'V1.0
.10
'
,
style:
TextStyle
(
color:
Colors
.
grey
,
fontSize:
12
,
...
...
lib/ui/pages/web_page.dart
View file @
02ea609
...
...
@@ -154,6 +154,8 @@ class WebPage extends StatelessWidget {
context
.
read
<
WebCubit
>().
chooseVideo
(
context
);
}
else
if
(
state
.
wechatQrBindCmdFlag
)
{
context
.
read
<
WebCubit
>().
showWechatQrBindDialog
(
context
);
}
else
if
(
state
.
suggestAppUpgrade
)
{
context
.
read
<
WebCubit
>().
suggestAppUpdate
(
context
);
}
},
),
...
...
lib/utils/video_util.dart
View file @
02ea609
...
...
@@ -9,7 +9,8 @@ class VideoUtil {
///
/// 将视频格式转换为mp4
/// 转码的同时,根据文件大小自动选择压缩参数
/// iOS: h264_videotoolbox 硬件加速 + 按文件大小自动码率
/// iOS 18.6.x: libx264 + 按文件大小自动CRF(h264_videotoolbox在iOS 18.6.x存在稳定性问题)
/// 其它iOS版本: h264_videotoolbox 硬件加速 + 按文件大小自动码率
/// Android: libx264 + 按文件大小自动CRF
/// [onProgress] 进度回调,值范围 0.0 ~ 1.0
///
...
...
@@ -23,14 +24,24 @@ class VideoUtil {
final
fileSize
=
await
File
(
inputPath
).
length
();
String
cmd
;
if
(
Platform
.
isIOS
)
{
if
(
Platform
.
isIOS
/*&& !_isIos18_6()*/
)
{
final
bitrate
=
_getBitrateByFileSize
(
fileSize
);
// cmd = '-i "$inputPath" '
// '-c:v h264_videotoolbox ' // 启用 iOS 硬件加速
// '-b:v ${bitrate}k ' // 根据文件大小自动计算码率
// '-vf scale=1280:-2 ' // 缩放到 720p (保持比例)
// '-c:a aac ' // 音频转为 AAC (兼容性最好)
// '-b:a 128k ' // 音频码率
// '"$outputPath"';
cmd
=
'-i "
$inputPath
" '
'-c:v h264_videotoolbox '
// 启用 iOS 硬件加速
'-b:v
${bitrate}
k '
// 根据文件大小自动计算码率
'-vf scale=1280:-2 '
// 缩放到 720p (保持比例)
'-c:a aac '
// 音频转为 AAC (兼容性最好)
'-b:a 128k '
// 音频码率
'-c:v h264_videotoolbox '
'-b:v
${bitrate}
k '
'-profile:v high '
// 确保使用高压缩率 Profile
'-vf "scale=trunc(oh*a/2)*2:720,format=yuv420p" '
// 强制 720p 且确保像素格式为 yuv420p
'-c:a aac '
'-b:a 128k '
'-movflags +faststart '
// 关键:优化 MP4 结构,支持边下边播
'-y '
// 自动覆盖已存在文件
'"
$outputPath
"'
;
}
else
{
final
crf
=
_getCrfByFileSize
(
fileSize
);
...
...
@@ -71,7 +82,10 @@ class VideoUtil {
///
/// 通过 ffmpeg 压缩视频
/// [quality] 压缩质量,可选值: 'low' | 'middle' | 'high'
/// 未指定时根据文件大小自动计算CRF:文件越大压缩率越高,文件越小清晰度越高
/// 未指定时根据文件大小自动计算压缩参数:文件越大压缩率越高,文件越小清晰度越高
/// iOS 18.6.x: libx264 + CRF(h264_videotoolbox在iOS 18.6.x存在稳定性问题)
/// 其它iOS版本: h264_videotoolbox 硬件加速 + 码率控制
/// Android: libx264 + CRF
/// [onProgress] 进度回调,值范围 0.0 ~ 1.0
///
static
Future
<
bool
>
compressVideo
(
...
...
@@ -82,40 +96,82 @@ class VideoUtil {
})
async
{
final
duration
=
await
_getVideoDuration
(
inputPath
);
// 使用CRF模式进行压缩,值范围0-51,建议值18-28
// 高质量: CRF 18-20
// 中等质量: CRF 23-26
// 低质量: CRF 28-32
int
crf
;
if
(
quality
!=
null
&&
quality
.
isNotEmpty
)
{
switch
(
quality
)
{
case
'low'
:
crf
=
32
;
break
;
case
'middle'
:
crf
=
26
;
break
;
case
'high'
:
crf
=
20
;
break
;
default
:
throw
Exception
(
'参数错误'
);
String
cmd
;
if
(
Platform
.
isIOS
/*&& !_isIos18_6()*/
)
{
// iOS(非18.6+): 使用 h264_videotoolbox 硬件加速
// h264_videotoolbox 不支持 CRF,使用码率控制质量
int
bitrate
;
if
(
quality
!=
null
&&
quality
.
isNotEmpty
)
{
switch
(
quality
)
{
case
'low'
:
bitrate
=
1500
;
break
;
case
'middle'
:
bitrate
=
2500
;
break
;
case
'high'
:
bitrate
=
4000
;
break
;
default
:
throw
Exception
(
'参数错误'
);
}
}
else
{
final
fileSize
=
await
File
(
inputPath
).
length
();
bitrate
=
_getBitrateByFileSize
(
fileSize
);
}
// cmd = '-i "$inputPath" ' // 输入文件
// '-c:v h264_videotoolbox ' // iOS硬件加速视频编码器
// '-b:v ${bitrate}k ' // 根据质量/文件大小自动计算码率
// '-c:a aac ' // 音频编码器
// '-b:a 128k ' // 音频码率
// '-movflags faststart ' // 优化MP4文件结构
// '"$outputPath"'; // 输出文件
cmd
=
'-i "
$inputPath
" '
'-c:v h264_videotoolbox '
'-b:v
${bitrate}
k '
'-profile:v high '
// 使用高配置以获得更好画质
'-pix_fmt yuv420p '
// 确保像素格式兼容性
'-vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" '
// 强制偶数分辨率,防止报错
'-c:a aac '
'-b:a 128k '
'-movflags +faststart '
// 允许边下边播
'"
$outputPath
"'
;
}
else
{
// 根据文件大小自动计算CRF
final
fileSize
=
await
File
(
inputPath
).
length
();
crf
=
_getCrfByFileSize
(
fileSize
);
// iOS 18.6+ / Android: 使用 libx264 软件编码
// 使用CRF模式进行压缩,值范围0-51,建议值18-28
// 高质量: CRF 18-20
// 中等质量: CRF 23-26
// 低质量: CRF 28-32
int
crf
;
if
(
quality
!=
null
&&
quality
.
isNotEmpty
)
{
switch
(
quality
)
{
case
'low'
:
crf
=
32
;
break
;
case
'middle'
:
crf
=
26
;
break
;
case
'high'
:
crf
=
20
;
break
;
default
:
throw
Exception
(
'参数错误'
);
}
}
else
{
final
fileSize
=
await
File
(
inputPath
).
length
();
crf
=
_getCrfByFileSize
(
fileSize
);
}
cmd
=
'-i "
$inputPath
" '
// 输入文件
'-c:v libx264 '
// 视频编码器
'-crf
$crf
'
// 恒定速率因子(质量控制)
'-c:a aac '
// 音频编码器
'-b:a 128k '
// 音频比特率
// '-preset medium ' // 编码预设
'-preset fast '
// 编码预设,编码速度显著提升,体积损失很小
'-threads 0 '
// 让 libx264 自动使用所有可用 CPU 核心,默认行为可能只用单核
'-movflags faststart '
// 优化MP4文件结构
'"
$outputPath
"'
;
// 输出文件
}
String
cmd
=
'-i "
$inputPath
" '
// 输入文件
'-c:v libx264 '
// 视频编码器
'-crf
$crf
'
// 恒定速率因子(质量控制)
'-c:a aac '
// 音频编码器
'-b:a 128k '
// 音频比特率
// '-preset medium ' // 编码预设
'-preset fast '
// 编码预设,编码速度显著提升,体积损失很小
'-threads 0 '
// 让 libx264 自动使用所有可用 CPU 核心,默认行为可能只用单核
'-movflags faststart '
// 优化MP4文件结构
'"
$outputPath
"'
;
// 输出文件
final
completer
=
Completer
<
bool
>();
...
...
@@ -138,6 +194,23 @@ class VideoUtil {
return
completer
.
future
;
}
/// 判断当前iOS系统版本是否为18.6.x
/// iOS 18.6.x中h264_videotoolbox硬件编码器存在稳定性问题,需改用libx264
static
bool
_isIos18_6
()
{
if
(!
Platform
.
isIOS
)
return
false
;
try
{
final
versionStr
=
Platform
.
operatingSystemVersion
;
final
regex
=
RegExp
(
r'(\d+)\.(\d+)'
);
final
match
=
regex
.
firstMatch
(
versionStr
);
if
(
match
!=
null
)
{
final
major
=
int
.
parse
(
match
.
group
(
1
)!);
final
minor
=
int
.
parse
(
match
.
group
(
2
)!);
return
major
==
18
&&
minor
==
6
;
}
}
catch
(
_
)
{}
return
false
;
}
/// 根据文件大小自动计算视频码率(kbps)
/// 用于iOS h264_videotoolbox硬件加速模式(不支持CRF,需用码率控制质量)
/// 文件越大码率越低(压缩率越高,清晰度越低)
...
...
pubspec.yaml
View file @
02ea609
name
:
appframe
description
:
"
app
frame
project."
publish_to
:
'
none'
version
:
1.0.
9+5
version
:
1.0.
10+6
environment
:
sdk
:
"
>=3.5.0
<4.0.0"
...
...
@@ -18,6 +18,7 @@ dependencies:
archive
:
^4.0.7
connectivity_plus
:
^7.0.0
device_info_plus
:
^11.5.0
package_info_plus
:
^8.3.1
dio
:
^5.9.0
equatable
:
^2.0.7
get_it
:
^8.2.0
...
...
Write
Preview
Styling with
Markdown
is supported
Attach a file
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to post a comment