diff --git a/config/baseUrl.js b/config/baseUrl.js
index 6c26018..fe61aa6 100644
--- a/config/baseUrl.js
+++ b/config/baseUrl.js
@@ -9,8 +9,8 @@ if (process.env.NODE_ENV === 'development') {
} else if (process.env.NODE_ENV === 'production') {
// 生产环境11
baseUrl = "https://api.nuttyreading.com/"; //线上正式
- //baseUrl = "https://testapi.nuttyreading.com/";
- // baseUrl = "http://192.168.110.100:9200/pb/"; //张川川
+ // baseUrl = "https://testapi.nuttyreading.com/";
+ // baseUrl = "http://192.168.110.131:9200/pb/"; // 本地
}
const courtConfig = {
//微信公众号APPID1
diff --git a/manifest.json b/manifest.json
index d242a10..dd9f050 100644
--- a/manifest.json
+++ b/manifest.json
@@ -12,8 +12,8 @@
"src" : "图片路径"
}
],
- "versionName" : "2.0.53",
- "versionCode" : 2053,
+ "versionName" : "2.0.54",
+ "versionCode" : 2054,
"sassImplementationName" : "node-sass",
"app-plus" : {
"nvueCompiler" : "uni-app",
diff --git a/package-lock.json b/package-lock.json
index 7dfbc5a..9e4e7b5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,7 @@
"dependencies": {
"animate.css": "^4.1.1",
"e-peanut": "file:",
- "edu-core": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#v1.0.14",
+ "edu-core": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#v1.0.15",
"element-plus": "^2.9.6",
"epubjs": "^0.3.93",
"jquery": "^2.2.4",
@@ -435,8 +435,8 @@
"link": true
},
"node_modules/edu-core": {
- "version": "1.0.14",
- "resolved": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#eb9eccdc3d281fe84127a93e019650512859ff0c",
+ "version": "1.0.15",
+ "resolved": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#df2306c058445de44914622bf56459f76247f5b1",
"license": "ISC"
},
"node_modules/element-plus": {
@@ -4173,7 +4173,7 @@
"requires": {
"animate.css": "^4.1.1",
"e-peanut": "file:",
- "edu-core": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#v1.0.14",
+ "edu-core": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#v1.0.15",
"element-plus": "^2.9.6",
"epubjs": "^0.3.93",
"jquery": "^2.2.4",
@@ -4494,8 +4494,8 @@
}
},
"edu-core": {
- "version": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#eb9eccdc3d281fe84127a93e019650512859ff0c",
- "from": "edu-core@git+https://git.nuttyreading.com/chenghuan/edu-core.git#v1.0.14"
+ "version": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#df2306c058445de44914622bf56459f76247f5b1",
+ "from": "edu-core@git+https://git.nuttyreading.com/chenghuan/edu-core.git#v1.0.15"
},
"element-plus": {
"version": "2.11.5",
@@ -7256,8 +7256,8 @@
}
},
"edu-core": {
- "version": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#eb9eccdc3d281fe84127a93e019650512859ff0c",
- "from": "edu-core@git+https://git.nuttyreading.com/chenghuan/edu-core.git#v1.0.14"
+ "version": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#df2306c058445de44914622bf56459f76247f5b1",
+ "from": "edu-core@git+https://git.nuttyreading.com/chenghuan/edu-core.git#v1.0.15"
},
"element-plus": {
"version": "2.11.5",
diff --git a/package.json b/package.json
index bf278c9..dd4b5bf 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"dependencies": {
"animate.css": "^4.1.1",
"e-peanut": "file:",
- "edu-core": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#v1.0.14",
+ "edu-core": "git+https://git.nuttyreading.com/chenghuan/edu-core.git#v1.0.15",
"element-plus": "^2.9.6",
"epubjs": "^0.3.93",
"jquery": "^2.2.4",
diff --git a/pages/test/test.vue b/pages/test/test.vue
index becb8e3..2cddefe 100644
--- a/pages/test/test.vue
+++ b/pages/test/test.vue
@@ -4,6 +4,7 @@
开启防截屏
关闭防截屏
关闭防截屏
+ 截屏测试
@@ -53,6 +54,19 @@
// screenshot.offAntiScreenshot()
screenGuard.offAntiScreenshot()
+ },
+ testCaptureScreen() {
+ console.log('点击了截屏测试按钮')
+ // #ifdef APP-PLUS
+ this.$captureScreen({
+ success(res) {
+ console.log('截屏成功', res.base64 && res.base64.length)
+ },
+ fail(err) {
+ console.error('截屏失败', err)
+ }
+ })
+ // #endif
}
}
}
diff --git a/uni_modules/uni-usercapturescreen/changelog.md b/uni_modules/uni-usercapturescreen/changelog.md
index 6bb238a..ef8c2eb 100644
--- a/uni_modules/uni-usercapturescreen/changelog.md
+++ b/uni_modules/uni-usercapturescreen/changelog.md
@@ -1,3 +1,5 @@
+## 1.0.7(2026-06-16)
+- 新增 iOS setUserCaptureScreen 支持 antiRecordOnly 参数:enable 为 false 且 antiRecordOnly 为 true 时,仅禁止录屏/投屏,允许截屏。实现方式为进入时提前挂载安全层(UITextField.isSecureTextEntry,默认不保护、保留 field 实例),通过 UIScreen.isCaptured 监听录屏/投屏,仅在录屏期间翻转 isSecureTextEntry——实时显示正常、录制到的文件为黑屏,平时不影响截屏(录屏进行中截屏也为黑)。Android/鸿蒙忽略该参数,仍为截屏+录屏一起禁止。
## 1.0.6(2024-11-22)
- 修复 HarmonyOS Next 上调用 setUserCaptureScreen 报错的 Bug
## 1.0.5(2024-10-14)
diff --git a/uni_modules/uni-usercapturescreen/index.d.ts b/uni_modules/uni-usercapturescreen/index.d.ts
index dbb3f92..0eef976 100644
--- a/uni_modules/uni-usercapturescreen/index.d.ts
+++ b/uni_modules/uni-usercapturescreen/index.d.ts
@@ -63,6 +63,12 @@ declare namespace UniNamespace {
*/
enable : boolean;
/**
+ * 仅 iOS 生效。仅在 enable 为 false 时有意义:
+ * true: 仅禁止录屏/投屏(录屏时画面被遮挡),允许截屏;
+ * false/不传: 截屏与录屏一起禁止(默认行为)。
+ */
+ antiRecordOnly ?: boolean;
+ /**
* 接口调用成功的回调函数
*/
// success : SetUserCaptureScreenSuccessCallback | null,
diff --git a/uni_modules/uni-usercapturescreen/package.json b/uni_modules/uni-usercapturescreen/package.json
index d27fbd6..1384482 100644
--- a/uni_modules/uni-usercapturescreen/package.json
+++ b/uni_modules/uni-usercapturescreen/package.json
@@ -1,7 +1,7 @@
{
"id": "uni-usercapturescreen",
"displayName": "uni-usercapturescreen",
- "version": "1.0.6",
+ "version": "1.0.7",
"description": "用户主动截屏事件监听",
"keywords": [
"截屏"
diff --git a/uni_modules/uni-usercapturescreen/utssdk/app-ios/index.uts b/uni_modules/uni-usercapturescreen/utssdk/app-ios/index.uts
index 17eec4b..a024811 100644
--- a/uni_modules/uni-usercapturescreen/utssdk/app-ios/index.uts
+++ b/uni_modules/uni-usercapturescreen/utssdk/app-ios/index.uts
@@ -12,6 +12,16 @@ class CaptureScreenTool {
static listener : UserCaptureScreenCallback | null;
static secureView : UIView | null;
+ // ===== 仅防录屏(动态切换)相关状态 =====
+ // 保留 UITextField,使 isSecureTextEntry 可在录屏开始/结束时反复切换
+ static secureField : UITextField | null;
+ // field 的安全画布(已挂入视图层级,承载 App 根视图)
+ static secureCanvas : UIView | null;
+ // 被安全画布包裹的 App 根视图(用于卸载时还原)
+ static secureRootView : UIView | null;
+ // 是否已为"仅防录屏"挂载安全层
+ static recordWrapped : boolean = false;
+
// 监听截屏
static listenCaptureScreen(callback : UserCaptureScreenCallback | null) {
this.listener = callback
@@ -51,28 +61,38 @@ class CaptureScreenTool {
return null
}
+ // 应用防截屏遮罩(secureView:实时显示正常,但被截屏/录屏捕获到的内容为黑屏)
+ // 不触发回调,供内部复用;注意:需在主线程调用
+ static applySecureViewInternal() {
+ if (this.secureView != null) {
+ // 已应用,避免重复包裹
+ return
+ }
+ let secureView = this.createSecureView()
+ let window = UTSiOS.getKeyWindow()
+ let rootView = window.rootViewController == null ? null : window.rootViewController!.view
+ if (secureView != null && rootView != null) {
+ let rootSuperview = rootView!.superview
+ if (rootSuperview != null) {
+ this.secureView = secureView
+ rootSuperview!.addSubview(secureView!)
+ rootView!.removeFromSuperview()
+ secureView!.addSubview(rootView!)
+ // secureView 充满父视图并随父视图宽高自适应(横竖屏、全屏切换时自动跟随)
+ secureView!.frame = rootSuperview!.bounds
+ secureView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
+ // rootView 充满 secureView 并自适应,避免全屏时被固定在竖屏尺寸而缩到角落
+ rootView!.frame = secureView!.bounds
+ rootView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
+ }
+ }
+ }
+
// 开启防截屏
static onAntiScreenshot(option : SetUserCaptureScreenOptions) {
// uts方法默认会在子线程中执行,涉及 UI 操作必须在主线程中运行,通过 DispatchQueue.main.async 方法可将代码在主线程中运行
DispatchQueue.main.async(execute = () : void => {
- let secureView = this.createSecureView()
- let window = UTSiOS.getKeyWindow()
- let rootView = window.rootViewController == null ? null : window.rootViewController!.view
- if (secureView != null && rootView != null) {
- let rootSuperview = rootView!.superview
- if (rootSuperview != null) {
- this.secureView = secureView
- rootSuperview!.addSubview(secureView!)
- rootView!.removeFromSuperview()
- secureView!.addSubview(rootView!)
- // secureView 充满父视图并随父视图宽高自适应(横竖屏、全屏切换时自动跟随)
- secureView!.frame = rootSuperview!.bounds
- secureView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
- // rootView 充满 secureView 并自适应,避免全屏时被固定在竖屏尺寸而缩到角落
- rootView!.frame = secureView!.bounds
- rootView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
- }
- }
+ this.applySecureViewInternal()
let res: SetUserCaptureScreenSuccess = {
}
option.success?.(res)
@@ -80,21 +100,107 @@ class CaptureScreenTool {
})
}
+ // ===== 仅防录屏:提前挂载安全层 + 翻转 isSecureTextEntry =====
+
+ // 提前挂载安全层(应在录屏开始前调用,例如进入页面时)。
+ // 挂载后默认不保护(isSecureTextEntry = false)→ 截屏正常、实时正常。
+ // 注意:需在主线程调用。
+ static setupRecordSecureWrapper() {
+ if (this.recordWrapped) {
+ return
+ }
+ let field = new UITextField(frame = CGRect.zero)
+ field.isSecureTextEntry = true
+ // 15.1 系统 secureView 不可用;拿不到安全画布则放弃
+ if (field.subviews.length == 0 || UIDevice.current.systemVersion == '15.1') {
+ return
+ }
+ let canvas = field.subviews[0]
+ canvas.subviews.forEach((item) => {
+ item.removeFromSuperview()
+ })
+ canvas.isUserInteractionEnabled = true
+
+ let window = UTSiOS.getKeyWindow()
+ let rootView = window.rootViewController == null ? null : window.rootViewController!.view
+ if (rootView == null) {
+ return
+ }
+ let rootSuperview = rootView!.superview
+ if (rootSuperview == null) {
+ return
+ }
+
+ // 把安全画布插入原层级,并把 App 根视图移入画布
+ rootSuperview!.addSubview(canvas)
+ rootView!.removeFromSuperview()
+ canvas.addSubview(rootView!)
+ canvas.frame = rootSuperview!.bounds
+ canvas.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
+ rootView!.frame = canvas.bounds
+ rootView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
+
+ // 关键:保留 field,后续才能切换 isSecureTextEntry
+ this.secureField = field
+ this.secureCanvas = canvas
+ this.secureRootView = rootView
+ this.recordWrapped = true
+
+ // 默认不保护:允许截屏、实时正常
+ field.isSecureTextEntry = false
+ }
+
+ // 切换录屏保护:true=保护(录屏文件黑、实时正常、此刻截屏也黑);false=不保护(允许截屏)
+ // 注意:需在主线程调用。
+ static setRecordSecure(secure : boolean) {
+ if (this.secureField != null) {
+ this.secureField!.isSecureTextEntry = secure
+ }
+ }
+
+ // 卸载安全层,恢复原始视图层级。注意:需在主线程调用。
+ static teardownRecordSecureWrapper() {
+ if (!this.recordWrapped) {
+ return
+ }
+ if (this.secureField != null) {
+ this.secureField!.isSecureTextEntry = false
+ }
+ if (this.secureCanvas != null && this.secureRootView != null) {
+ let rootSuperview = this.secureCanvas!.superview
+ if (rootSuperview != null) {
+ rootSuperview!.addSubview(this.secureRootView!)
+ this.secureRootView!.frame = rootSuperview!.bounds
+ this.secureCanvas!.removeFromSuperview()
+ }
+ }
+ this.secureField = null
+ this.secureCanvas = null
+ this.secureRootView = null
+ this.recordWrapped = false
+ }
+
+ // 移除防截屏遮罩视图(不触发回调,供内部复用)
+ // 注意:需在主线程调用
+ static removeSecureViewInternal() {
+ if (this.secureView != null) {
+ let window = UTSiOS.getKeyWindow()
+ let rootView = window.rootViewController == null ? null : window.rootViewController!.view
+ if (rootView != null && this.secureView!.superview != null) {
+ let rootSuperview = this.secureView!.superview
+ if (rootSuperview != null) {
+ rootSuperview!.addSubview(rootView!)
+ this.secureView!.removeFromSuperview()
+ }
+ }
+ this.secureView = null
+ }
+ }
+
// 关闭防截屏
static offAntiScreenshot(option : SetUserCaptureScreenOptions) {
DispatchQueue.main.async(execute = () : void => {
- if (this.secureView != null) {
- let window = UTSiOS.getKeyWindow()
- let rootView = window.rootViewController == null ? null : window.rootViewController!.view
- if (rootView != null && this.secureView!.superview != null) {
- let rootSuperview = this.secureView!.superview
- if (rootSuperview != null) {
- rootSuperview!.addSubview(rootView!)
- this.secureView!.removeFromSuperview()
- }
- }
- this.secureView = null
- }
+ this.removeSecureViewInternal()
let res: SetUserCaptureScreenSuccess = {
}
option.success?.(res)
@@ -103,6 +209,58 @@ class CaptureScreenTool {
}
}
+/**
+ * 防录屏工具类:基于 UIScreen.isCaptured 监听录屏/投屏状态,动态切换安全层。
+ * 截屏不会触发 isCaptured,因此可做到"允许截屏、仅在录屏时保护"。
+ *
+ * 实现要点(避免实时画面变黑):
+ * 1) 进入时(录屏开始前)就提前挂好安全层,且默认不保护 → 截屏正常、实时正常;
+ * 2) 仅在录屏期间翻转 isSecureTextEntry = true → 录屏文件黑、实时正常、此刻截屏也黑;
+ * 3) 录屏结束翻回 false → 截屏恢复正常。
+ */
+class ScreenRecordTool {
+ // 是否已注册监听
+ static monitoring : boolean = false;
+
+ // 开启防录屏:提前挂载安全层并监听录屏状态变化
+ static onAntiScreenRecord() {
+ DispatchQueue.main.async(execute = () : void => {
+ // 关键:录屏开始前就把安全层挂好(默认不保护)
+ CaptureScreenTool.setupRecordSecureWrapper()
+ if (!this.monitoring) {
+ this.monitoring = true
+ const method = Selector("screenCaptureDidChange")
+ NotificationCenter.default.addObserver(this, selector = method, name = UIScreen.capturedDidChangeNotification, object = null)
+ }
+ // 进入时若已经在录屏,立即按当前状态保护
+ this.applyIfNeeded()
+ })
+ }
+
+ // 录屏状态变化回调
+ @objc static screenCaptureDidChange() {
+ DispatchQueue.main.async(execute = () : void => {
+ this.applyIfNeeded()
+ })
+ }
+
+ // 根据当前录屏状态翻转安全层开关
+ static applyIfNeeded() {
+ CaptureScreenTool.setRecordSecure(UIScreen.main.isCaptured)
+ }
+
+ // 关闭防录屏:移除监听并卸载安全层
+ static offAntiScreenRecord() {
+ DispatchQueue.main.async(execute = () : void => {
+ if (this.monitoring) {
+ NotificationCenter.default.removeObserver(this, name = UIScreen.capturedDidChangeNotification, object = null)
+ this.monitoring = false
+ }
+ CaptureScreenTool.teardownRecordSecureWrapper()
+ })
+ }
+}
+
/**
* 开启截图监听
*/
@@ -121,21 +279,46 @@ export const offUserCaptureScreen : OffUserCaptureScreen = function (callback :
* 开启/关闭防截屏
*/
export const setUserCaptureScreen : SetUserCaptureScreen = function (options : SetUserCaptureScreenOptions) {
- if (UIDevice.current.systemVersion < "13.0") {
- let res = new SetUserCaptureScreenFailImpl(12001)
- options.fail?.(res);
- options.complete?.(res);
+ const systemVersion = UIDevice.current.systemVersion
- } else if (UIDevice.current.systemVersion == "15.1") {
- let res = new SetUserCaptureScreenFailImpl(12010)
- options.fail?.(res);
- options.complete?.(res);
- } else {
- if (options.enable == true) {
- CaptureScreenTool.offAntiScreenshot(options)
- } else {
- CaptureScreenTool.onAntiScreenshot(options)
+ if (options.enable == true) {
+ // 允许截屏与录屏:无条件清理录屏遮罩,再移除防截屏遮罩。
+ // 清理动作不受版本限制,避免遗留监听/遮罩。
+ ScreenRecordTool.offAntiScreenRecord()
+ CaptureScreenTool.offAntiScreenshot(options)
+ return
+ }
+
+ if (options.antiRecordOnly == true) {
+ // 仅禁止录屏、允许截屏:进入页面时提前挂好安全层(默认不保护),
+ // 用 UIScreen.isCaptured(iOS 11+)监听录屏/投屏,仅在录屏期间翻转 isSecureTextEntry
+ // —— 实时显示正常、录到的画面为黑屏,且平时不影响截屏。
+ // 注意:secureView 在 iOS 15.1 上不可用,该版本录屏保护会失效(属系统已知问题)。
+ if (systemVersion < "11.0") {
+ let res = new SetUserCaptureScreenFailImpl(12001)
+ options.fail?.(res)
+ options.complete?.(res)
+ return
}
+ ScreenRecordTool.onAntiScreenRecord()
+ let res: SetUserCaptureScreenSuccess = {
+ }
+ options.success?.(res)
+ options.complete?.(res)
+ return
+ }
+
+ // 默认:截屏与录屏一起禁止(secureView 方案,受系统版本限制)
+ if (systemVersion < "13.0") {
+ let res = new SetUserCaptureScreenFailImpl(12001)
+ options.fail?.(res)
+ options.complete?.(res)
+ } else if (systemVersion == "15.1") {
+ let res = new SetUserCaptureScreenFailImpl(12010)
+ options.fail?.(res)
+ options.complete?.(res)
+ } else {
+ CaptureScreenTool.onAntiScreenshot(options)
}
}
diff --git a/uni_modules/uni-usercapturescreen/utssdk/interface.uts b/uni_modules/uni-usercapturescreen/utssdk/interface.uts
index b4abf42..0d37a18 100644
--- a/uni_modules/uni-usercapturescreen/utssdk/interface.uts
+++ b/uni_modules/uni-usercapturescreen/utssdk/interface.uts
@@ -49,6 +49,12 @@
*/
enable : boolean;
/**
+ * 仅 iOS 生效。仅在 enable 为 false 时有意义:
+ * true: 仅禁止录屏/投屏(录屏时画面被遮挡),允许截屏;
+ * false/不传: 截屏与录屏一起禁止(默认行为)。
+ */
+ antiRecordOnly ?: boolean;
+ /**
* 接口调用成功的回调函数
*/
// success : SetUserCaptureScreenSuccessCallback | null,
diff --git a/uni_modules/yb-screen-capture/index.d.ts b/uni_modules/yb-screen-capture/index.d.ts
new file mode 100644
index 0000000..27d645f
--- /dev/null
+++ b/uni_modules/yb-screen-capture/index.d.ts
@@ -0,0 +1,29 @@
+export type CaptureScreenSuccess = {
+ /**
+ * 截图结果,格式为 data URL:data:image/jpeg;base64,xxxx
+ */
+ base64 : string
+}
+
+export type CaptureScreenSuccessCallback = (res : CaptureScreenSuccess) => void
+
+export type CaptureScreenFail = {
+ errCode : number,
+ errMsg : string
+}
+
+export type CaptureScreenFailCallback = (res : CaptureScreenFail) => void
+
+export type CaptureScreenCompleteCallback = (res : any) => void
+
+export type CaptureScreenOptions = {
+ success ?: CaptureScreenSuccessCallback,
+ fail ?: CaptureScreenFailCallback,
+ complete ?: CaptureScreenCompleteCallback
+}
+
+/**
+ * 截取当前应用窗口画面(含视频等硬件加速渲染内容),通过 base64 返回。
+ * 仅 App-Android 生效,基于 Android PixelCopy 实现。
+ */
+export declare const captureScreen : (options : CaptureScreenOptions) => void
diff --git a/uni_modules/yb-screen-capture/package.json b/uni_modules/yb-screen-capture/package.json
new file mode 100644
index 0000000..64fab2c
--- /dev/null
+++ b/uni_modules/yb-screen-capture/package.json
@@ -0,0 +1,85 @@
+{
+ "id": "yb-screen-capture",
+ "displayName": "yb-screen-capture",
+ "version": "1.0.0",
+ "description": "截取当前应用窗口(含硬件加速渲染的视频画面),返回 base64",
+ "keywords": [
+ "截图",
+ "截屏",
+ "PixelCopy"
+ ],
+ "repository": "",
+ "engines": {
+ "HBuilderX": "^3.7.7"
+ },
+ "dcloudext": {
+ "type": "uts",
+ "sale": {
+ "regular": {
+ "price": "0.00"
+ },
+ "sourcecode": {
+ "price": "0.00"
+ }
+ },
+ "contact": {
+ "qq": ""
+ },
+ "declaration": {
+ "ads": "无",
+ "data": "插件不采集任何数据",
+ "permissions": "无"
+ },
+ "npmurl": ""
+ },
+ "uni_modules": {
+ "dependencies": [],
+ "encrypt": [],
+ "platforms": {
+ "cloud": {
+ "tcb": "y",
+ "aliyun": "y",
+ "alipay": "n"
+ },
+ "client": {
+ "Vue": {
+ "vue2": "y",
+ "vue3": "y"
+ },
+ "App": {
+ "app-android": "y",
+ "app-ios": "n",
+ "app-harmony": "n"
+ },
+ "H5-mobile": {
+ "Safari": "n",
+ "Android Browser": "n",
+ "微信浏览器(Android)": "n",
+ "QQ浏览器(Android)": "n"
+ },
+ "H5-pc": {
+ "Chrome": "n",
+ "IE": "n",
+ "Edge": "n",
+ "Firefox": "n",
+ "Safari": "n"
+ },
+ "小程序": {
+ "微信": "n",
+ "阿里": "n",
+ "百度": "n",
+ "字节跳动": "n",
+ "QQ": "n",
+ "钉钉": "n",
+ "快手": "n",
+ "飞书": "n",
+ "京东": "n"
+ },
+ "快应用": {
+ "华为": "n",
+ "联盟": "n"
+ }
+ }
+ }
+ }
+}
diff --git a/uni_modules/yb-screen-capture/readme.md b/uni_modules/yb-screen-capture/readme.md
new file mode 100644
index 0000000..63e18f4
--- /dev/null
+++ b/uni_modules/yb-screen-capture/readme.md
@@ -0,0 +1,22 @@
+# yb-screen-capture
+
+截取当前应用窗口画面(含视频等硬件加速渲染内容),通过 base64 返回。
+
+- 仅 **App-Android** 生效,基于 Android `PixelCopy` 实现(Android 8.0 及以上能正确截取视频画面;低于 8.0 降级为 View 截图,视频区域可能为黑屏)。
+- 截图范围为「当前屏幕可见区域」,不是长页面截图。
+
+## 使用
+
+```js
+uni.captureScreen({
+ success: (res) => {
+ // res.base64 形如:data:image/jpeg;base64,xxxx
+ console.log(res.base64)
+ },
+ fail: (err) => {
+ console.error(err.errCode, err.errMsg)
+ }
+})
+```
+
+> 注意:UTS 插件需要打自定义基座(标准基座无法运行)才能调试/运行。
diff --git a/uni_modules/yb-screen-capture/utssdk/app-android/index.uts b/uni_modules/yb-screen-capture/utssdk/app-android/index.uts
new file mode 100644
index 0000000..904acad
--- /dev/null
+++ b/uni_modules/yb-screen-capture/utssdk/app-android/index.uts
@@ -0,0 +1,127 @@
+import { UTSAndroid } from "io.dcloud.uts";
+import PixelCopy from "android.view.PixelCopy";
+import OnPixelCopyFinishedListener from "android.view.PixelCopy.OnPixelCopyFinishedListener";
+import Bitmap from "android.graphics.Bitmap";
+import Canvas from "android.graphics.Canvas";
+import Handler from "android.os.Handler";
+import Looper from "android.os.Looper";
+import Build from "android.os.Build";
+import ByteArrayOutputStream from "java.io.ByteArrayOutputStream";
+import Base64 from "android.util.Base64";
+import {
+ CaptureScreen,
+ CaptureScreenOptions,
+ CaptureScreenSuccess,
+ CaptureScreenFail
+} from "../interface.uts";
+
+/**
+ * 将 Bitmap 压缩为 JPEG 并编码为 data URL 形式的 base64
+ */
+function encodeBitmapToBase64(bitmap : Bitmap) : string {
+ const baos = new ByteArrayOutputStream();
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos);
+ const bytes = baos.toByteArray();
+ const base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
+ bitmap.recycle();
+ return "data:image/jpeg;base64," + base64;
+}
+
+/**
+ * PixelCopy 完成监听器
+ */
+class PixelCopyListener extends OnPixelCopyFinishedListener {
+
+ private bitmap : Bitmap;
+ private options : CaptureScreenOptions;
+
+ constructor(bitmap : Bitmap, options : CaptureScreenOptions) {
+ super();
+ this.bitmap = bitmap;
+ this.options = options;
+ }
+
+ override onPixelCopyFinished(copyResult : Int) : void {
+ if (copyResult == PixelCopy.SUCCESS) {
+ const base64 = encodeBitmapToBase64(this.bitmap);
+ const res : CaptureScreenSuccess = { base64: base64 };
+ this.options.success?.(res);
+ this.options.complete?.(res);
+ } else {
+ this.bitmap.recycle();
+ const res : CaptureScreenFail = {
+ errCode: 12010,
+ errMsg: "captureScreen:fail PixelCopy copyResult=" + copyResult
+ };
+ this.options.fail?.(res);
+ this.options.complete?.(res);
+ }
+ }
+}
+
+/**
+ * 在 UI 线程执行实际截图
+ */
+class CaptureRunnable extends Runnable {
+
+ private options : CaptureScreenOptions;
+
+ constructor(options : CaptureScreenOptions) {
+ super();
+ this.options = options;
+ }
+
+ override run() : void {
+ try {
+ const activity = UTSAndroid.getUniActivity();
+ if (activity == null) {
+ const res : CaptureScreenFail = { errCode: 12010, errMsg: "captureScreen:fail activity is null" };
+ this.options.fail?.(res);
+ this.options.complete?.(res);
+ return;
+ }
+ // 注意:UTS 中 getWindow() 返回可空类型,必须用 ! 断言,否则编译失败
+ const window = activity!.getWindow()!;
+ const decorView = window.getDecorView()!;
+ const width = decorView.getWidth();
+ const height = decorView.getHeight();
+ if (width <= 0 || height <= 0) {
+ const res : CaptureScreenFail = { errCode: 12010, errMsg: "captureScreen:fail invalid window size" };
+ this.options.fail?.(res);
+ this.options.complete?.(res);
+ return;
+ }
+
+ const bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ // Android 8.0+:使用 PixelCopy 可以正确截取 SurfaceView/视频等硬件加速画面
+ const handler = new Handler(Looper.getMainLooper());
+ PixelCopy.request(window, bitmap, new PixelCopyListener(bitmap, this.options), handler);
+ } else {
+ // 低版本降级:仅能截取 View 层级内容,视频区域可能为黑屏
+ const canvas = new Canvas(bitmap);
+ decorView.draw(canvas);
+ const base64 = encodeBitmapToBase64(bitmap);
+ const res : CaptureScreenSuccess = { base64: base64 };
+ this.options.success?.(res);
+ this.options.complete?.(res);
+ }
+ } catch (e) {
+ const res : CaptureScreenFail = { errCode: 12010, errMsg: "captureScreen:fail " + e.toString() };
+ this.options.fail?.(res);
+ this.options.complete?.(res);
+ }
+ }
+}
+
+export const captureScreen : CaptureScreen = function (options : CaptureScreenOptions) {
+ const activity = UTSAndroid.getUniActivity();
+ if (activity == null) {
+ const res : CaptureScreenFail = { errCode: 12010, errMsg: "captureScreen:fail activity is null" };
+ options.fail?.(res);
+ options.complete?.(res);
+ return;
+ }
+ activity!.runOnUiThread(new CaptureRunnable(options));
+}
diff --git a/uni_modules/yb-screen-capture/utssdk/interface.uts b/uni_modules/yb-screen-capture/utssdk/interface.uts
new file mode 100644
index 0000000..e829e8f
--- /dev/null
+++ b/uni_modules/yb-screen-capture/utssdk/interface.uts
@@ -0,0 +1,73 @@
+/**
+ * uni.captureScreen 成功回调参数
+ */
+export type CaptureScreenSuccess = {
+ /**
+ * 截图结果,格式为 data URL:data:image/jpeg;base64,xxxx
+ */
+ base64 : string
+}
+
+/**
+ * uni.captureScreen 成功回调函数定义
+ */
+export type CaptureScreenSuccessCallback = (res : CaptureScreenSuccess) => void
+
+/**
+ * uni.captureScreen 失败回调参数
+ */
+export type CaptureScreenFail = {
+ errCode : number,
+ errMsg : string
+}
+
+/**
+ * uni.captureScreen 失败回调函数定义
+ */
+export type CaptureScreenFailCallback = (res : CaptureScreenFail) => void
+
+/**
+ * uni.captureScreen 完成回调函数定义(成功、失败都会执行)
+ */
+export type CaptureScreenCompleteCallback = (res : any) => void
+
+/**
+ * uni.captureScreen 参数
+ */
+export type CaptureScreenOptions = {
+ /**
+ * 接口调用成功的回调函数
+ */
+ success ?: CaptureScreenSuccessCallback,
+ /**
+ * 接口调用失败的回调函数
+ */
+ fail ?: CaptureScreenFailCallback,
+ /**
+ * 接口调用结束的回调函数(调用成功、失败都会执行)
+ */
+ complete ?: CaptureScreenCompleteCallback
+}
+
+export type CaptureScreen = (options : CaptureScreenOptions) => void
+
+export interface Uni {
+ /**
+ * 截取当前应用窗口画面(含视频等硬件加速渲染内容),通过 base64 返回。
+ * 仅 App-Android 生效,基于 Android PixelCopy 实现。
+ *
+ * @param {CaptureScreenOptions} options
+ * @uniPlatform {
+ * "app": {
+ * "android": {
+ * "osVer": "8.0",
+ * "uniVer": "3.7.7",
+ * "unixVer": "3.9.0"
+ * }
+ * }
+ * }
+ * @uniVersion 3.7.7
+ * @uniVueVersion 2,3
+ */
+ captureScreen(options : CaptureScreenOptions) : void
+}
diff --git a/unpackage/debug/android_debug.apk b/unpackage/debug/android_debug.apk
index 0e85d4b..9b7965f 100644
Binary files a/unpackage/debug/android_debug.apk and b/unpackage/debug/android_debug.apk differ
diff --git a/unpackage/debug/iOS_debug.ipa b/unpackage/debug/iOS_debug.ipa
index fc532f1..72622a8 100644
Binary files a/unpackage/debug/iOS_debug.ipa and b/unpackage/debug/iOS_debug.ipa differ