From c29273c855e3d62f84b447ee8632de7d89e0be3e Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Mon, 30 Mar 2026 15:55:57 +0800 Subject: [PATCH 01/51] add class and method --- AndroidPDACommand.csproj | 2 + OfflinePlusAddPattern_Async.cs | 109 ++++++++++++++++++++++++++++++++ OfflinePlusGetPatterns_Async.cs | 18 ++++++ PluginConfig.json | 2 +- Resources/AndroidPDACommand.js | 86 ++++++++++++++++++++++++- 5 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 OfflinePlusAddPattern_Async.cs create mode 100644 OfflinePlusGetPatterns_Async.cs diff --git a/AndroidPDACommand.csproj b/AndroidPDACommand.csproj index 9d634e0..c53bcc5 100644 --- a/AndroidPDACommand.csproj +++ b/AndroidPDACommand.csproj @@ -87,6 +87,8 @@ + + diff --git a/OfflinePlusAddPattern_Async.cs b/OfflinePlusAddPattern_Async.cs new file mode 100644 index 0000000..dfbe591 --- /dev/null +++ b/OfflinePlusAddPattern_Async.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using System.ComponentModel; +using GrapeCity.Forguncy.Commands; +using GrapeCity.Forguncy.Plugin; + +namespace AndroidPDACommand +{ + public class OfflinePlusAddPattern_Async : BaseAsyncCommand + { + [FormulaProperty] + [DisplayName("项目编号")] + [Description("项目的唯一标识,全局唯一")] + public object PatternId { get; set; } + + [DisplayName("名称")] + [FormulaProperty] + public object Title { get; set; } + + [DisplayName("描述")] + [FormulaProperty] + public object Description { get; set; } + + [DisplayName("表单项")] + [ObjectListProperty(ItemType = typeof(OfflineFormItem))] + public List FormItems { get; set; } + + public override string ToString() + { + return "添加项目"; + } + } + + public class OfflineFormItem : ObjectPropertyBase, INamedObject + { + public string Name { get; set; } + + [FormulaProperty] + [DisplayName("编号")] + [Description("该表单项的唯一标识,全局唯一,无论是同一项目内,还是跨项目,都必须唯一,更建议使用项目编号作为前缀拼接;例如项目为Project01,则该表单项编号为Project01_Name")] + public object ItemId { get; set; } + + [DisplayName("类型")] + [ComboProperty(ValueList = "textItem|passwordItem|selectItem", DisplayList = "文本框|密码框|选择框")] + public string ItemType { get; set; } = "textItem"; + + [FormulaProperty] + [DisplayName("标题")] + public object Title { get; set; } + + [FormulaProperty] + [DisplayName("水印")] + public object Hint { get; set; } + + [DisplayName("必填")] + public bool Required { get; set; } + + [FormulaProperty] + [DisplayName("默认值")] + public object Value { get; set; } + + + [ObjectProperty(ObjType = typeof(CheckOptions))] + [DisplayName("校验规则")] + public CheckOptions CheckOptions { get; set; } + + [ListProperty] + [DisplayName("选则项")] + public List SelectOptionsList { get; set; } + + public override bool GetDesignerPropertyVisible(string propertyName) + { + switch (propertyName) + { + case nameof(SelectOptionsList): + return ItemType == "selectItem"; + case nameof(CheckOptions): + return ItemType == "textItem" || ItemType == "passwordItem"; + default: + return base.GetDesignerPropertyVisible(propertyName); + } + } + } + + public class CheckOptions : ObjectPropertyBase + { + [FormulaProperty] + [DisplayName("最小长度")] + public object MinLength { get; set; } + + [FormulaProperty] + [DisplayName("最大长度")] + public object MaxLength { get; set; } + + [FormulaProperty] + [DisplayName("正则表达式")] + public object RegexPattern { get; set; } + } + + public class SelectOptions : ObjectPropertyBase + { + [FormulaProperty] + [DisplayName("值")] + public object Value { get; set; } + + [FormulaProperty] + [DisplayName("显示文本")] + public object Label { get; set; } + } +} \ No newline at end of file diff --git a/OfflinePlusGetPatterns_Async.cs b/OfflinePlusGetPatterns_Async.cs new file mode 100644 index 0000000..b3257de --- /dev/null +++ b/OfflinePlusGetPatterns_Async.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using GrapeCity.Forguncy.Commands; + +namespace AndroidPDACommand +{ + public class OfflinePlusGetPatterns_Async : BaseAsyncCommand + { + + [Browsable(false)] + [ResultToProperty] + public string ProjectsResult { get; set; } = "projects"; + + public override string ToString() + { + return "获取已保存项目"; + } + } +} \ No newline at end of file diff --git a/PluginConfig.json b/PluginConfig.json index df57b62..2ac37f6 100644 --- a/PluginConfig.json +++ b/PluginConfig.json @@ -12,7 +12,7 @@ "name": "PDA(Android)交互命令", "pluginType": "command", "guid": "{16B8DF69-1379-41C1-9194-BE89BAF8BB32}", - "version": "1.21.0.0", + "version": "1.22.0.0", "dependenceVersion": "8.0.0.0", "bundleJavaScript": true, "bundleCSS": true diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index dafc0e1..0555f00 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2561,4 +2561,88 @@ var Biometric_Available_Async_Command = (function (_super) { return Biometric_Available_Async_Command; }(Forguncy.CommandBase)); -Forguncy.CommandFactory.registerCommand("AndroidPDACommand.Biometric_Available_Async, AndroidPDACommand", Biometric_Available_Async_Command); \ No newline at end of file +Forguncy.CommandFactory.registerCommand("AndroidPDACommand.Biometric_Available_Async, AndroidPDACommand", Biometric_Available_Async_Command); + +var OfflinePlusAddPattern_Async_Command = (function (_super) { + __extends(OfflinePlusAddPattern_Async_Command, _super); + function OfflinePlusAddPattern_Async_Command() { + return _super !== null && _super.apply(this, arguments) || this; + } + + OfflinePlusAddPattern_Async_Command.prototype.execute = function () { + + let me = this; + let params = this.CommandParam; + + let _ticket = HAC_GenerateCallbackTicket(me, function (payload, payload2) { + me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { + runTimePageName: me.CommandExecutingInfo.runTimePageName, + commandID: new Date().valueOf().toString(), + initParams: { + "IsSuccess": true, + "Error": "" + }, + locationString: "执行window.offlinePlus.offlinePlusAddPattern" + }); + }); + + let patternParam = { + ticket: _ticket, + patternId: this.evaluateFormula(params.PatternId), + title: this.evaluateFormula(params.Title), + description: this.evaluateFormula(params.Description), + formItems: params.FormItems + }; + + console.log(patternParam); + if (window.offlinePlus && window.offlinePlus.offlinePlusAddPattern) { + + window.offlinePlus.offlinePlusAddPatternAsync(JSON.stringify(patternParam)); + } else { + HAC_CallFunctionOutOfHAC(); + } + }; + + + return OfflinePlusAddPattern_Async_Command; +}(Forguncy.CommandBase)); + +Forguncy.CommandFactory.registerCommand("AndroidPDACommand.OfflinePlusAddPattern_Async, AndroidPDACommand", OfflinePlusAddPattern_Async_Command); + +var OfflinePlusGetPatterns_Async_Command = (function (_super) { + __extends(OfflinePlusGetPatterns_Async_Command, _super); + function OfflinePlusGetPatterns_Async_Command() { + return _super !== null && _super.apply(this, arguments) || this; + } + + OfflinePlusGetPatterns_Async_Command.prototype.execute = function () { + + let me = this; + + let ticket = HAC_GenerateCallbackTicket(me, function (payload, payload2) { + me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { + runTimePageName: me.CommandExecutingInfo.runTimePageName, + commandID: new Date().valueOf().toString(), + initParams: { + "ProjectsResult": payload, + "IsSuccess": true, + "Error": "" + }, + locationString: "执行window.offlinePlus.offlinePlusGetPatternsAsync" + }); + }); + + if (window.offlinePlus && window.offlinePlus.offlinePlusGetPatternsAsync) { + window.offlinePlus.offlinePlusGetPatternsAsync(ticket); + } else { + HAC_CallFunctionOutOfHAC(); + } + + }; + + + return OfflinePlusGetPatterns_Async_Command; +}(Forguncy.CommandBase)); + +Forguncy.CommandFactory.registerCommand("AndroidPDACommand.OfflinePlusGetPatterns_Async, AndroidPDACommand", OfflinePlusGetPatterns_Async_Command); + -- Gitee From 8dc864d908fd015609277f380c7d7198088e37b5 Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Thu, 4 Jun 2026 12:04:02 +0800 Subject: [PATCH 02/51] add async --- Resources/AndroidPDACommand.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 0555f00..795ce02 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2595,7 +2595,7 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { }; console.log(patternParam); - if (window.offlinePlus && window.offlinePlus.offlinePlusAddPattern) { + if (window.offlinePlus && window.offlinePlus.offlinePlusAddPatternAsync) { window.offlinePlus.offlinePlusAddPatternAsync(JSON.stringify(patternParam)); } else { -- Gitee From 8a1a8b517e52a276b5f5d42a24ba16abc0156948 Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Thu, 4 Jun 2026 13:51:11 +0800 Subject: [PATCH 03/51] chore: migrate HAC plugin to net8 and add offline diagnostics Migrate the plugin project to SDK-style net8.0-windows so it can reference Forguncy v12 assemblies. Add offline form command diagnostics and a success alert showing the JSON payload used by OfflinePlusAddPattern_Async. --- AndroidPDACommand.csproj | 131 +++++++++++---------------------- JPushServerCommand.cs | 3 +- OfflinePlusAddPattern_Async.cs | 3 +- Resources/AndroidPDACommand.js | 40 +++++++--- 4 files changed, 76 insertions(+), 101 deletions(-) diff --git a/AndroidPDACommand.csproj b/AndroidPDACommand.csproj index c53bcc5..376c9ec 100644 --- a/AndroidPDACommand.csproj +++ b/AndroidPDACommand.csproj @@ -1,76 +1,38 @@ - - - + - Debug - AnyCPU - {8414FB54-B60D-4F9A-9505-B5E0B2D86C43} + net8.0-windows Library - Properties AndroidPDACommand AndroidPDACommand - v4.7.2 - 512 SAK SAK SAK SAK - true + true + true + true + false + false + false + false + bin\ true full false - bin\ DEBUG;TRACE prompt 4 - MinimumRecommendedRules.ruleset pdbonly true - bin\ TRACE prompt 4 - - - false - - - D:\ForguncyOtherVersion\8.0.6.0\Website\designerBin\Forguncy.Commands.dll - - - D:\ForguncyOtherVersion\8.0.6.0\Website\designerBin\Forguncy.Commands.Design.dll - - - D:\ForguncyOtherVersion\8.0.6.0\Website\designerBin\GrapeCity.Forguncy.Plugin.dll - - - False - D:\ForguncyOtherVersion\8.0.6.0\Website\bin\GrapeCity.Forguncy.ServerApi.dll - - - packages\Jiguang.JPush.1.2.5\lib\net45\Jiguang.JPush.dll - - - packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll - - - - - - - - - - - - - @@ -143,77 +105,68 @@ - + + + + + E:\forAgentA\forguncy\Forguncy.Server2\designerBin\Forguncy.Commands.dll + False + + + E:\forAgentA\forguncy\Forguncy.Server2\designerBin\Forguncy.Commands.Design.dll + False + + + E:\forAgentA\forguncy\Forguncy.Server2\designerBin\GrapeCity.Forguncy.Plugin.dll + False + + + E:\forAgentA\forguncy\Forguncy.Server2\bin\GrapeCity.Forguncy.ServerApi.dll + False + + + + + - + PreserveNewest - - - + PreserveNewest - - + Always - - - + PreserveNewest + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "D:\ForguncyCode\puglin\PluginTools\PluginPackageTool\bin\Release\PluginPackageTool.exe" "$(ProjectDir)\" "$(ConfigurationName)" - - \ No newline at end of file + + + + diff --git a/JPushServerCommand.cs b/JPushServerCommand.cs index 317c421..c6ff1b6 100644 --- a/JPushServerCommand.cs +++ b/JPushServerCommand.cs @@ -12,7 +12,6 @@ using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using System.Windows.Markup; -using System.Xml.Serialization.Configuration; namespace AndroidPDACommand { @@ -66,7 +65,7 @@ namespace AndroidPDACommand if (!string.IsNullOrEmpty(url)) { - intentObj.Add("url", "intent:#Intent;action=com.huozige.lab.container.navigate;component=com.huozige.lab.container/com.huozige.lab.container.MainActivity;S.url=" + System.Web.HttpUtility.UrlEncode(url) + ";end"); + intentObj.Add("url", "intent:#Intent;action=com.huozige.lab.container.navigate;component=com.huozige.lab.container/com.huozige.lab.container.MainActivity;S.url=" + WebUtility.UrlEncode(url) + ";end"); } else { diff --git a/OfflinePlusAddPattern_Async.cs b/OfflinePlusAddPattern_Async.cs index dfbe591..1e6a3bc 100644 --- a/OfflinePlusAddPattern_Async.cs +++ b/OfflinePlusAddPattern_Async.cs @@ -5,6 +5,7 @@ using GrapeCity.Forguncy.Plugin; namespace AndroidPDACommand { + [Category("活字格安卓容器(HAC)")] public class OfflinePlusAddPattern_Async : BaseAsyncCommand { [FormulaProperty] @@ -26,7 +27,7 @@ namespace AndroidPDACommand public override string ToString() { - return "添加项目"; + return "添加离线表单项目"; } } diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 795ce02..0eb19e1 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -1,10 +1,29 @@  var ERROR_NOT_RUN_IN_HAC = "当前APP不支持该命令,这通常是因为APP版本过低。您可以通过在葡萄城技术社区搜索“HAC”下载最新版APP。"; -var HAC_CallFunctionOutOfHAC = function () { - alert(ERROR_NOT_RUN_IN_HAC); +var HAC_CallFunctionOutOfHAC = function (debugInfo) { + var message = ERROR_NOT_RUN_IN_HAC; + if (debugInfo) { + message += "\r\n\r\n诊断信息:\r\n" + debugInfo; + } + alert(message); } +var HAC_GetOfflinePlusDebugInfo = function (commandName, stage) { + var offlinePlus = window.offlinePlus; + var hac = window.HAC; + return [ + "命令:" + commandName, + "阶段:" + stage, + "window.HAC:" + (hac ? typeof hac : "undefined"), + "window.HAC.registryCallback:" + (hac && hac.registryCallback ? typeof hac.registryCallback : "undefined"), + "window.offlinePlus:" + (offlinePlus ? typeof offlinePlus : "undefined"), + "window.offlinePlus.offlinePlusAddPatternAsync:" + (offlinePlus && offlinePlus.offlinePlusAddPatternAsync ? typeof offlinePlus.offlinePlusAddPatternAsync : "undefined"), + "window.offlinePlus.offlinePlusGetPatternsAsync:" + (offlinePlus && offlinePlus.offlinePlusGetPatternsAsync ? typeof offlinePlus.offlinePlusGetPatternsAsync : "undefined"), + "location:" + window.location.href + ].join("\r\n"); +}; + var HAC_GenerateCellInfo = function (context, formula) { var cellLocation = context.getCellLocation(formula); @@ -18,9 +37,9 @@ var HAC_GenerateCellInfo = function (context, formula) { return JSON.stringify(cellLocation); }; -var HAC_GenerateCallbackTicket = function (context, onSuccess) { +var HAC_GenerateCallbackTicket = function (context, onSuccess, debugInfo) { if (!window.HAC) { - HAC_CallFunctionOutOfHAC(); + HAC_CallFunctionOutOfHAC(debugInfo || "window.HAC is undefined."); } else { var ticket = { @@ -2573,8 +2592,10 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { let me = this; let params = this.CommandParam; + let patternParamJson = ""; let _ticket = HAC_GenerateCallbackTicket(me, function (payload, payload2) { + alert("离线表单项目设置成功\r\n\r\nJSON Schema:\r\n" + patternParamJson); me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { runTimePageName: me.CommandExecutingInfo.runTimePageName, commandID: new Date().valueOf().toString(), @@ -2584,7 +2605,7 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { }, locationString: "执行window.offlinePlus.offlinePlusAddPattern" }); - }); + }, HAC_GetOfflinePlusDebugInfo("添加离线表单项目", "生成回调ticket")); let patternParam = { ticket: _ticket, @@ -2594,12 +2615,13 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { formItems: params.FormItems }; + patternParamJson = JSON.stringify(patternParam, null, 2); console.log(patternParam); if (window.offlinePlus && window.offlinePlus.offlinePlusAddPatternAsync) { - window.offlinePlus.offlinePlusAddPatternAsync(JSON.stringify(patternParam)); + window.offlinePlus.offlinePlusAddPatternAsync(patternParamJson); } else { - HAC_CallFunctionOutOfHAC(); + HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("添加离线表单项目", "调用offlinePlusAddPatternAsync")); } }; @@ -2630,12 +2652,12 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { }, locationString: "执行window.offlinePlus.offlinePlusGetPatternsAsync" }); - }); + }, HAC_GetOfflinePlusDebugInfo("获取已保存项目", "生成回调ticket")); if (window.offlinePlus && window.offlinePlus.offlinePlusGetPatternsAsync) { window.offlinePlus.offlinePlusGetPatternsAsync(ticket); } else { - HAC_CallFunctionOutOfHAC(); + HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("获取已保存项目", "调用offlinePlusGetPatternsAsync")); } }; -- Gitee From 5f3d38bbe9ef4268b49ac2a5b2f4c4c8bcf51cef Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Thu, 4 Jun 2026 16:31:13 +0800 Subject: [PATCH 04/51] feat: add SchemaVersion property to OfflinePlusAddPattern and update command to include it --- OfflinePlusAddPattern_Async.cs | 9 ++++++++- Resources/AndroidPDACommand.js | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/OfflinePlusAddPattern_Async.cs b/OfflinePlusAddPattern_Async.cs index 1e6a3bc..af41d9c 100644 --- a/OfflinePlusAddPattern_Async.cs +++ b/OfflinePlusAddPattern_Async.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using GrapeCity.Forguncy.Commands; using GrapeCity.Forguncy.Plugin; @@ -21,6 +22,12 @@ namespace AndroidPDACommand [FormulaProperty] public object Description { get; set; } + [Required] + [FormulaProperty] + [DisplayName("版本号")] + [Description("同一项目编号下的表单定义版本,用于区分表单结构变更。")] + public string SchemaVersion { get; set; } = "1.0"; + [DisplayName("表单项")] [ObjectListProperty(ItemType = typeof(OfflineFormItem))] public List FormItems { get; set; } @@ -107,4 +114,4 @@ namespace AndroidPDACommand [DisplayName("显示文本")] public object Label { get; set; } } -} \ No newline at end of file +} diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 0eb19e1..dd474d9 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2610,6 +2610,7 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { let patternParam = { ticket: _ticket, patternId: this.evaluateFormula(params.PatternId), + schemaVersion: this.evaluateFormula(params.SchemaVersion), title: this.evaluateFormula(params.Title), description: this.evaluateFormula(params.Description), formItems: params.FormItems -- Gitee From eabb098ab7c0b0240a71fc83672644afc50329b5 Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Mon, 8 Jun 2026 11:47:44 +0800 Subject: [PATCH 05/51] feat(offline): support stepped form schema Replace the flat offline FormItems command model with a Steps list so each offline pattern can describe multiple pages or tabs. Add tree-backed offline form nodes for step content. Nodes can represent collapsible groups, static text, or leaf field items; designer property visibility now follows the selected node type and field item type. Update the HAC command runtime serializer to emit the new steps JSON shape, evaluate nested formula-backed fields, serialize validation/options data, and keep field/text nodes as leaf output structures. --- OfflinePlusAddPattern_Async.cs | 81 ++++++++++++++++++++-- Resources/AndroidPDACommand.js | 121 ++++++++++++++++++++++++++++++++- 2 files changed, 195 insertions(+), 7 deletions(-) diff --git a/OfflinePlusAddPattern_Async.cs b/OfflinePlusAddPattern_Async.cs index af41d9c..9cbc110 100644 --- a/OfflinePlusAddPattern_Async.cs +++ b/OfflinePlusAddPattern_Async.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Linq; using GrapeCity.Forguncy.Commands; using GrapeCity.Forguncy.Plugin; @@ -28,9 +29,9 @@ namespace AndroidPDACommand [Description("同一项目编号下的表单定义版本,用于区分表单结构变更。")] public string SchemaVersion { get; set; } = "1.0"; - [DisplayName("表单项")] - [ObjectListProperty(ItemType = typeof(OfflineFormItem))] - public List FormItems { get; set; } + [DisplayName("步骤")] + [ObjectListProperty(ItemType = typeof(OfflineFormStep))] + public List Steps { get; set; } public override string ToString() { @@ -38,10 +39,43 @@ namespace AndroidPDACommand } } - public class OfflineFormItem : ObjectPropertyBase, INamedObject + public class OfflineFormStep : ObjectPropertyBase, INamedObject { public string Name { get; set; } + [FormulaProperty] + [DisplayName("步骤编号")] + [Description("该步骤的唯一标识,同一个离线表单项目内必须唯一。")] + public object StepId { get; set; } + + [FormulaProperty] + [DisplayName("标题")] + public object Title { get; set; } + + [TreeProperty(NodeType = typeof(OfflineFormNode), DefaultNodeName = "节点")] + [DisplayName("页面内容")] + public List Items { get; set; } = new List(); + } + + public class OfflineFormNode : ObjectPropertyBase, ITreeNode + { + [DisplayName("节点名称")] + public string Text { get; set; } + + [Browsable(false)] + public IEnumerable Children { get; set; } = new List(); + + [DisplayName("节点类型")] + [ComboProperty(ValueList = "group|text|field", DisplayList = "分组|普通文本|表单项")] + public string NodeType { get; set; } = "group"; + + [FormulaProperty] + [DisplayName("说明内容")] + public object Content { get; set; } + + [DisplayName("默认折叠")] + public bool DefaultCollapsed { get; set; } + [FormulaProperty] [DisplayName("编号")] [Description("该表单项的唯一标识,全局唯一,无论是同一项目内,还是跨项目,都必须唯一,更建议使用项目编号作为前缀拼接;例如项目为Project01,则该表单项编号为Project01_Name")] @@ -77,16 +111,51 @@ namespace AndroidPDACommand public override bool GetDesignerPropertyVisible(string propertyName) { + var isField = NodeType == "field"; + var isText = NodeType == "text"; + var isGroup = NodeType == "group"; + switch (propertyName) { + case nameof(Content): + return isText; + case nameof(DefaultCollapsed): + return isGroup; + case nameof(ItemId): + case nameof(ItemType): + case nameof(Title): + case nameof(Hint): + case nameof(Required): + case nameof(Value): + return isField; case nameof(SelectOptionsList): - return ItemType == "selectItem"; + return isField && ItemType == "selectItem"; case nameof(CheckOptions): - return ItemType == "textItem" || ItemType == "passwordItem"; + return isField && (ItemType == "textItem" || ItemType == "passwordItem"); default: return base.GetDesignerPropertyVisible(propertyName); } } + + public override object Clone() + { + return new OfflineFormNode + { + Text = Text, + Children = Children == null ? new List() : Children.ToList(), + NodeType = NodeType, + Content = Content, + DefaultCollapsed = DefaultCollapsed, + ItemId = ItemId, + ItemType = ItemType, + Title = Title, + Hint = Hint, + Required = Required, + Value = Value, + CheckOptions = CheckOptions, + SelectOptionsList = SelectOptionsList + }; + } } public class CheckOptions : ObjectPropertyBase diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index dd474d9..96b794a 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2594,6 +2594,125 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { let params = this.CommandParam; let patternParamJson = ""; + function evaluate(value) { + if (value === undefined || value === null) { + return value; + } + return me.evaluateFormula(value); + } + + function getValue(obj, pascalName, camelName) { + if (!obj) { + return undefined; + } + if (obj[pascalName] !== undefined) { + return obj[pascalName]; + } + return obj[camelName]; + } + + function getList(value) { + return Array.isArray(value) ? value : []; + } + + function buildSelectOptions(options) { + return getList(options).map(function (option) { + return { + value: evaluate(getValue(option, "Value", "value")), + label: evaluate(getValue(option, "Label", "label")) + }; + }); + } + + function buildCheckOptions(options) { + if (!options) { + return undefined; + } + + var checkOptions = {}; + var minLength = evaluate(getValue(options, "MinLength", "minLength")); + var maxLength = evaluate(getValue(options, "MaxLength", "maxLength")); + var regexPattern = evaluate(getValue(options, "RegexPattern", "regexPattern")); + if (minLength !== undefined && minLength !== null && minLength !== "") { + checkOptions.minLength = minLength; + } + if (maxLength !== undefined && maxLength !== null && maxLength !== "") { + checkOptions.maxLength = maxLength; + } + if (regexPattern !== undefined && regexPattern !== null && regexPattern !== "") { + checkOptions.regexPattern = regexPattern; + } + return Object.keys(checkOptions).length > 0 ? checkOptions : undefined; + } + + function buildField(node) { + var field = { + itemId: evaluate(getValue(node, "ItemId", "itemId")), + itemType: getValue(node, "ItemType", "itemType"), + title: evaluate(getValue(node, "Title", "title")), + hint: evaluate(getValue(node, "Hint", "hint")), + required: !!getValue(node, "Required", "required") + }; + + var value = evaluate(getValue(node, "Value", "value")); + var checkOptions = buildCheckOptions(getValue(node, "CheckOptions", "checkOptions")); + var selectOptions = buildSelectOptions(getValue(node, "SelectOptionsList", "selectOptionsList")); + + if (value !== undefined && value !== null && value !== "") { + field.value = value; + } + if (checkOptions) { + field.checkOptions = checkOptions; + } + if (selectOptions.length > 0) { + field.options = selectOptions; + } + return field; + } + + function buildNodes(nodes) { + return getList(nodes).map(function (node) { + var nodeType = getValue(node, "NodeType", "nodeType") || "group"; + var children = getList(getValue(node, "Children", "children")); + + if (nodeType !== "group" && children.length > 0) { + console.warn("离线表单节点不是分组,但包含子节点。", node); + } + + if (nodeType === "field") { + return { + nodeType: "field", + field: buildField(node) + }; + } + + if (nodeType === "text") { + return { + nodeType: "text", + title: getValue(node, "Text", "text"), + content: evaluate(getValue(node, "Content", "content")) + }; + } + + return { + nodeType: "group", + title: getValue(node, "Text", "text"), + defaultCollapsed: !!getValue(node, "DefaultCollapsed", "defaultCollapsed"), + children: buildNodes(children) + }; + }); + } + + function buildSteps(steps) { + return getList(steps).map(function (step) { + return { + stepId: evaluate(getValue(step, "StepId", "stepId")), + title: evaluate(getValue(step, "Title", "title")), + items: buildNodes(getValue(step, "Items", "items")) + }; + }); + } + let _ticket = HAC_GenerateCallbackTicket(me, function (payload, payload2) { alert("离线表单项目设置成功\r\n\r\nJSON Schema:\r\n" + patternParamJson); me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { @@ -2613,7 +2732,7 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { schemaVersion: this.evaluateFormula(params.SchemaVersion), title: this.evaluateFormula(params.Title), description: this.evaluateFormula(params.Description), - formItems: params.FormItems + steps: buildSteps(params.Steps) }; patternParamJson = JSON.stringify(patternParam, null, 2); -- Gitee From 8631f8d7c29cca8e9b6828a1cdcd5faa681a1ecb Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Mon, 8 Jun 2026 15:18:23 +0800 Subject: [PATCH 06/51] refactor: simplify value retrieval in OfflinePlusAddPattern_Async_Command --- Resources/AndroidPDACommand.js | 70 +++++++++++++++------------------- 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 96b794a..802d9ca 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2366,7 +2366,7 @@ var GenericBroadcast_Stop_Command = (function (_super) { var params = this.CommandParam; var uuid = this.evaluateFormula(params.HandlerID); - + if (window.broadcast && window.broadcast.stopReceiving) { window.broadcast.stopReceiving(uuid); @@ -2447,14 +2447,14 @@ var OnKeyDown_Listen_Start_Async_Command = (function (_super) { locationString: "执行window.onKeyDownListen.startOnKeyDownListenAsync的回调" }); }); - + if (window.onKeyDownListen && window.onKeyDownListen.startOnKeyDownListenAsync) { window.onKeyDownListen.startOnKeyDownListenAsync(ticket, theKey, isCover); } else { HAC_CallFunctionOutOfHAC(); } } - + return OnKeyDown_Listen_Start_Async_Command; }(Forguncy.CommandBase)); @@ -2470,14 +2470,14 @@ var OnKeyDown_Listen_Stop_Command = (function (_super) { var params = this.CommandParam; var theKey = this.evaluateFormula(params.TheKey); - + if (window.onKeyDownListen && window.onKeyDownListen.stopOnKeyDownListen) { window.onKeyDownListen.stopOnKeyDownListen(theKey); } else { HAC_CallFunctionOutOfHAC(); } }; - + return OnKeyDown_Listen_Stop_Command; }(Forguncy.CommandBase)); @@ -2601,16 +2601,6 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { return me.evaluateFormula(value); } - function getValue(obj, pascalName, camelName) { - if (!obj) { - return undefined; - } - if (obj[pascalName] !== undefined) { - return obj[pascalName]; - } - return obj[camelName]; - } - function getList(value) { return Array.isArray(value) ? value : []; } @@ -2618,8 +2608,8 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { function buildSelectOptions(options) { return getList(options).map(function (option) { return { - value: evaluate(getValue(option, "Value", "value")), - label: evaluate(getValue(option, "Label", "label")) + value: evaluate(option.Value), + label: evaluate(option.Label) }; }); } @@ -2630,9 +2620,9 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { } var checkOptions = {}; - var minLength = evaluate(getValue(options, "MinLength", "minLength")); - var maxLength = evaluate(getValue(options, "MaxLength", "maxLength")); - var regexPattern = evaluate(getValue(options, "RegexPattern", "regexPattern")); + var minLength = evaluate(options.MinLength); + var maxLength = evaluate(options.MaxLength); + var regexPattern = evaluate(options.RegexPattern); if (minLength !== undefined && minLength !== null && minLength !== "") { checkOptions.minLength = minLength; } @@ -2647,16 +2637,16 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { function buildField(node) { var field = { - itemId: evaluate(getValue(node, "ItemId", "itemId")), - itemType: getValue(node, "ItemType", "itemType"), - title: evaluate(getValue(node, "Title", "title")), - hint: evaluate(getValue(node, "Hint", "hint")), - required: !!getValue(node, "Required", "required") + itemId: evaluate(node.ItemId), + itemType: node.ItemType, + title: evaluate(node.Title), + hint: evaluate(node.Hint), + required: !!node.Required }; - var value = evaluate(getValue(node, "Value", "value")); - var checkOptions = buildCheckOptions(getValue(node, "CheckOptions", "checkOptions")); - var selectOptions = buildSelectOptions(getValue(node, "SelectOptionsList", "selectOptionsList")); + var value = evaluate(node.Value); + var checkOptions = buildCheckOptions(node.CheckOptions); + var selectOptions = buildSelectOptions(node.SelectOptionsList); if (value !== undefined && value !== null && value !== "") { field.value = value; @@ -2672,8 +2662,8 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { function buildNodes(nodes) { return getList(nodes).map(function (node) { - var nodeType = getValue(node, "NodeType", "nodeType") || "group"; - var children = getList(getValue(node, "Children", "children")); + var nodeType = node.NodeType || "group"; + var children = getList(node.Children); if (nodeType !== "group" && children.length > 0) { console.warn("离线表单节点不是分组,但包含子节点。", node); @@ -2689,15 +2679,15 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { if (nodeType === "text") { return { nodeType: "text", - title: getValue(node, "Text", "text"), - content: evaluate(getValue(node, "Content", "content")) + title: node.Text, + content: evaluate(node.Content) }; } return { nodeType: "group", - title: getValue(node, "Text", "text"), - defaultCollapsed: !!getValue(node, "DefaultCollapsed", "defaultCollapsed"), + title: node.Text, + defaultCollapsed: !!node.DefaultCollapsed, children: buildNodes(children) }; }); @@ -2706,9 +2696,9 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { function buildSteps(steps) { return getList(steps).map(function (step) { return { - stepId: evaluate(getValue(step, "StepId", "stepId")), - title: evaluate(getValue(step, "Title", "title")), - items: buildNodes(getValue(step, "Items", "items")) + stepId: evaluate(step.StepId), + title: evaluate(step.Title), + items: buildNodes(step.Items) }; }); } @@ -2725,20 +2715,20 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { locationString: "执行window.offlinePlus.offlinePlusAddPattern" }); }, HAC_GetOfflinePlusDebugInfo("添加离线表单项目", "生成回调ticket")); - + const steps = buildSteps(params.Steps); let patternParam = { ticket: _ticket, patternId: this.evaluateFormula(params.PatternId), schemaVersion: this.evaluateFormula(params.SchemaVersion), title: this.evaluateFormula(params.Title), description: this.evaluateFormula(params.Description), - steps: buildSteps(params.Steps) + steps: steps }; patternParamJson = JSON.stringify(patternParam, null, 2); console.log(patternParam); if (window.offlinePlus && window.offlinePlus.offlinePlusAddPatternAsync) { - + window.offlinePlus.offlinePlusAddPatternAsync(patternParamJson); } else { HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("添加离线表单项目", "调用offlinePlusAddPatternAsync")); -- Gitee From 20c8c8e95d3d892f205abf5dbc488bde7b0d36f3 Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Thu, 11 Jun 2026 10:37:57 +0800 Subject: [PATCH 07/51] =?UTF-8?q?feat(offline-form):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=97=A5=E6=9C=9F=E6=97=B6=E9=97=B4=E5=9B=BE=E7=89=87=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E9=A1=B9=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次提交补齐离线填报新增表单项在活字格插件侧的配置生成能力,使命令设计器能够产出 Android 端已支持的 datePicker、timePicker 和 imageItem schema。 OfflinePlusAddPattern_Async.cs:扩展离线表单项类型下拉选项,新增 datePicker、timePicker、imageItem;为 imageItem 增加最大图片数量和压缩参数配置;按表单项类型控制属性面板显示,避免选择框、校验项、图片配置在不适用的类型上暴露。 Resources/AndroidPDACommand.js:生成离线表单 JSON 时按 itemType 输出类型专属字段;textItem/passwordItem 只输出 checkOptions,selectItem 只输出 options,imageItem 输出 maxCount 与 compression,并保持 Android 端需要的小驼峰字段名。 验证:已执行 git diff --check;已执行 node --check Resources\\AndroidPDACommand.js;已执行 dotnet build AndroidPDACommand.sln --no-restore,结果 0 Warning(s)、0 Error(s)。 --- OfflinePlusAddPattern_Async.cs | 40 +++++++++++++++++++++++++++++++--- Resources/AndroidPDACommand.js | 37 +++++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/OfflinePlusAddPattern_Async.cs b/OfflinePlusAddPattern_Async.cs index 9cbc110..a196909 100644 --- a/OfflinePlusAddPattern_Async.cs +++ b/OfflinePlusAddPattern_Async.cs @@ -82,7 +82,7 @@ namespace AndroidPDACommand public object ItemId { get; set; } [DisplayName("类型")] - [ComboProperty(ValueList = "textItem|passwordItem|selectItem", DisplayList = "文本框|密码框|选择框")] + [ComboProperty(ValueList = "textItem|passwordItem|selectItem|datePicker|timePicker|imageItem", DisplayList = "文本框|密码框|选择框|日期选择|时间选择|图片列表")] public string ItemType { get; set; } = "textItem"; [FormulaProperty] @@ -109,6 +109,14 @@ namespace AndroidPDACommand [DisplayName("选则项")] public List SelectOptionsList { get; set; } + [FormulaProperty] + [DisplayName("图片最大数量")] + public object MaxCount { get; set; } = 9; + + [ObjectProperty(ObjType = typeof(ImageCompressionOptions))] + [DisplayName("图片压缩设置")] + public ImageCompressionOptions CompressionOptions { get; set; } = new ImageCompressionOptions(); + public override bool GetDesignerPropertyVisible(string propertyName) { var isField = NodeType == "field"; @@ -126,12 +134,16 @@ namespace AndroidPDACommand case nameof(Title): case nameof(Hint): case nameof(Required): - case nameof(Value): return isField; + case nameof(Value): + return isField && ItemType != "imageItem"; case nameof(SelectOptionsList): return isField && ItemType == "selectItem"; case nameof(CheckOptions): return isField && (ItemType == "textItem" || ItemType == "passwordItem"); + case nameof(MaxCount): + case nameof(CompressionOptions): + return isField && ItemType == "imageItem"; default: return base.GetDesignerPropertyVisible(propertyName); } @@ -153,7 +165,9 @@ namespace AndroidPDACommand Required = Required, Value = Value, CheckOptions = CheckOptions, - SelectOptionsList = SelectOptionsList + SelectOptionsList = SelectOptionsList, + MaxCount = MaxCount, + CompressionOptions = CompressionOptions }; } } @@ -183,4 +197,24 @@ namespace AndroidPDACommand [DisplayName("显示文本")] public object Label { get; set; } } + + public class ImageCompressionOptions : ObjectPropertyBase + { + [FormulaProperty] + [DisplayName("最长边像素")] + public object MaxLongEdge { get; set; } = 1600; + + [FormulaProperty] + [DisplayName("JPEG质量")] + public object JpegQuality { get; set; } = 80; + + [FormulaProperty] + [DisplayName("目标大小KB")] + [Description("0表示不按文件大小继续压缩。")] + public object MaxFileSizeKb { get; set; } = 0; + + [FormulaProperty] + [DisplayName("最低JPEG质量")] + public object MinQuality { get; set; } = 60; + } } diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 802d9ca..59ae86b 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2635,6 +2635,31 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { return Object.keys(checkOptions).length > 0 ? checkOptions : undefined; } + function buildImageCompression(options) { + if (!options) { + return undefined; + } + + var compression = {}; + var maxLongEdge = evaluate(options.MaxLongEdge); + var jpegQuality = evaluate(options.JpegQuality); + var maxFileSizeKb = evaluate(options.MaxFileSizeKb); + var minQuality = evaluate(options.MinQuality); + if (maxLongEdge !== undefined && maxLongEdge !== null && maxLongEdge !== "") { + compression.maxLongEdge = maxLongEdge; + } + if (jpegQuality !== undefined && jpegQuality !== null && jpegQuality !== "") { + compression.jpegQuality = jpegQuality; + } + if (maxFileSizeKb !== undefined && maxFileSizeKb !== null && maxFileSizeKb !== "") { + compression.maxFileSizeKb = maxFileSizeKb; + } + if (minQuality !== undefined && minQuality !== null && minQuality !== "") { + compression.minQuality = minQuality; + } + return Object.keys(compression).length > 0 ? compression : undefined; + } + function buildField(node) { var field = { itemId: evaluate(node.ItemId), @@ -2645,8 +2670,10 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { }; var value = evaluate(node.Value); - var checkOptions = buildCheckOptions(node.CheckOptions); - var selectOptions = buildSelectOptions(node.SelectOptionsList); + var checkOptions = node.ItemType === "textItem" || node.ItemType === "passwordItem" ? buildCheckOptions(node.CheckOptions) : undefined; + var selectOptions = node.ItemType === "selectItem" ? buildSelectOptions(node.SelectOptionsList) : []; + var maxCount = node.ItemType === "imageItem" ? evaluate(node.MaxCount) : undefined; + var compression = node.ItemType === "imageItem" ? buildImageCompression(node.CompressionOptions) : undefined; if (value !== undefined && value !== null && value !== "") { field.value = value; @@ -2657,6 +2684,12 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { if (selectOptions.length > 0) { field.options = selectOptions; } + if (maxCount !== undefined && maxCount !== null && maxCount !== "") { + field.maxCount = maxCount; + } + if (compression) { + field.compression = compression; + } return field; } -- Gitee From 831ac11ec1501de6815ddab4a1c189a6a002b443 Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Thu, 11 Jun 2026 15:25:18 +0800 Subject: [PATCH 08/51] feat(offline): add title property to field node in OfflinePlusAddPattern_Async_Command --- Resources/AndroidPDACommand.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 59ae86b..44b0882 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2705,6 +2705,7 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { if (nodeType === "field") { return { nodeType: "field", + title: node.Text, field: buildField(node) }; } -- Gitee From 0d491fdb985041384a0b4ea7020cc421f86d57bf Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Thu, 11 Jun 2026 16:31:23 +0800 Subject: [PATCH 09/51] =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=A6=BB=E7=BA=BF?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=E6=97=B6=E9=97=B4=E5=AD=97=E6=AE=B5=E7=A7=92?= =?UTF-8?q?=E6=95=B0=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OfflinePlusAddPattern_Async.cs:在时间选择字段上增加包含秒配置,并用字段类型到属性集合的映射统一控制各表单类型可见属性,减少 AndroidPDACommand.js 与设计器属性可见性之间继续堆 if/else 的成本,同时保持 OfflineFormNode 属性平铺。 Resources/AndroidPDACommand.js:用 fieldTypeSpecs 按表单类型集中生成专属 JSON 属性,让 timePicker 仅在启用包含秒时输出 includeSeconds,同时保留文本、密码、选择和图片字段原有配置输出规则。 --- OfflinePlusAddPattern_Async.cs | 73 +++++++++++++++++++++++++++------- Resources/AndroidPDACommand.js | 71 +++++++++++++++++++++++++-------- 2 files changed, 112 insertions(+), 32 deletions(-) diff --git a/OfflinePlusAddPattern_Async.cs b/OfflinePlusAddPattern_Async.cs index a196909..87c9cfc 100644 --- a/OfflinePlusAddPattern_Async.cs +++ b/OfflinePlusAddPattern_Async.cs @@ -59,6 +59,35 @@ namespace AndroidPDACommand public class OfflineFormNode : ObjectPropertyBase, ITreeNode { + private static readonly string[] CommonFieldVisibleProperties = + { + nameof(ItemId), + nameof(ItemType), + nameof(Title), + nameof(Hint), + nameof(Required) + }; + + private static readonly HashSet FieldTypeControlledProperties = new HashSet(CommonFieldVisibleProperties) + { + nameof(Value), + nameof(CheckOptions), + nameof(SelectOptionsList), + nameof(IncludeSeconds), + nameof(MaxCount), + nameof(CompressionOptions) + }; + + private static readonly Dictionary> FieldVisiblePropertiesByType = new Dictionary> + { + { "textItem", CreateFieldVisibleProperties(nameof(Value), nameof(CheckOptions)) }, + { "passwordItem", CreateFieldVisibleProperties(nameof(Value), nameof(CheckOptions)) }, + { "selectItem", CreateFieldVisibleProperties(nameof(Value), nameof(SelectOptionsList)) }, + { "datePicker", CreateFieldVisibleProperties(nameof(Value)) }, + { "timePicker", CreateFieldVisibleProperties(nameof(Value), nameof(IncludeSeconds)) }, + { "imageItem", CreateFieldVisibleProperties(nameof(MaxCount), nameof(CompressionOptions)) } + }; + [DisplayName("节点名称")] public string Text { get; set; } @@ -99,6 +128,10 @@ namespace AndroidPDACommand [FormulaProperty] [DisplayName("默认值")] public object Value { get; set; } + + [DisplayName("包含秒")] + [Description("仅时间选择类型生效。启用后可选择时分秒;关闭时使用系统原生时分选择器。")] + public bool IncludeSeconds { get; set; } [ObjectProperty(ObjType = typeof(CheckOptions))] @@ -129,22 +162,11 @@ namespace AndroidPDACommand return isText; case nameof(DefaultCollapsed): return isGroup; - case nameof(ItemId): - case nameof(ItemType): - case nameof(Title): - case nameof(Hint): - case nameof(Required): - return isField; - case nameof(Value): - return isField && ItemType != "imageItem"; - case nameof(SelectOptionsList): - return isField && ItemType == "selectItem"; - case nameof(CheckOptions): - return isField && (ItemType == "textItem" || ItemType == "passwordItem"); - case nameof(MaxCount): - case nameof(CompressionOptions): - return isField && ItemType == "imageItem"; default: + if (isField && FieldTypeControlledProperties.Contains(propertyName)) + { + return IsFieldPropertyVisible(ItemType, propertyName); + } return base.GetDesignerPropertyVisible(propertyName); } } @@ -164,12 +186,33 @@ namespace AndroidPDACommand Hint = Hint, Required = Required, Value = Value, + IncludeSeconds = IncludeSeconds, CheckOptions = CheckOptions, SelectOptionsList = SelectOptionsList, MaxCount = MaxCount, CompressionOptions = CompressionOptions }; } + + private static HashSet CreateFieldVisibleProperties(params string[] propertyNames) + { + var visibleProperties = new HashSet(CommonFieldVisibleProperties); + foreach (var propertyName in propertyNames) + { + visibleProperties.Add(propertyName); + } + return visibleProperties; + } + + private static bool IsFieldPropertyVisible(string itemType, string propertyName) + { + HashSet visibleProperties; + if (!FieldVisiblePropertiesByType.TryGetValue(itemType ?? "textItem", out visibleProperties)) + { + visibleProperties = FieldVisiblePropertiesByType["textItem"]; + } + return visibleProperties.Contains(propertyName); + } } public class CheckOptions : ObjectPropertyBase diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 44b0882..64321d4 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2660,6 +2660,59 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { return Object.keys(compression).length > 0 ? compression : undefined; } + var fieldTypeSpecs = { + textItem: { + apply: function (field, node) { + var checkOptions = buildCheckOptions(node.CheckOptions); + if (checkOptions) { + field.checkOptions = checkOptions; + } + } + }, + passwordItem: { + apply: function (field, node) { + var checkOptions = buildCheckOptions(node.CheckOptions); + if (checkOptions) { + field.checkOptions = checkOptions; + } + } + }, + selectItem: { + apply: function (field, node) { + var selectOptions = buildSelectOptions(node.SelectOptionsList); + if (selectOptions.length > 0) { + field.options = selectOptions; + } + } + }, + timePicker: { + apply: function (field, node) { + if (node.IncludeSeconds) { + field.includeSeconds = true; + } + } + }, + imageItem: { + apply: function (field, node) { + var maxCount = evaluate(node.MaxCount); + var compression = buildImageCompression(node.CompressionOptions); + if (maxCount !== undefined && maxCount !== null && maxCount !== "") { + field.maxCount = maxCount; + } + if (compression) { + field.compression = compression; + } + } + } + }; + + function applyFieldTypeSpec(field, node) { + var spec = fieldTypeSpecs[node.ItemType]; + if (spec && spec.apply) { + spec.apply(field, node); + } + } + function buildField(node) { var field = { itemId: evaluate(node.ItemId), @@ -2670,26 +2723,10 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { }; var value = evaluate(node.Value); - var checkOptions = node.ItemType === "textItem" || node.ItemType === "passwordItem" ? buildCheckOptions(node.CheckOptions) : undefined; - var selectOptions = node.ItemType === "selectItem" ? buildSelectOptions(node.SelectOptionsList) : []; - var maxCount = node.ItemType === "imageItem" ? evaluate(node.MaxCount) : undefined; - var compression = node.ItemType === "imageItem" ? buildImageCompression(node.CompressionOptions) : undefined; - if (value !== undefined && value !== null && value !== "") { field.value = value; } - if (checkOptions) { - field.checkOptions = checkOptions; - } - if (selectOptions.length > 0) { - field.options = selectOptions; - } - if (maxCount !== undefined && maxCount !== null && maxCount !== "") { - field.maxCount = maxCount; - } - if (compression) { - field.compression = compression; - } + applyFieldTypeSpec(field, node); return field; } -- Gitee From 28672e4cdf76f72b6b14114b736e38ee8859437d Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Fri, 12 Jun 2026 11:39:12 +0800 Subject: [PATCH 10/51] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=A6=BB=E7=BA=BF?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=88=97=E8=A1=A8=E8=AE=BE=E8=AE=A1=E6=97=B6?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OfflinePlusAddPattern_Async.cs:在图片列表表单项中补充允许图片上传、图片压缩开关和拍照水印配置,使设计器能够按图片项展示这些配置;新增拍照水印配置对象和自定义字段列表,默认提供用户名、姓名两个字段,并把这些属性纳入克隆逻辑,避免设计器复制节点时丢失配置。 Resources/AndroidPDACommand.js:在生成离线表单 JSON 时输出 allowImageUpload、compression.enableCompression 和 watermark 配置;watermark 使用 enableTimestamp 与 key/value 字段列表,供 Android 端后续按字段顺序用横线拼接生成拍照水印。 --- OfflinePlusAddPattern_Async.cs | 46 +++++++++++++++++++++++++++++++--- Resources/AndroidPDACommand.js | 32 +++++++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/OfflinePlusAddPattern_Async.cs b/OfflinePlusAddPattern_Async.cs index 87c9cfc..f0e9034 100644 --- a/OfflinePlusAddPattern_Async.cs +++ b/OfflinePlusAddPattern_Async.cs @@ -75,7 +75,9 @@ namespace AndroidPDACommand nameof(SelectOptionsList), nameof(IncludeSeconds), nameof(MaxCount), - nameof(CompressionOptions) + nameof(AllowImageUpload), + nameof(CompressionOptions), + nameof(WatermarkOptions) }; private static readonly Dictionary> FieldVisiblePropertiesByType = new Dictionary> @@ -85,7 +87,7 @@ namespace AndroidPDACommand { "selectItem", CreateFieldVisibleProperties(nameof(Value), nameof(SelectOptionsList)) }, { "datePicker", CreateFieldVisibleProperties(nameof(Value)) }, { "timePicker", CreateFieldVisibleProperties(nameof(Value), nameof(IncludeSeconds)) }, - { "imageItem", CreateFieldVisibleProperties(nameof(MaxCount), nameof(CompressionOptions)) } + { "imageItem", CreateFieldVisibleProperties(nameof(MaxCount), nameof(AllowImageUpload), nameof(CompressionOptions), nameof(WatermarkOptions)) } }; [DisplayName("节点名称")] @@ -146,10 +148,18 @@ namespace AndroidPDACommand [DisplayName("图片最大数量")] public object MaxCount { get; set; } = 9; + [DisplayName("允许图片上传")] + [Description("仅图片列表类型生效。启用后填报时除拍照外还可以从本地选择图片。")] + public bool AllowImageUpload { get; set; } + [ObjectProperty(ObjType = typeof(ImageCompressionOptions))] [DisplayName("图片压缩设置")] public ImageCompressionOptions CompressionOptions { get; set; } = new ImageCompressionOptions(); + [ObjectProperty(ObjType = typeof(ImageWatermarkOptions))] + [DisplayName("拍照水印配置")] + public ImageWatermarkOptions WatermarkOptions { get; set; } = new ImageWatermarkOptions(); + public override bool GetDesignerPropertyVisible(string propertyName) { var isField = NodeType == "field"; @@ -190,7 +200,9 @@ namespace AndroidPDACommand CheckOptions = CheckOptions, SelectOptionsList = SelectOptionsList, MaxCount = MaxCount, - CompressionOptions = CompressionOptions + AllowImageUpload = AllowImageUpload, + CompressionOptions = CompressionOptions, + WatermarkOptions = WatermarkOptions }; } @@ -243,6 +255,10 @@ namespace AndroidPDACommand public class ImageCompressionOptions : ObjectPropertyBase { + [DisplayName("启用压缩")] + [Description("默认关闭,关闭时按原图保存。启用后才使用下面的压缩参数。")] + public bool EnableCompression { get; set; } + [FormulaProperty] [DisplayName("最长边像素")] public object MaxLongEdge { get; set; } = 1600; @@ -260,4 +276,28 @@ namespace AndroidPDACommand [DisplayName("最低JPEG质量")] public object MinQuality { get; set; } = 60; } + + public class ImageWatermarkOptions : ObjectPropertyBase + { + [DisplayName("时间戳水印")] + public bool EnableTimestamp { get; set; } = true; + + [ListProperty] + [DisplayName("自定义字段")] + public List Fields { get; set; } = new List + { + new ImageWatermarkField { Key = "用户名", Value = "用户名" }, + new ImageWatermarkField { Key = "姓名", Value = "姓名" } + }; + } + + public class ImageWatermarkField : ObjectPropertyBase + { + [DisplayName("Key")] + public string Key { get; set; } + + [FormulaProperty] + [DisplayName("Value")] + public object Value { get; set; } + } } diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 64321d4..330ecbb 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2645,6 +2645,7 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { var jpegQuality = evaluate(options.JpegQuality); var maxFileSizeKb = evaluate(options.MaxFileSizeKb); var minQuality = evaluate(options.MinQuality); + compression.enableCompression = !!options.EnableCompression; if (maxLongEdge !== undefined && maxLongEdge !== null && maxLongEdge !== "") { compression.maxLongEdge = maxLongEdge; } @@ -2660,6 +2661,30 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { return Object.keys(compression).length > 0 ? compression : undefined; } + function buildImageWatermark(options) { + if (!options) { + return undefined; + } + + var watermark = {}; + var items = getList(options.Fields).map(function (field) { + return { + key: field.Key, + value: evaluate(field.Value) + }; + }).filter(function (field) { + return field.value !== undefined && field.value !== null && field.value !== ""; + }); + + if (options.EnableTimestamp) { + watermark.enableTimestamp = true; + } + if (items.length > 0) { + watermark.items = items; + } + return Object.keys(watermark).length > 0 ? watermark : undefined; + } + var fieldTypeSpecs = { textItem: { apply: function (field, node) { @@ -2696,12 +2721,19 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { apply: function (field, node) { var maxCount = evaluate(node.MaxCount); var compression = buildImageCompression(node.CompressionOptions); + var watermark = buildImageWatermark(node.WatermarkOptions); if (maxCount !== undefined && maxCount !== null && maxCount !== "") { field.maxCount = maxCount; } + if (node.AllowImageUpload) { + field.allowImageUpload = true; + } if (compression) { field.compression = compression; } + if (watermark) { + field.watermark = watermark; + } } } }; -- Gitee From 76a8c8f3f2e738fcb398df38c556c10882ea74f4 Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Fri, 12 Jun 2026 12:05:23 +0800 Subject: [PATCH 11/51] feat(watermark): disable timestamp by default and update field handling logic --- OfflinePlusAddPattern_Async.cs | 4 +--- Resources/AndroidPDACommand.js | 9 ++++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/OfflinePlusAddPattern_Async.cs b/OfflinePlusAddPattern_Async.cs index f0e9034..08b1ee2 100644 --- a/OfflinePlusAddPattern_Async.cs +++ b/OfflinePlusAddPattern_Async.cs @@ -280,14 +280,12 @@ namespace AndroidPDACommand public class ImageWatermarkOptions : ObjectPropertyBase { [DisplayName("时间戳水印")] - public bool EnableTimestamp { get; set; } = true; + public bool EnableTimestamp { get; set; } = false; [ListProperty] [DisplayName("自定义字段")] public List Fields { get; set; } = new List { - new ImageWatermarkField { Key = "用户名", Value = "用户名" }, - new ImageWatermarkField { Key = "姓名", Value = "姓名" } }; } diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 330ecbb..762e79e 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2666,14 +2666,17 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { return undefined; } + var fields = getList(options.Fields); + if (!options.EnableTimestamp && fields.length === 0) { + return undefined; + } + var watermark = {}; - var items = getList(options.Fields).map(function (field) { + var items = fields.map(function (field) { return { key: field.Key, value: evaluate(field.Value) }; - }).filter(function (field) { - return field.value !== undefined && field.value !== null && field.value !== ""; }); if (options.EnableTimestamp) { -- Gitee From fa1641062a7419d6a3bd46e8d9cef45ae2dda5d2 Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Fri, 12 Jun 2026 17:45:19 +0800 Subject: [PATCH 12/51] =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=A6=BB=E7=BA=BF?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E8=AE=BE=E8=AE=A1=E6=97=B6?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OfflinePlusAddPattern_Async.cs:在离线表单节点类型中增加 fileItem,并新增 FileItemConfig 配置对象,让设计时可以配置最大数量、允许扩展名和单文件大小限制;同时把文件上传配置纳入字段类型属性可见性控制,只在 fileItem 类型下显示。 Resources/AndroidPDACommand.js:为 fileItem 增加运行时 JSON 输出逻辑,将设计时 FileItemConfig 转换成 field.fileItemConfig;扩展名字符串按用户输入原样输出,Android 运行时再根据扩展名做选择器 MIME 推断和保存前校验。 --- OfflinePlusAddPattern_Async.cs | 33 +++++++++++++++++++++++++++++---- Resources/AndroidPDACommand.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/OfflinePlusAddPattern_Async.cs b/OfflinePlusAddPattern_Async.cs index 08b1ee2..f791b8b 100644 --- a/OfflinePlusAddPattern_Async.cs +++ b/OfflinePlusAddPattern_Async.cs @@ -77,7 +77,8 @@ namespace AndroidPDACommand nameof(MaxCount), nameof(AllowImageUpload), nameof(CompressionOptions), - nameof(WatermarkOptions) + nameof(WatermarkOptions), + nameof(FileItemConfig) }; private static readonly Dictionary> FieldVisiblePropertiesByType = new Dictionary> @@ -87,7 +88,8 @@ namespace AndroidPDACommand { "selectItem", CreateFieldVisibleProperties(nameof(Value), nameof(SelectOptionsList)) }, { "datePicker", CreateFieldVisibleProperties(nameof(Value)) }, { "timePicker", CreateFieldVisibleProperties(nameof(Value), nameof(IncludeSeconds)) }, - { "imageItem", CreateFieldVisibleProperties(nameof(MaxCount), nameof(AllowImageUpload), nameof(CompressionOptions), nameof(WatermarkOptions)) } + { "imageItem", CreateFieldVisibleProperties(nameof(MaxCount), nameof(AllowImageUpload), nameof(CompressionOptions), nameof(WatermarkOptions)) }, + { "fileItem", CreateFieldVisibleProperties(nameof(FileItemConfig)) } }; [DisplayName("节点名称")] @@ -113,7 +115,7 @@ namespace AndroidPDACommand public object ItemId { get; set; } [DisplayName("类型")] - [ComboProperty(ValueList = "textItem|passwordItem|selectItem|datePicker|timePicker|imageItem", DisplayList = "文本框|密码框|选择框|日期选择|时间选择|图片列表")] + [ComboProperty(ValueList = "textItem|passwordItem|selectItem|datePicker|timePicker|imageItem|fileItem", DisplayList = "文本框|密码框|选择框|日期选择|时间选择|图片列表|文件上传")] public string ItemType { get; set; } = "textItem"; [FormulaProperty] @@ -160,6 +162,10 @@ namespace AndroidPDACommand [DisplayName("拍照水印配置")] public ImageWatermarkOptions WatermarkOptions { get; set; } = new ImageWatermarkOptions(); + [ObjectProperty(ObjType = typeof(FileItemConfig))] + [DisplayName("文件上传配置")] + public FileItemConfig FileItemConfig { get; set; } = new FileItemConfig(); + public override bool GetDesignerPropertyVisible(string propertyName) { var isField = NodeType == "field"; @@ -202,7 +208,8 @@ namespace AndroidPDACommand MaxCount = MaxCount, AllowImageUpload = AllowImageUpload, CompressionOptions = CompressionOptions, - WatermarkOptions = WatermarkOptions + WatermarkOptions = WatermarkOptions, + FileItemConfig = FileItemConfig }; } @@ -298,4 +305,22 @@ namespace AndroidPDACommand [DisplayName("Value")] public object Value { get; set; } } + + public class FileItemConfig : ObjectPropertyBase + { + [FormulaProperty] + [DisplayName("最大数量")] + [Description("0表示不限制上传文件数量。")] + public object MaxCount { get; set; } = 0; + + [FormulaProperty] + [DisplayName("允许扩展名")] + [Description("逗号分隔,每个扩展名前带点;留空表示不限制,例如:.pdf,.doc,.docx。")] + public object AllowedExtensions { get; set; } + + [FormulaProperty] + [DisplayName("单文件最大大小KB")] + [Description("0表示不限制单文件大小。")] + public object MaxFileSizeKb { get; set; } = 0; + } } diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 762e79e..1d82cad 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2688,6 +2688,27 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { return Object.keys(watermark).length > 0 ? watermark : undefined; } + function buildFileItemConfig(options) { + if (!options) { + return undefined; + } + + var config = {}; + var maxCount = evaluate(options.MaxCount); + var allowedExtensions = evaluate(options.AllowedExtensions); + var maxFileSizeKb = evaluate(options.MaxFileSizeKb); + if (maxCount !== undefined && maxCount !== null && maxCount !== "") { + config.maxCount = maxCount; + } + if (allowedExtensions !== undefined && allowedExtensions !== null && allowedExtensions !== "") { + config.allowedExtensions = allowedExtensions; + } + if (maxFileSizeKb !== undefined && maxFileSizeKb !== null && maxFileSizeKb !== "") { + config.maxFileSizeKb = maxFileSizeKb; + } + return Object.keys(config).length > 0 ? config : undefined; + } + var fieldTypeSpecs = { textItem: { apply: function (field, node) { @@ -2738,6 +2759,14 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { field.watermark = watermark; } } + }, + fileItem: { + apply: function (field, node) { + var config = buildFileItemConfig(node.FileItemConfig); + if (config) { + field.fileItemConfig = config; + } + } } }; -- Gitee From fc5e4d7b95280df450abff481eaf62ef8626333c Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Sat, 13 Jun 2026 11:56:04 +0800 Subject: [PATCH 13/51] =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=A6=BB=E7=BA=BF?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=E6=89=8B=E5=86=8C=E4=B8=8E=E5=91=98=E5=B7=A5?= =?UTF-8?q?=E7=AD=BE=E5=90=8D=E8=AE=BE=E8=AE=A1=E6=97=B6=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OfflinePlusAddPattern_Async.cs:新增手册附件单元格配置,让设计人员可以指定保存 PDF 手册附件的单元格,后续下载离线表单时可把手册随表单传给安卓端保存。新增员工签名单元格配置,用于接收签名单元格直接产出的 Base64 字符串,使员工签名能跟随离线表单一起下发到离线设备。 Resources/AndroidPDACommand.js:将添加离线表单项目命令改为 async 执行,以便在提交表单定义前先通过手册附件单元格生成下载地址并读取 PDF Base64。保留表单定义 JSON 本身不包含手册和签名内容,改为调用 offlinePlusAddPatternAsync 时把手册 Base64 作为第二个参数、签名 Base64 作为第三个参数传给安卓端,避免把大附件内容拼进 definition JSON。 --- OfflinePlusAddPattern_Async.cs | 10 +++++++ Resources/AndroidPDACommand.js | 53 ++++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/OfflinePlusAddPattern_Async.cs b/OfflinePlusAddPattern_Async.cs index f791b8b..16a2886 100644 --- a/OfflinePlusAddPattern_Async.cs +++ b/OfflinePlusAddPattern_Async.cs @@ -29,6 +29,16 @@ namespace AndroidPDACommand [Description("同一项目编号下的表单定义版本,用于区分表单结构变更。")] public string SchemaVersion { get; set; } = "1.0"; + [FormulaProperty] + [DisplayName("手册附件单元格")] + [Description("选择保存 PDF 手册附件的单元格。下载离线表单时会把 PDF 手册随表单一起保存到离线设备。")] + public object ManualAttachmentCell { get; set; } + + [FormulaProperty] + [DisplayName("员工签名单元格")] + [Description("选择保存员工签名附件的单元格。下载离线表单时会把签名文件随表单一起保存到离线设备。")] + public object SignatureAttachmentCell { get; set; } + [DisplayName("步骤")] [ObjectListProperty(ItemType = typeof(OfflineFormStep))] public List Steps { get; set; } diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 1d82cad..14f71b4 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2588,7 +2588,7 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { return _super !== null && _super.apply(this, arguments) || this; } - OfflinePlusAddPattern_Async_Command.prototype.execute = function () { + OfflinePlusAddPattern_Async_Command.prototype.execute = async function () { let me = this; let params = this.CommandParam; @@ -2838,6 +2838,47 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { }); } + function readBlobAsBase64(blob) { + return new Promise(function (resolve, reject) { + var reader = new FileReader(); + reader.onload = function () { + var dataUrl = reader.result || ""; + var commaIndex = dataUrl.indexOf(","); + resolve(commaIndex >= 0 ? dataUrl.substring(commaIndex + 1) : dataUrl); + }; + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + } + + async function loadManualPdfBase64() { + if (params.ManualAttachmentCell === undefined || params.ManualAttachmentCell === null || params.ManualAttachmentCell === "") { + return undefined; + } + + var manualValue = evaluate(params.ManualAttachmentCell); + if (!manualValue) { + return undefined; + } + + var fileId = (manualValue + "").split("|")[0]; + if (!fileId) { + console.warn("手册附件单元格没有文件标识。", manualValue); + return undefined; + } + + var manualDownloadUrl = Forguncy.Helper.SpecialPath.getFileDownloadUrl(fileId); + var response = await fetch(manualDownloadUrl); + if (!response.ok) { + throw new Error("下载手册 PDF 失败:" + response.status); + } + + var blob = await response.blob(); + var base64 = await readBlobAsBase64(blob); + console.log("Manual PDF Base64", base64); + return base64; + } + let _ticket = HAC_GenerateCallbackTicket(me, function (payload, payload2) { alert("离线表单项目设置成功\r\n\r\nJSON Schema:\r\n" + patternParamJson); me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { @@ -2859,12 +2900,18 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { description: this.evaluateFormula(params.Description), steps: steps }; + const signatureBase64 = this.evaluateFormula(params.SignatureAttachmentCell); + var manualPdfBase64; + try { + manualPdfBase64 = await loadManualPdfBase64(); + } catch (error) { + console.error("读取手册 PDF Base64 失败:", error); + } patternParamJson = JSON.stringify(patternParam, null, 2); console.log(patternParam); if (window.offlinePlus && window.offlinePlus.offlinePlusAddPatternAsync) { - - window.offlinePlus.offlinePlusAddPatternAsync(patternParamJson); + window.offlinePlus.offlinePlusAddPatternAsync(patternParamJson, manualPdfBase64, signatureBase64); } else { HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("添加离线表单项目", "调用offlinePlusAddPatternAsync")); } -- Gitee From 7af7e61a13f00474603d3516c31d1ba7fdd5e934 Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Sat, 13 Jun 2026 12:31:43 +0800 Subject: [PATCH 14/51] refactor(offline-plus): simplify manual PDF handling and improve URL retrieval --- Resources/AndroidPDACommand.js | 39 ++++++---------------------------- 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 14f71b4..c7c9dc8 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2838,20 +2838,7 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { }); } - function readBlobAsBase64(blob) { - return new Promise(function (resolve, reject) { - var reader = new FileReader(); - reader.onload = function () { - var dataUrl = reader.result || ""; - var commaIndex = dataUrl.indexOf(","); - resolve(commaIndex >= 0 ? dataUrl.substring(commaIndex + 1) : dataUrl); - }; - reader.onerror = reject; - reader.readAsDataURL(blob); - }); - } - - async function loadManualPdfBase64() { + function getManualPdfUrl() { if (params.ManualAttachmentCell === undefined || params.ManualAttachmentCell === null || params.ManualAttachmentCell === "") { return undefined; } @@ -2867,16 +2854,7 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { return undefined; } - var manualDownloadUrl = Forguncy.Helper.SpecialPath.getFileDownloadUrl(fileId); - var response = await fetch(manualDownloadUrl); - if (!response.ok) { - throw new Error("下载手册 PDF 失败:" + response.status); - } - - var blob = await response.blob(); - var base64 = await readBlobAsBase64(blob); - console.log("Manual PDF Base64", base64); - return base64; + return Forguncy.Helper.SpecialPath.getFileDownloadUrl(fileId); } let _ticket = HAC_GenerateCallbackTicket(me, function (payload, payload2) { @@ -2900,18 +2878,15 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { description: this.evaluateFormula(params.Description), steps: steps }; - const signatureBase64 = this.evaluateFormula(params.SignatureAttachmentCell); - var manualPdfBase64; - try { - manualPdfBase64 = await loadManualPdfBase64(); - } catch (error) { - console.error("读取手册 PDF Base64 失败:", error); - } + const signatureBase64 = params.SignatureAttachmentCell === undefined || params.SignatureAttachmentCell === null || params.SignatureAttachmentCell === "" + ? "" + : this.evaluateFormula(params.SignatureAttachmentCell); + var manualPdfUrl = getManualPdfUrl(); patternParamJson = JSON.stringify(patternParam, null, 2); console.log(patternParam); if (window.offlinePlus && window.offlinePlus.offlinePlusAddPatternAsync) { - window.offlinePlus.offlinePlusAddPatternAsync(patternParamJson, manualPdfBase64, signatureBase64); + window.offlinePlus.offlinePlusAddPatternAsync(patternParamJson, manualPdfUrl || "", signatureBase64 || ""); } else { HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("添加离线表单项目", "调用offlinePlusAddPatternAsync")); } -- Gitee From 2b0f5ca487e7a35a457a1f2cd85a80eacb25846f Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Sat, 13 Jun 2026 15:24:45 +0800 Subject: [PATCH 15/51] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=AF=BB=E5=8F=96?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E8=AE=BE=E5=A4=87=E4=BF=A1=E6=81=AF=E5=91=BD?= =?UTF-8?q?=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Device_Info_Json.cs:新增读取设备信息命令,作为旧 Device_Info 唯一标识命令的并列新命令,避免改变现有命令的行为和兼容性。 Device_Info_Json.cs:命令结果属性 DeviceInfoJson 使用 ResultToProperty 返回完整设备信息 JSON,设计器显示文案明确包含唯一标识、品牌、型号、Android 版本、APP 版本和 WebView 版本。 Resources/AndroidPDACommand.js:注册 AndroidPDACommand.Device_Info_Json 命令,并按 C# 属性名直接读取 DeviceInfoJson,运行时调用 window.device.getDeviceInfo(),HAC 外环境仍按现有模式返回空字符串。 AndroidPDACommand.csproj:加入 Device_Info_Json.cs 编译项,确保新命令随插件工程一起编译和安装。 --- AndroidPDACommand.csproj | 1 + Device_Info_Json.cs | 27 +++++++++++++++++++++++++++ Resources/AndroidPDACommand.js | 25 +++++++++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 Device_Info_Json.cs diff --git a/AndroidPDACommand.csproj b/AndroidPDACommand.csproj index 376c9ec..6892606 100644 --- a/AndroidPDACommand.csproj +++ b/AndroidPDACommand.csproj @@ -81,6 +81,7 @@ + diff --git a/Device_Info_Json.cs b/Device_Info_Json.cs new file mode 100644 index 0000000..2c3c05a --- /dev/null +++ b/Device_Info_Json.cs @@ -0,0 +1,27 @@ +using GrapeCity.Forguncy.Commands; +using GrapeCity.Forguncy.Plugin; +using System.ComponentModel; + +namespace AndroidPDACommand +{ + [Icon("pack://application:,,,/AndroidPDACommand;component/Resources/Icon_Phone.png")] + [Category("活字格安卓容器(HAC)")] + [OrderWeight(903)] + public class Device_Info_Json : BaseCommand + { + [DisplayName("将设备信息JSON返回到变量")] + [Description("读取设备唯一标识、品牌、型号、Android版本、APP版本、WebView版本等基础信息。")] + [SearchableProperty] + [ResultToProperty] + public string DeviceInfoJson { get; set; } + + public override string ToString() + { + if (string.IsNullOrEmpty(DeviceInfoJson)) + { + return "读取设备信息"; + } + return "读取设备信息:" + DeviceInfoJson; + } + } +} diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index c7c9dc8..e7f0bbd 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -1405,6 +1405,31 @@ var Device_Info_Command = (function (_super) { Forguncy.CommandFactory.registerCommand("AndroidPDACommand.Device_Info, AndroidPDACommand", Device_Info_Command); +var Device_Info_Json_Command = (function (_super) { + __extends(Device_Info_Json_Command, _super); + function Device_Info_Json_Command() { + return _super !== null && _super.apply(this, arguments) || this; + } + + Device_Info_Json_Command.prototype.execute = function () { + var params = this.CommandParam; + var pName = params.DeviceInfoJson; + + if (window.device && window.device.getDeviceInfo) { + var value = window.device.getDeviceInfo(); + + HAC_ReturnToParam(pName, value); + } else { + HAC_CallFunctionOutOfHAC(); + HAC_ReturnToParam(pName, ""); + } + }; + + return Device_Info_Json_Command; +}(Forguncy.CommandBase)); + +Forguncy.CommandFactory.registerCommand("AndroidPDACommand.Device_Info_Json, AndroidPDACommand", Device_Info_Json_Command); + var BLE_Scan_Command = (function (_super) { __extends(BLE_Scan_Command, _super); function BLE_Scan_Command() { -- Gitee From 501db334a3e7c85ffc1b929ce935098142a710a3 Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Sat, 13 Jun 2026 16:46:40 +0800 Subject: [PATCH 16/51] =?UTF-8?q?=E8=B0=83=E6=95=B4=E7=A6=BB=E7=BA=BF?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=E4=B8=8A=E4=BC=A0=E4=B8=8B=E8=BD=BD=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OfflinePlusAddPattern_Async.cs:仅调整设计器展示名称为“下载离线表单”,保留原 OfflinePlusAddPattern_Async 类名和运行时注册名,使下载动作与上传动作在设计器中形成对应关系。 OfflinePlusGetPatterns_Async.cs:保留原类名作为上传命令入口,将展示名称改为“上传离线表单”;新增动作属性,当前只提供“获取设备上的离线表单项目编号列表”,为后续上传数据、删除记录等 action 预留同一命令入口。 OfflinePlusGetPatterns_Async.cs:将 ProjectsResult 改为设计器可见的结果变量配置,确保项目编号列表 JSON 能由活字格开发人员明确指定返回变量。 Resources/AndroidPDACommand.js:在原 OfflinePlusGetPatterns_Async_Command 内部直接增加 action 判断,不新增额外 helper,不改变原 JS 构造函数名和命令注册名;Android 回调后同时写入 ProjectsResult 变量并保留子命令 initParams。 Resources/AndroidPDACommand.js:调用 window.offlinePlus.offlinePlusGetPatternsAsync(ticket),与 Android bridge 方法名保持一致,并将诊断文案调整为上传离线表单。 AndroidPDACommand.csproj:新增下载/上传两个离线表单图标资源的嵌入配置,保证设计器命令图标可加载。 Resources/Icon_OfflineDownload.png、Resources/Icon_OfflineUpload.png:新增一组方向相反的离线表单图标,用于表达下载离线表单和上传离线表单的对应关系。 --- AndroidPDACommand.csproj | 2 ++ OfflinePlusAddPattern_Async.cs | 3 ++- OfflinePlusGetPatterns_Async.cs | 18 ++++++++++++------ Resources/AndroidPDACommand.js | 14 ++++++++++---- Resources/Icon_OfflineDownload.png | Bin 0 -> 290 bytes Resources/Icon_OfflineUpload.png | Bin 0 -> 279 bytes 6 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 Resources/Icon_OfflineDownload.png create mode 100644 Resources/Icon_OfflineUpload.png diff --git a/AndroidPDACommand.csproj b/AndroidPDACommand.csproj index 6892606..13b436e 100644 --- a/AndroidPDACommand.csproj +++ b/AndroidPDACommand.csproj @@ -166,6 +166,8 @@ + + diff --git a/OfflinePlusAddPattern_Async.cs b/OfflinePlusAddPattern_Async.cs index 16a2886..308192d 100644 --- a/OfflinePlusAddPattern_Async.cs +++ b/OfflinePlusAddPattern_Async.cs @@ -7,6 +7,7 @@ using GrapeCity.Forguncy.Plugin; namespace AndroidPDACommand { + [Icon("pack://application:,,,/AndroidPDACommand;component/Resources/Icon_OfflineDownload.png")] [Category("活字格安卓容器(HAC)")] public class OfflinePlusAddPattern_Async : BaseAsyncCommand { @@ -45,7 +46,7 @@ namespace AndroidPDACommand public override string ToString() { - return "添加离线表单项目"; + return "下载离线表单"; } } diff --git a/OfflinePlusGetPatterns_Async.cs b/OfflinePlusGetPatterns_Async.cs index b3257de..cc1b509 100644 --- a/OfflinePlusGetPatterns_Async.cs +++ b/OfflinePlusGetPatterns_Async.cs @@ -1,18 +1,24 @@ using System.ComponentModel; using GrapeCity.Forguncy.Commands; +using GrapeCity.Forguncy.Plugin; namespace AndroidPDACommand { + [Icon("pack://application:,,,/AndroidPDACommand;component/Resources/Icon_OfflineUpload.png")] + [Category("活字格安卓容器(HAC)")] public class OfflinePlusGetPatterns_Async : BaseAsyncCommand { - - [Browsable(false)] + [DisplayName("动作")] + [ComboProperty(ValueList = "GetPatterns", DisplayList = "获取设备上的离线表单项目编号列表")] + public string Action { get; set; } = "GetPatterns"; + + [DisplayName("将离线表单项目编号列表返回到变量")] [ResultToProperty] - public string ProjectsResult { get; set; } = "projects"; - + public string ProjectsResult { get; set; } + public override string ToString() { - return "获取已保存项目"; + return "上传离线表单"; } } -} \ No newline at end of file +} diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index e7f0bbd..40ff061 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2930,10 +2930,18 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { } OfflinePlusGetPatterns_Async_Command.prototype.execute = function () { + var params = this.CommandParam; + var action = params.Action || "GetPatterns"; + + if (action !== "GetPatterns") { + alert("不支持的离线表单上传动作:" + action); + return; + } let me = this; let ticket = HAC_GenerateCallbackTicket(me, function (payload, payload2) { + HAC_ReturnToParam(params.ProjectsResult, payload); me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { runTimePageName: me.CommandExecutingInfo.runTimePageName, commandID: new Date().valueOf().toString(), @@ -2944,17 +2952,15 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { }, locationString: "执行window.offlinePlus.offlinePlusGetPatternsAsync" }); - }, HAC_GetOfflinePlusDebugInfo("获取已保存项目", "生成回调ticket")); + }, HAC_GetOfflinePlusDebugInfo("上传离线表单", "生成回调ticket")); if (window.offlinePlus && window.offlinePlus.offlinePlusGetPatternsAsync) { window.offlinePlus.offlinePlusGetPatternsAsync(ticket); } else { - HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("获取已保存项目", "调用offlinePlusGetPatternsAsync")); + HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("上传离线表单", "调用offlinePlusGetPatternsAsync")); } - }; - return OfflinePlusGetPatterns_Async_Command; }(Forguncy.CommandBase)); diff --git a/Resources/Icon_OfflineDownload.png b/Resources/Icon_OfflineDownload.png new file mode 100644 index 0000000000000000000000000000000000000000..1295a66c444353576d3580da122e425f19b457b9 GIT binary patch literal 290 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc!R?+djv*HQ$q5oy9QN4Va@=IMi_vBAX+c(p zS*p@W|C%#OEO`=yuNU05*z(BUDD3p9U;IW4Kn0Cg7E3S){rUBnpDXYFX0{xL4!7yd zE01R#xZQvMzkS_T&YsuX^w^~t79}PrRNj(uFxEeiqn2^tqV@IzQVuuL!<26@2;AM1 zta?K=UGCoDd*#ZQ#1h+ZS|H`Di)2F%$F8(BY%^R#YS}m!C7-=w!(%AG gw$k0OA%=lL^u#J-k=mV=Krb+Oy85}Sb4q9e01!22OaK4? literal 0 HcmV?d00001 diff --git a/Resources/Icon_OfflineUpload.png b/Resources/Icon_OfflineUpload.png new file mode 100644 index 0000000000000000000000000000000000000000..cb7876aa0535f3281c9228c20470eeb39031e1e6 GIT binary patch literal 279 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc!8M*Pjv*HQ$q5TgZ@kZ6*P!QSa^rFSybbyX z{@8c8O;_efn3XtVfudO<@Ah72?SIV$RRv5j%0_IRhp#kTSuDXI^yk;(^Z)(x@BiPw zGet~d{<>2v*(?_g3@6xl$a>g@G2g5E^Y8!1)&2=JiSI%-Fe*r?u}xaOmhptdlavWS zUM$~o#vL&Xn`eGw3wXnt^5@Z=%nfM*y?X9b7=IYFF{TNI&Ub7&k~E=8g!u$3!xpDA V52S>6s(|ih@O1TaS?83{1OO1XWwZbQ literal 0 HcmV?d00001 -- Gitee From db356877e20c1ed86c0b40f1fc964da871785d1f Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Sat, 13 Jun 2026 17:33:03 +0800 Subject: [PATCH 17/51] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E7=A6=BB=E7=BA=BF=E8=A1=A8=E5=8D=95=E6=95=B0=E6=8D=AE=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E5=8A=A8=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OfflinePlusGetPatterns_Async.cs:在保留原 OfflinePlusGetPatterns_Async 类名和命令注册名的前提下,为上传离线表单命令新增 ExportRecords 动作,设计器显示为“导出离线表单数据”。 OfflinePlusGetPatterns_Async.cs:新增 ProjectId 输入和 ExportResult 输出,只在 ExportRecords 动作下显示;ProjectsResult 继续只在 GetPatterns 动作下显示,避免两个 action 的参数混在一起误用。 Resources/AndroidPDACommand.js:在原 OfflinePlusGetPatterns_Async_Command 内直接增加 ExportRecords 分支,使用 HAC_GenerateCallbackTicket 生成回调并调用 window.offlinePlus.offlinePlusExportRecordsAsync(projectId, ticket)。 Resources/AndroidPDACommand.js:导出回调返回后写入 ExportResult 变量,并继续通过子命令 initParams 传递 ExportResult、IsSuccess 和 Error,保持与现有异步命令执行模式一致。 --- OfflinePlusGetPatterns_Async.cs | 23 ++++++++++++++++++++++- Resources/AndroidPDACommand.js | 25 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/OfflinePlusGetPatterns_Async.cs b/OfflinePlusGetPatterns_Async.cs index cc1b509..0dd3b6f 100644 --- a/OfflinePlusGetPatterns_Async.cs +++ b/OfflinePlusGetPatterns_Async.cs @@ -9,16 +9,37 @@ namespace AndroidPDACommand public class OfflinePlusGetPatterns_Async : BaseAsyncCommand { [DisplayName("动作")] - [ComboProperty(ValueList = "GetPatterns", DisplayList = "获取设备上的离线表单项目编号列表")] + [ComboProperty(ValueList = "GetPatterns|ExportRecords", DisplayList = "获取设备上的离线表单项目编号列表|导出离线表单数据")] public string Action { get; set; } = "GetPatterns"; + [FormulaProperty] + [DisplayName("项目编号")] + public object ProjectId { get; set; } + [DisplayName("将离线表单项目编号列表返回到变量")] [ResultToProperty] public string ProjectsResult { get; set; } + [DisplayName("将离线表单数据返回到变量")] + [ResultToProperty] + public string ExportResult { get; set; } + public override string ToString() { return "上传离线表单"; } + + public override bool GetDesignerPropertyVisible(string propertyName, CommandScope commandScope) + { + if (propertyName == nameof(ProjectId) || propertyName == nameof(ExportResult)) + { + return Action == "ExportRecords"; + } + if (propertyName == nameof(ProjectsResult)) + { + return Action == "GetPatterns"; + } + return base.GetDesignerPropertyVisible(propertyName, commandScope); + } } } diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 40ff061..eecd4d7 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2933,6 +2933,31 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { var params = this.CommandParam; var action = params.Action || "GetPatterns"; + if (action === "ExportRecords") { + var projectId = this.evaluateFormula(params.ProjectId); + let me = this; + let ticket = HAC_GenerateCallbackTicket(me, function (payload, payload2) { + HAC_ReturnToParam(params.ExportResult, payload); + me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { + runTimePageName: me.CommandExecutingInfo.runTimePageName, + commandID: new Date().valueOf().toString(), + initParams: { + "ExportResult": payload, + "IsSuccess": true, + "Error": "" + }, + locationString: "执行window.offlinePlus.offlinePlusExportRecordsAsync" + }); + }, HAC_GetOfflinePlusDebugInfo("上传离线表单", "生成导出数据回调ticket")); + + if (window.offlinePlus && window.offlinePlus.offlinePlusExportRecordsAsync) { + window.offlinePlus.offlinePlusExportRecordsAsync(projectId, ticket); + } else { + HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("上传离线表单", "调用offlinePlusExportRecordsAsync")); + } + return; + } + if (action !== "GetPatterns") { alert("不支持的离线表单上传动作:" + action); return; -- Gitee From 4e662aba3c50fca81764bac1e71254d66dae50cf Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Sat, 13 Jun 2026 17:48:29 +0800 Subject: [PATCH 18/51] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E7=A6=BB=E7=BA=BF?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=E6=94=AF=E6=8C=81=E6=A0=87=E8=AE=B0=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=B7=B2=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OfflinePlusGetPatterns_Async.cs:在上传离线表单命令中新增“标记离线记录已导出”动作,增加项目编号、记录编号和操作结果的设计时可见性控制;记录编号参数改为 RecordIds,并补充“多个记录编号用英文逗号分隔”的设计时备注,明确运行时输入格式。Resources/AndroidPDACommand.js:为 MarkRecordExported 动作生成 HAC 异步回调 ticket,调用 window.offlinePlus.offlinePlusMarkRecordExportedAsync(projectId, recordIds, ticket),并将安卓端返回的操作结果写回 OperationResult;JS 只传递逗号分隔字符串,不在前端拆分,保持拆分和逐条标记逻辑在安卓端完成。 --- OfflinePlusGetPatterns_Async.cs | 21 +++++++++++++++++++-- Resources/AndroidPDACommand.js | 26 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/OfflinePlusGetPatterns_Async.cs b/OfflinePlusGetPatterns_Async.cs index 0dd3b6f..4584c67 100644 --- a/OfflinePlusGetPatterns_Async.cs +++ b/OfflinePlusGetPatterns_Async.cs @@ -9,13 +9,18 @@ namespace AndroidPDACommand public class OfflinePlusGetPatterns_Async : BaseAsyncCommand { [DisplayName("动作")] - [ComboProperty(ValueList = "GetPatterns|ExportRecords", DisplayList = "获取设备上的离线表单项目编号列表|导出离线表单数据")] + [ComboProperty(ValueList = "GetPatterns|ExportRecords|MarkRecordExported", DisplayList = "获取设备上的离线表单项目编号列表|导出离线表单数据|标记离线记录已导出")] public string Action { get; set; } = "GetPatterns"; [FormulaProperty] [DisplayName("项目编号")] public object ProjectId { get; set; } + [FormulaProperty] + [DisplayName("记录编号")] + [Description("多个记录编号用英文逗号分隔。")] + public object RecordIds { get; set; } + [DisplayName("将离线表单项目编号列表返回到变量")] [ResultToProperty] public string ProjectsResult { get; set; } @@ -24,6 +29,10 @@ namespace AndroidPDACommand [ResultToProperty] public string ExportResult { get; set; } + [DisplayName("将操作结果返回到变量")] + [ResultToProperty] + public string OperationResult { get; set; } + public override string ToString() { return "上传离线表单"; @@ -31,7 +40,15 @@ namespace AndroidPDACommand public override bool GetDesignerPropertyVisible(string propertyName, CommandScope commandScope) { - if (propertyName == nameof(ProjectId) || propertyName == nameof(ExportResult)) + if (propertyName == nameof(ProjectId)) + { + return Action == "ExportRecords" || Action == "MarkRecordExported"; + } + if (propertyName == nameof(RecordIds) || propertyName == nameof(OperationResult)) + { + return Action == "MarkRecordExported"; + } + if (propertyName == nameof(ExportResult)) { return Action == "ExportRecords"; } diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index eecd4d7..cadb2cd 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2958,6 +2958,32 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { return; } + if (action === "MarkRecordExported") { + var projectId = this.evaluateFormula(params.ProjectId); + var recordIds = this.evaluateFormula(params.RecordIds); + let me = this; + let ticket = HAC_GenerateCallbackTicket(me, function (payload, payload2) { + HAC_ReturnToParam(params.OperationResult, payload); + me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { + runTimePageName: me.CommandExecutingInfo.runTimePageName, + commandID: new Date().valueOf().toString(), + initParams: { + "OperationResult": payload, + "IsSuccess": true, + "Error": "" + }, + locationString: "执行window.offlinePlus.offlinePlusMarkRecordExportedAsync" + }); + }, HAC_GetOfflinePlusDebugInfo("上传离线表单", "生成标记导出回调ticket")); + + if (window.offlinePlus && window.offlinePlus.offlinePlusMarkRecordExportedAsync) { + window.offlinePlus.offlinePlusMarkRecordExportedAsync(projectId, recordIds, ticket); + } else { + HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("上传离线表单", "调用offlinePlusMarkRecordExportedAsync")); + } + return; + } + if (action !== "GetPatterns") { alert("不支持的离线表单上传动作:" + action); return; -- Gitee From a926e1e0f366ea04e76f72f4d4ca3b48af031983 Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Sat, 13 Jun 2026 18:06:01 +0800 Subject: [PATCH 19/51] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E7=A6=BB=E7=BA=BF?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=E6=94=AF=E6=8C=81=E5=88=A0=E9=99=A4=E5=B7=B2?= =?UTF-8?q?=E8=AF=BB=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OfflinePlusGetPatterns_Async.cs:在上传离线表单命令中新增“删除已读离线记录”动作,复用项目编号、记录编号和操作结果参数;记录编号备注说明多个值用英文逗号分隔,并明确不填时删除项目下所有已读记录,方便设计时理解参数语义。Resources/AndroidPDACommand.js:新增 DeleteReadRecords action,使用 HAC_GenerateCallbackTicket 调用 window.offlinePlus.offlinePlusDeleteReadRecordsAsync(projectId, recordIds, ticket),并把安卓端返回结果写回 OperationResult;JS 仅传递 recordIds 字符串,不在前端拆分,保持拆分和删除逻辑在安卓端完成。 --- OfflinePlusGetPatterns_Async.cs | 8 ++++---- Resources/AndroidPDACommand.js | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/OfflinePlusGetPatterns_Async.cs b/OfflinePlusGetPatterns_Async.cs index 4584c67..4652452 100644 --- a/OfflinePlusGetPatterns_Async.cs +++ b/OfflinePlusGetPatterns_Async.cs @@ -9,7 +9,7 @@ namespace AndroidPDACommand public class OfflinePlusGetPatterns_Async : BaseAsyncCommand { [DisplayName("动作")] - [ComboProperty(ValueList = "GetPatterns|ExportRecords|MarkRecordExported", DisplayList = "获取设备上的离线表单项目编号列表|导出离线表单数据|标记离线记录已导出")] + [ComboProperty(ValueList = "GetPatterns|ExportRecords|MarkRecordExported|DeleteReadRecords", DisplayList = "获取设备上的离线表单项目编号列表|导出离线表单数据|标记离线记录已导出|删除已读离线记录")] public string Action { get; set; } = "GetPatterns"; [FormulaProperty] @@ -18,7 +18,7 @@ namespace AndroidPDACommand [FormulaProperty] [DisplayName("记录编号")] - [Description("多个记录编号用英文逗号分隔。")] + [Description("多个记录编号用英文逗号分隔。删除已读离线记录时不填,则删除项目下所有已读记录。")] public object RecordIds { get; set; } [DisplayName("将离线表单项目编号列表返回到变量")] @@ -42,11 +42,11 @@ namespace AndroidPDACommand { if (propertyName == nameof(ProjectId)) { - return Action == "ExportRecords" || Action == "MarkRecordExported"; + return Action == "ExportRecords" || Action == "MarkRecordExported" || Action == "DeleteReadRecords"; } if (propertyName == nameof(RecordIds) || propertyName == nameof(OperationResult)) { - return Action == "MarkRecordExported"; + return Action == "MarkRecordExported" || Action == "DeleteReadRecords"; } if (propertyName == nameof(ExportResult)) { diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index cadb2cd..fbec884 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2984,6 +2984,32 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { return; } + if (action === "DeleteReadRecords") { + var projectId = this.evaluateFormula(params.ProjectId); + var recordIds = this.evaluateFormula(params.RecordIds); + let me = this; + let ticket = HAC_GenerateCallbackTicket(me, function (payload, payload2) { + HAC_ReturnToParam(params.OperationResult, payload); + me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { + runTimePageName: me.CommandExecutingInfo.runTimePageName, + commandID: new Date().valueOf().toString(), + initParams: { + "OperationResult": payload, + "IsSuccess": true, + "Error": "" + }, + locationString: "执行window.offlinePlus.offlinePlusDeleteReadRecordsAsync" + }); + }, HAC_GetOfflinePlusDebugInfo("上传离线表单", "生成删除已读记录回调ticket")); + + if (window.offlinePlus && window.offlinePlus.offlinePlusDeleteReadRecordsAsync) { + window.offlinePlus.offlinePlusDeleteReadRecordsAsync(projectId, recordIds, ticket); + } else { + HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("上传离线表单", "调用offlinePlusDeleteReadRecordsAsync")); + } + return; + } + if (action !== "GetPatterns") { alert("不支持的离线表单上传动作:" + action); return; -- Gitee From a8b7381a62a63f971aa5a022b083f7bed593c4d2 Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Sat, 13 Jun 2026 18:12:58 +0800 Subject: [PATCH 20/51] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E7=A6=BB=E7=BA=BF?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=E6=94=AF=E6=8C=81=E5=88=A0=E9=99=A4=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OfflinePlusGetPatterns_Async.cs:在上传离线表单命令中新增“删除离线表单项目”动作,并将设计时可见参数限制为项目编号和操作结果,避免展示无关的记录编号参数。Resources/AndroidPDACommand.js:新增 DeleteProject action,使用 HAC_GenerateCallbackTicket 调用 window.offlinePlus.offlinePlusDeleteProjectAsync(projectId, ticket),并将安卓端返回结果写回 OperationResult,保持删除项目流程与其它上传离线表单动作一致。 --- OfflinePlusGetPatterns_Async.cs | 10 +++++++--- Resources/AndroidPDACommand.js | 25 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/OfflinePlusGetPatterns_Async.cs b/OfflinePlusGetPatterns_Async.cs index 4652452..1a597ef 100644 --- a/OfflinePlusGetPatterns_Async.cs +++ b/OfflinePlusGetPatterns_Async.cs @@ -9,7 +9,7 @@ namespace AndroidPDACommand public class OfflinePlusGetPatterns_Async : BaseAsyncCommand { [DisplayName("动作")] - [ComboProperty(ValueList = "GetPatterns|ExportRecords|MarkRecordExported|DeleteReadRecords", DisplayList = "获取设备上的离线表单项目编号列表|导出离线表单数据|标记离线记录已导出|删除已读离线记录")] + [ComboProperty(ValueList = "GetPatterns|ExportRecords|MarkRecordExported|DeleteReadRecords|DeleteProject", DisplayList = "获取设备上的离线表单项目编号列表|导出离线表单数据|标记离线记录已导出|删除已读离线记录|删除离线表单项目")] public string Action { get; set; } = "GetPatterns"; [FormulaProperty] @@ -42,12 +42,16 @@ namespace AndroidPDACommand { if (propertyName == nameof(ProjectId)) { - return Action == "ExportRecords" || Action == "MarkRecordExported" || Action == "DeleteReadRecords"; + return Action == "ExportRecords" || Action == "MarkRecordExported" || Action == "DeleteReadRecords" || Action == "DeleteProject"; } - if (propertyName == nameof(RecordIds) || propertyName == nameof(OperationResult)) + if (propertyName == nameof(RecordIds)) { return Action == "MarkRecordExported" || Action == "DeleteReadRecords"; } + if (propertyName == nameof(OperationResult)) + { + return Action == "MarkRecordExported" || Action == "DeleteReadRecords" || Action == "DeleteProject"; + } if (propertyName == nameof(ExportResult)) { return Action == "ExportRecords"; diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index fbec884..6d0d91b 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -3010,6 +3010,31 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { return; } + if (action === "DeleteProject") { + var projectId = this.evaluateFormula(params.ProjectId); + let me = this; + let ticket = HAC_GenerateCallbackTicket(me, function (payload, payload2) { + HAC_ReturnToParam(params.OperationResult, payload); + me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { + runTimePageName: me.CommandExecutingInfo.runTimePageName, + commandID: new Date().valueOf().toString(), + initParams: { + "OperationResult": payload, + "IsSuccess": true, + "Error": "" + }, + locationString: "执行window.offlinePlus.offlinePlusDeleteProjectAsync" + }); + }, HAC_GetOfflinePlusDebugInfo("上传离线表单", "生成删除离线表单项目回调ticket")); + + if (window.offlinePlus && window.offlinePlus.offlinePlusDeleteProjectAsync) { + window.offlinePlus.offlinePlusDeleteProjectAsync(projectId, ticket); + } else { + HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("上传离线表单", "调用offlinePlusDeleteProjectAsync")); + } + return; + } + if (action !== "GetPatterns") { alert("不支持的离线表单上传动作:" + action); return; -- Gitee From 70cdf7ba70f00914ea0edc78cf271c8dbf29389b Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Mon, 15 Jun 2026 09:53:51 +0800 Subject: [PATCH 21/51] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E7=A6=BB=E7=BA=BF=E8=A1=A8=E5=8D=95=E8=AE=B0=E5=BD=95=E7=BC=96?= =?UTF-8?q?=E5=8F=B7=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OfflinePlusGetPatterns_Async.cs:修正 RecordIds 参数的设计时备注,明确该参数在“标记离线记录已导出”动作下必填,在“删除已读离线记录”动作下可不填并表示删除项目下所有已读记录,避免设计时误解参数是否可选。 --- OfflinePlusGetPatterns_Async.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OfflinePlusGetPatterns_Async.cs b/OfflinePlusGetPatterns_Async.cs index 1a597ef..01dbc68 100644 --- a/OfflinePlusGetPatterns_Async.cs +++ b/OfflinePlusGetPatterns_Async.cs @@ -18,7 +18,7 @@ namespace AndroidPDACommand [FormulaProperty] [DisplayName("记录编号")] - [Description("多个记录编号用英文逗号分隔。删除已读离线记录时不填,则删除项目下所有已读记录。")] + [Description("多个记录编号用英文逗号分隔。标记离线记录已导出时必填;删除已读离线记录时不填,则删除项目下所有已读记录。")] public object RecordIds { get; set; } [DisplayName("将离线表单项目编号列表返回到变量")] -- Gitee From 86b41cb9f28130db7ee2cae0302cf81112131843 Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Mon, 15 Jun 2026 12:04:28 +0800 Subject: [PATCH 22/51] =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E7=A6=BB=E7=BA=BF=E8=A1=A8=E5=8D=95=E9=99=84=E4=BB=B6=E6=95=B0?= =?UTF-8?q?=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OfflinePlusGetPatterns_Async.cs:为上传离线表单命令增加隐藏的 FileUploadLimit 服务端属性,让运行时 JS 可以通过 ServerPropertiesId.FileUploadLimit 拿到文件上传限制 ID,并复用本体临时文件上传能力。 Resources/AndroidPDACommand.js:导出离线表单数据时解析 Android 返回的 attachments,逐个从 HAC 读取离线本地文件并上传到 Forguncy 临时目录,再把 records 中对应图片/附件字段替换为竖线分隔的临时文件名字符串;增加上传进度遮罩,显示文件数量、MB 进度和上传速度。空图片/附件占位值已改由 Android 导出阶段处理,因此删除前端 "[]"/"{}" 兜底归一化代码,避免两端职责重复。 --- OfflinePlusGetPatterns_Async.cs | 36 +++++ Resources/AndroidPDACommand.js | 236 ++++++++++++++++++++++++++++++-- 2 files changed, 260 insertions(+), 12 deletions(-) diff --git a/OfflinePlusGetPatterns_Async.cs b/OfflinePlusGetPatterns_Async.cs index 01dbc68..6b82b0c 100644 --- a/OfflinePlusGetPatterns_Async.cs +++ b/OfflinePlusGetPatterns_Async.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System; using GrapeCity.Forguncy.Commands; using GrapeCity.Forguncy.Plugin; @@ -29,6 +30,12 @@ namespace AndroidPDACommand [ResultToProperty] public string ExportResult { get; set; } + [ServerProperty] + [Browsable(false)] + [PageMetadataJsonIgnore] + [SaveJsonIgnore] + public UploadLimit FileUploadLimit { get; set; } = new UploadLimit(); + [DisplayName("将操作结果返回到变量")] [ResultToProperty] public string OperationResult { get; set; } @@ -63,4 +70,33 @@ namespace AndroidPDACommand return base.GetDesignerPropertyVisible(propertyName, commandScope); } } + + public class UploadLimit : ICloneable, IUploadLimit, ISupportDeclareObjectEmpty + { + public UploadLimit() + { + ExtensionFilter = ""; + SizeLimit = 0; + MaxUploadFileCount = 0; + } + + [DefaultValue("")] + public string ExtensionFilter { get; set; } + + [DefaultValue(0d)] + public double SizeLimit { get; set; } + + [DefaultValue(0)] + public int MaxUploadFileCount { get; set; } + + public object Clone() + { + return MemberwiseClone(); + } + + public bool IsObjectEmpty() + { + return SizeLimit == 0 && MaxUploadFileCount == 0 && ExtensionFilter == ""; + } + } } diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 6d0d91b..c38aa82 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -74,6 +74,197 @@ var HAC_ReturnToParam = function (OutParamaterName, data) { } }; +var HAC_DataUrlToFile = function (dataUrl, fileName) { + var parts = (dataUrl || "").split(","); + if (parts.length < 2) { + return null; + } + var header = parts[0]; + var mimeMatch = header.match(/data:([^;]+);base64/i); + var mimeType = mimeMatch ? mimeMatch[1] : "application/octet-stream"; + var binary = atob(parts.slice(1).join(",")); + var bytes = new Uint8Array(binary.length); + for (var i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return new File([bytes], fileName || "offlineAttachment", { type: mimeType }); +}; + +var HAC_FormatBytesAsMb = function (bytes) { + return (bytes / 1024 / 1024).toFixed(2) + " MB"; +}; + +var HAC_OfflineUploadProgress = { + element: null, + fill: null, + title: null, + detail: null, + speed: null, + ensure: function () { + if (this.element) { + return; + } + var root = document.createElement("div"); + root.style.cssText = "position:fixed;left:0;top:0;right:0;bottom:0;z-index:2147483647;background:rgba(0,0,0,.28);display:flex;align-items:center;justify-content:center;font-family:Arial,'Microsoft YaHei',sans-serif;"; + root.innerHTML = "
" + + "
正在上传离线附件
" + + "
" + + "
" + + "
" + + "
"; + document.body.appendChild(root); + this.element = root; + this.fill = root.querySelector("[data-role='fill']"); + this.title = root.querySelector("[data-role='title']"); + this.detail = root.querySelector("[data-role='detail']"); + this.speed = root.querySelector("[data-role='speed']"); + }, + update: function (options) { + this.ensure(); + var percent = Math.max(0, Math.min(100, options.percent || 0)); + this.fill.style.width = percent.toFixed(1) + "%"; + this.title.textContent = options.title || "正在上传离线附件"; + this.detail.textContent = options.detail || ""; + this.speed.textContent = options.speed || ""; + }, + close: function () { + if (this.element && this.element.parentNode) { + this.element.parentNode.removeChild(this.element); + } + this.element = null; + this.fill = null; + this.title = null; + this.detail = null; + this.speed = null; + } +}; + +var HAC_UploadFileToTemp = function (file, uploadLimitId, onProgress) { + return new Promise(function (resolve, reject) { + var formData = new FormData(); + formData.append("file", file); + formData.append("uploadLimitId", uploadLimitId); + formData.append("uploadFolder", ""); + + var startTime = new Date().valueOf(); + var xhr = new XMLHttpRequest(); + xhr.open("POST", Forguncy.Common.uploadUrl, true); + xhr.setRequestHeader("Accept", "application/json"); + xhr.upload.onprogress = function (event) { + if (onProgress && event.lengthComputable) { + var elapsedSeconds = Math.max((new Date().valueOf() - startTime) / 1000, 0.1); + onProgress(event.loaded, event.total, event.loaded / elapsedSeconds); + } + }; + xhr.onload = function () { + if (xhr.status >= 200 && xhr.status < 300) { + resolve(xhr.responseText); + } else { + reject(xhr.responseText || xhr.statusText || "上传离线附件失败。"); + } + }; + xhr.onerror = function () { + reject("上传离线附件失败。"); + }; + xhr.send(formData); + }); +}; + +var HAC_ParseJsonValue = function (value, fallback) { + if (value === undefined || value === null || value === "") { + return fallback; + } + try { + return JSON.parse(value); + } catch (e) { + return fallback; + } +}; + +var HAC_FindOfflineRecord = function (records, recordId) { + for (var i = 0; i < records.length; i++) { + if (records[i] && records[i].recordId === recordId) { + return records[i]; + } + } + return null; +}; + +var HAC_ApplyUploadedAttachmentValue = function (record, attachment, attachmentValue, uploadedValues) { + if (!record || !record.values || !attachment || !attachment.path || attachment.path.length < 3) { + return; + } + + var fieldId = attachment.path[1]; + var key = attachment.path[0] + "|" + fieldId; + if (!uploadedValues[key]) { + uploadedValues[key] = []; + } + uploadedValues[key].push(attachmentValue); + record.values[fieldId] = uploadedValues[key].join("|"); +}; + +var HAC_ProcessOfflineExportAttachments = async function (projectId, exportData, uploadLimitId) { + if (!exportData) { + return exportData; + } + var records = exportData.records || []; + if (!exportData.attachments || exportData.attachments.length === 0) { + delete exportData.attachments; + return exportData; + } + if (!uploadLimitId) { + throw "请设置文件上传限制ID。"; + } + if (!window.offlinePlus || !window.offlinePlus.offlinePlusLoadAttachment) { + HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("上传离线表单", "调用offlinePlusLoadAttachment")); + throw "当前APP不支持读取离线附件。"; + } + + var uploadedValues = {}; + HAC_OfflineUploadProgress.update({ + percent: 0, + detail: "准备上传 " + exportData.attachments.length + " 个离线附件", + speed: "" + }); + for (var i = 0; i < exportData.attachments.length; i++) { + var attachment = exportData.attachments[i]; + if (!attachment || !attachment.localName || !attachment.path || attachment.path.length < 3) { + continue; + } + + var fileIndexText = (i + 1) + " / " + exportData.attachments.length; + HAC_OfflineUploadProgress.update({ + percent: exportData.attachments.length === 0 ? 0 : i / exportData.attachments.length * 100, + detail: "正在读取离线文件:" + fileIndexText + " " + attachment.localName, + speed: "" + }); + var dataUrl = window.offlinePlus.offlinePlusLoadAttachment(projectId, attachment.localName); + var fileName = attachment.type === "image" ? attachment.localName : attachment.path[2]; + var file = HAC_DataUrlToFile(dataUrl, fileName); + if (!file) { + throw "读取离线附件失败:" + attachment.localName; + } + + var attachmentValue = await HAC_UploadFileToTemp(file, uploadLimitId, function (loaded, total, bytesPerSecond) { + HAC_OfflineUploadProgress.update({ + percent: (i + (total > 0 ? loaded / total : 0)) / exportData.attachments.length * 100, + detail: "正在上传:" + fileIndexText + " " + file.name + "," + HAC_FormatBytesAsMb(loaded) + " / " + HAC_FormatBytesAsMb(total), + speed: "速度:" + HAC_FormatBytesAsMb(bytesPerSecond) + "/s" + }); + }); + var record = HAC_FindOfflineRecord(records, attachment.path[0]); + HAC_ApplyUploadedAttachmentValue(record, attachment, attachmentValue, uploadedValues); + } + HAC_OfflineUploadProgress.update({ + percent: 100, + detail: "正在生成导出数据", + speed: "" + }); + delete exportData.attachments; + return exportData; +}; + var HAC_DothanPrinterOp = function (callback) { if (window.dothanPrinter) { var status = window.dothanPrinter.getStatus(); @@ -2935,19 +3126,40 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { if (action === "ExportRecords") { var projectId = this.evaluateFormula(params.ProjectId); + var fileUploadLimit = (params.ServerPropertiesId && params.ServerPropertiesId.FileUploadLimit) || ""; let me = this; - let ticket = HAC_GenerateCallbackTicket(me, function (payload, payload2) { - HAC_ReturnToParam(params.ExportResult, payload); - me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { - runTimePageName: me.CommandExecutingInfo.runTimePageName, - commandID: new Date().valueOf().toString(), - initParams: { - "ExportResult": payload, - "IsSuccess": true, - "Error": "" - }, - locationString: "执行window.offlinePlus.offlinePlusExportRecordsAsync" - }); + let ticket = HAC_GenerateCallbackTicket(me, async function (payload, payload2) { + try { + var exportData = HAC_ParseJsonValue(payload, null); + if (exportData) { + exportData = await HAC_ProcessOfflineExportAttachments(projectId, exportData, fileUploadLimit); + payload = JSON.stringify(exportData); + } + HAC_ReturnToParam(params.ExportResult, payload); + me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { + runTimePageName: me.CommandExecutingInfo.runTimePageName, + commandID: new Date().valueOf().toString(), + initParams: { + "ExportResult": payload, + "IsSuccess": true, + "Error": "" + }, + locationString: "执行window.offlinePlus.offlinePlusExportRecordsAsync" + }); + } catch (e) { + me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { + runTimePageName: me.CommandExecutingInfo.runTimePageName, + commandID: new Date().valueOf().toString(), + initParams: { + "ExportResult": payload, + "IsSuccess": false, + "Error": e ? e.toString() : "导出离线附件失败。" + }, + locationString: "处理离线附件上传时出错" + }); + } finally { + HAC_OfflineUploadProgress.close(); + } }, HAC_GetOfflinePlusDebugInfo("上传离线表单", "生成导出数据回调ticket")); if (window.offlinePlus && window.offlinePlus.offlinePlusExportRecordsAsync) { -- Gitee From d6e115e24bde15ff7567a0d67e3eafc408ab43ec Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Mon, 15 Jun 2026 14:55:45 +0800 Subject: [PATCH 23/51] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=EF=BC=8C=E5=A2=9E=E5=8A=A0=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=E7=9A=84=E5=8F=AF=E8=AF=BB?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Resources/AndroidPDACommand.js | 71 +++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index c38aa82..c58bb27 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -73,7 +73,12 @@ var HAC_ReturnToParam = function (OutParamaterName, data) { console.log("OutParamaterName was not set, the value is: " + JSON.stringify(data)); } }; - +/** + * 将DataURL转换为File对象 + * @param {*} dataUrl + * @param {*} fileName + * @returns File对象,或转换失败时返回null + */ var HAC_DataUrlToFile = function (dataUrl, fileName) { var parts = (dataUrl || "").split(","); if (parts.length < 2) { @@ -90,10 +95,15 @@ var HAC_DataUrlToFile = function (dataUrl, fileName) { return new File([bytes], fileName || "offlineAttachment", { type: mimeType }); }; +/** + * 将字节数格式化为MB字符串 + */ var HAC_FormatBytesAsMb = function (bytes) { return (bytes / 1024 / 1024).toFixed(2) + " MB"; }; - +/** + * 离线附件上传进度显示组件 + */ var HAC_OfflineUploadProgress = { element: null, fill: null, @@ -138,8 +148,14 @@ var HAC_OfflineUploadProgress = { this.speed = null; } }; - -var HAC_UploadFileToTemp = function (file, uploadLimitId, onProgress) { +/** + * 将文件上传到服务器临时目录,并返回服务器上的文件标识 + * @param {*} file + * @param {*} uploadLimitId + * @param {*} onProgress + * @returns Promise,成功时返回服务器上的文件标识,失败时返回错误信息 + */ +function HAC_UploadFileToTemp(file, uploadLimitId, onProgress) { return new Promise(function (resolve, reject) { var formData = new FormData(); formData.append("file", file); @@ -168,29 +184,11 @@ var HAC_UploadFileToTemp = function (file, uploadLimitId, onProgress) { }; xhr.send(formData); }); -}; - -var HAC_ParseJsonValue = function (value, fallback) { - if (value === undefined || value === null || value === "") { - return fallback; - } - try { - return JSON.parse(value); - } catch (e) { - return fallback; - } -}; - -var HAC_FindOfflineRecord = function (records, recordId) { - for (var i = 0; i < records.length; i++) { - if (records[i] && records[i].recordId === recordId) { - return records[i]; - } - } - return null; -}; - -var HAC_ApplyUploadedAttachmentValue = function (record, attachment, attachmentValue, uploadedValues) { +} +/** + * 将上传后的附件值应用到记录中,并更新记录的字段值 + */ +function HAC_ApplyUploadedAttachmentValue(record, attachment, attachmentValue, uploadedValues) { if (!record || !record.values || !attachment || !attachment.path || attachment.path.length < 3) { return; } @@ -202,9 +200,16 @@ var HAC_ApplyUploadedAttachmentValue = function (record, attachment, attachmentV } uploadedValues[key].push(attachmentValue); record.values[fieldId] = uploadedValues[key].join("|"); -}; +} -var HAC_ProcessOfflineExportAttachments = async function (projectId, exportData, uploadLimitId) { +/** + * 处理离线导出附件 + * @param {*} projectId + * @param {*} exportData + * @param {*} uploadLimitId + * @returns Promise,成功时返回处理后的导出数据,失败时返回错误信息 + */ +async function HAC_ProcessOfflineExportAttachments(projectId, exportData, uploadLimitId) { if (!exportData) { return exportData; } @@ -253,7 +258,9 @@ var HAC_ProcessOfflineExportAttachments = async function (projectId, exportData, speed: "速度:" + HAC_FormatBytesAsMb(bytesPerSecond) + "/s" }); }); - var record = HAC_FindOfflineRecord(records, attachment.path[0]); + var record = records.find(function (record) { + return record && record.recordId === attachment.path[0]; + }); HAC_ApplyUploadedAttachmentValue(record, attachment, attachmentValue, uploadedValues); } HAC_OfflineUploadProgress.update({ @@ -263,7 +270,7 @@ var HAC_ProcessOfflineExportAttachments = async function (projectId, exportData, }); delete exportData.attachments; return exportData; -}; +} var HAC_DothanPrinterOp = function (callback) { if (window.dothanPrinter) { @@ -3130,7 +3137,7 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { let me = this; let ticket = HAC_GenerateCallbackTicket(me, async function (payload, payload2) { try { - var exportData = HAC_ParseJsonValue(payload, null); + var exportData = JSON.parse(payload); if (exportData) { exportData = await HAC_ProcessOfflineExportAttachments(projectId, exportData, fileUploadLimit); payload = JSON.stringify(exportData); -- Gitee From f7f0cd1ea51ad34723ceba6eae885e4aecf753d9 Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Tue, 16 Jun 2026 10:38:00 +0800 Subject: [PATCH 24/51] =?UTF-8?q?=EF=BB=BFfix(offline):=20=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E7=A6=BB=E7=BA=BF=E9=99=84=E4=BB=B6=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 配合 Android 侧图片和文件附件值统一为 AttachmentFormItemValue 列表,调整插件运行时导出处理逻辑。 按文件说明: - Resources/AndroidPDACommand.js:导出附件上传时改用 recordId/fieldId/fileName/originalName 统一字段;移除图片和文件文件名策略分支;上传遮罩拦截 touchmove 并锁定 body/html 滚动,避免移动端拖动穿透到底层页面;添加离线表单项目时将各控件类型专属配置统一写入 field.options。 - OfflinePlusGetPatterns_Async.cs:补充导出结果说明,说明图片和文件字段会按统一附件模型上传并回写字段值。 验证: - node --check Resources/AndroidPDACommand.js - git diff --cached --check --- OfflinePlusGetPatterns_Async.cs | 1 + Resources/AndroidPDACommand.js | 79 ++++++++++++++++++++++++++------- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/OfflinePlusGetPatterns_Async.cs b/OfflinePlusGetPatterns_Async.cs index 6b82b0c..3061322 100644 --- a/OfflinePlusGetPatterns_Async.cs +++ b/OfflinePlusGetPatterns_Async.cs @@ -27,6 +27,7 @@ namespace AndroidPDACommand public string ProjectsResult { get; set; } [DisplayName("将离线表单数据返回到变量")] + [Description("导出离线记录数据。图片和文件字段会先按统一附件模型读取本地附件并上传,再把上传结果写回对应字段值。")] [ResultToProperty] public string ExportResult { get; set; } diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index c58bb27..6022923 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -110,18 +110,29 @@ var HAC_OfflineUploadProgress = { title: null, detail: null, speed: null, + preventScrollHandler: null, + bodyOverflow: null, + documentOverflow: null, ensure: function () { if (this.element) { return; } var root = document.createElement("div"); - root.style.cssText = "position:fixed;left:0;top:0;right:0;bottom:0;z-index:2147483647;background:rgba(0,0,0,.28);display:flex;align-items:center;justify-content:center;font-family:Arial,'Microsoft YaHei',sans-serif;"; + root.style.cssText = "position:fixed;left:0;top:0;right:0;bottom:0;z-index:2147483647;background:rgba(0,0,0,.28);display:flex;align-items:center;justify-content:center;font-family:Arial,'Microsoft YaHei',sans-serif;touch-action:none;overscroll-behavior:contain;"; root.innerHTML = "
" + "
正在上传离线附件
" + "
" + "
" + "
" + "
"; + this.bodyOverflow = document.body.style.overflow; + this.documentOverflow = document.documentElement.style.overflow; + document.body.style.overflow = "hidden"; + document.documentElement.style.overflow = "hidden"; + this.preventScrollHandler = function (event) { + event.preventDefault(); + }; + root.addEventListener("touchmove", this.preventScrollHandler, { passive: false }); document.body.appendChild(root); this.element = root; this.fill = root.querySelector("[data-role='fill']"); @@ -138,14 +149,22 @@ var HAC_OfflineUploadProgress = { this.speed.textContent = options.speed || ""; }, close: function () { + if (this.element && this.preventScrollHandler) { + this.element.removeEventListener("touchmove", this.preventScrollHandler); + } if (this.element && this.element.parentNode) { this.element.parentNode.removeChild(this.element); } + document.body.style.overflow = this.bodyOverflow || ""; + document.documentElement.style.overflow = this.documentOverflow || ""; this.element = null; this.fill = null; this.title = null; this.detail = null; this.speed = null; + this.preventScrollHandler = null; + this.bodyOverflow = null; + this.documentOverflow = null; } }; /** @@ -189,12 +208,17 @@ function HAC_UploadFileToTemp(file, uploadLimitId, onProgress) { * 将上传后的附件值应用到记录中,并更新记录的字段值 */ function HAC_ApplyUploadedAttachmentValue(record, attachment, attachmentValue, uploadedValues) { - if (!record || !record.values || !attachment || !attachment.path || attachment.path.length < 3) { + if (!record || !record.values || !attachment) { + return; + } + + var recordId = HAC_GetOfflineAttachmentRecordId(attachment); + var fieldId = HAC_GetOfflineAttachmentFieldId(attachment); + if (!recordId || !fieldId) { return; } - var fieldId = attachment.path[1]; - var key = attachment.path[0] + "|" + fieldId; + var key = recordId + "|" + fieldId; if (!uploadedValues[key]) { uploadedValues[key] = []; } @@ -202,6 +226,18 @@ function HAC_ApplyUploadedAttachmentValue(record, attachment, attachmentValue, u record.values[fieldId] = uploadedValues[key].join("|"); } +function HAC_GetOfflineAttachmentRecordId(attachment) { + return attachment && attachment.recordId || attachment && attachment.path && attachment.path[0] || ""; +} + +function HAC_GetOfflineAttachmentFieldId(attachment) { + return attachment && attachment.fieldId || attachment && attachment.path && attachment.path[1] || ""; +} + +function HAC_GetOfflineAttachmentUploadFileName(attachment) { + return attachment && (attachment.originalName || attachment.fileName || attachment.localName) || "offlineAttachment"; +} + /** * 处理离线导出附件 * @param {*} projectId @@ -234,7 +270,7 @@ async function HAC_ProcessOfflineExportAttachments(projectId, exportData, upload }); for (var i = 0; i < exportData.attachments.length; i++) { var attachment = exportData.attachments[i]; - if (!attachment || !attachment.localName || !attachment.path || attachment.path.length < 3) { + if (!attachment || !attachment.localName || !HAC_GetOfflineAttachmentRecordId(attachment) || !HAC_GetOfflineAttachmentFieldId(attachment)) { continue; } @@ -245,8 +281,7 @@ async function HAC_ProcessOfflineExportAttachments(projectId, exportData, upload speed: "" }); var dataUrl = window.offlinePlus.offlinePlusLoadAttachment(projectId, attachment.localName); - var fileName = attachment.type === "image" ? attachment.localName : attachment.path[2]; - var file = HAC_DataUrlToFile(dataUrl, fileName); + var file = HAC_DataUrlToFile(dataUrl, HAC_GetOfflineAttachmentUploadFileName(attachment)); if (!file) { throw "读取离线附件失败:" + attachment.localName; } @@ -259,7 +294,7 @@ async function HAC_ProcessOfflineExportAttachments(projectId, exportData, upload }); }); var record = records.find(function (record) { - return record && record.recordId === attachment.path[0]; + return record && record.recordId === HAC_GetOfflineAttachmentRecordId(attachment); }); HAC_ApplyUploadedAttachmentValue(record, attachment, attachmentValue, uploadedValues); } @@ -2937,7 +2972,7 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { apply: function (field, node) { var checkOptions = buildCheckOptions(node.CheckOptions); if (checkOptions) { - field.checkOptions = checkOptions; + field.options = checkOptions; } } }, @@ -2945,7 +2980,7 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { apply: function (field, node) { var checkOptions = buildCheckOptions(node.CheckOptions); if (checkOptions) { - field.checkOptions = checkOptions; + field.options = checkOptions; } } }, @@ -2953,33 +2988,41 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { apply: function (field, node) { var selectOptions = buildSelectOptions(node.SelectOptionsList); if (selectOptions.length > 0) { - field.options = selectOptions; + field.options = { + selectOptions: selectOptions + }; } } }, timePicker: { apply: function (field, node) { if (node.IncludeSeconds) { - field.includeSeconds = true; + field.options = { + includeSeconds: true + }; } } }, imageItem: { apply: function (field, node) { + var options = {}; var maxCount = evaluate(node.MaxCount); var compression = buildImageCompression(node.CompressionOptions); var watermark = buildImageWatermark(node.WatermarkOptions); if (maxCount !== undefined && maxCount !== null && maxCount !== "") { - field.maxCount = maxCount; + options.maxCount = maxCount; } if (node.AllowImageUpload) { - field.allowImageUpload = true; + options.allowImageUpload = true; } if (compression) { - field.compression = compression; + options.compression = compression; } if (watermark) { - field.watermark = watermark; + options.watermark = watermark; + } + if (Object.keys(options).length > 0) { + field.options = options; } } }, @@ -2987,7 +3030,9 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { apply: function (field, node) { var config = buildFileItemConfig(node.FileItemConfig); if (config) { - field.fileItemConfig = config; + field.options = { + fileItemConfig: config + }; } } } -- Gitee From fd347305f8f1d6ea84edadc954215928f7687f53 Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Tue, 16 Jun 2026 14:30:38 +0800 Subject: [PATCH 25/51] feat(offline): update MaxCount property to allow unlimited image uploads --- OfflinePlusAddPattern_Async.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OfflinePlusAddPattern_Async.cs b/OfflinePlusAddPattern_Async.cs index 308192d..751a119 100644 --- a/OfflinePlusAddPattern_Async.cs +++ b/OfflinePlusAddPattern_Async.cs @@ -159,7 +159,8 @@ namespace AndroidPDACommand [FormulaProperty] [DisplayName("图片最大数量")] - public object MaxCount { get; set; } = 9; + [Description("0表示不限制图片数量。")] + public object MaxCount { get; set; } = 0; [DisplayName("允许图片上传")] [Description("仅图片列表类型生效。启用后填报时除拍照外还可以从本地选择图片。")] -- Gitee From 3cbf0ebf430dac04e32b7b0adbcdb4fc8c556897 Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Wed, 17 Jun 2026 09:29:34 +0800 Subject: [PATCH 26/51] fix(offline): hide field options for non-field nodes OfflinePlusAddPattern_Async.cs: keep field-type controlled designer properties hidden unless the selected offline form node is a form field. This prevents group and text nodes from showing image, file, time, and other field-specific configuration panels. --- OfflinePlusAddPattern_Async.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OfflinePlusAddPattern_Async.cs b/OfflinePlusAddPattern_Async.cs index 751a119..fd5a6e7 100644 --- a/OfflinePlusAddPattern_Async.cs +++ b/OfflinePlusAddPattern_Async.cs @@ -191,9 +191,9 @@ namespace AndroidPDACommand case nameof(DefaultCollapsed): return isGroup; default: - if (isField && FieldTypeControlledProperties.Contains(propertyName)) + if (FieldTypeControlledProperties.Contains(propertyName)) { - return IsFieldPropertyVisible(ItemType, propertyName); + return isField && IsFieldPropertyVisible(ItemType, propertyName); } return base.GetDesignerPropertyVisible(propertyName); } -- Gitee From d018c787892307f7d17cfeb2bb8eed10e1f4e061 Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Wed, 24 Jun 2026 12:16:07 +0800 Subject: [PATCH 27/51] =?UTF-8?q?=E6=8B=86=E5=88=86=E7=A6=BB=E7=BA=BF?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=E6=9E=84=E5=BB=BA=E4=B8=8E=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AndroidPDACommand.csproj:加入 OfflinePlusBuildPattern.cs 编译项和 Icon_OfflineBuild.png 资源项,确保新增构建命令及其图标能被插件编译和打包。 OfflinePlusAddPattern_Async.cs:收敛为下载离线表单命令,只保留离线表单 JSON、手册附件单元格和员工签名单元格参数,避免下载命令继续承担表单结构配置职责。 OfflinePlusBuildPattern.cs:新增构建离线表单命令文件,承接原下载命令中的项目元信息、步骤、节点和字段配置,并将构建出的结构 JSON 返回到变量,避免在构建阶段写入手册或签名大字段。 Resources/AndroidPDACommand.js:新增构建命令运行时注册,生成纯表单结构 JSON;调整下载命令从 JSON 解析表单结构,在下载阶段生成 ticket,并沿用 Android 端现有三参桥协议传入表单 JSON、手册 URL 和签名 Base64。 Resources/Icon_OfflineBuild.png:新增构建离线表单命令图标,使新命令在设计器命令列表中和下载命令区分。 --- AndroidPDACommand.csproj | 2 + OfflinePlusAddPattern_Async.cs | 311 +----------------------------- OfflinePlusBuildPattern.cs | 332 ++++++++++++++++++++++++++++++++ Resources/AndroidPDACommand.js | 108 ++++++++--- Resources/Icon_OfflineBuild.png | Bin 0 -> 295 bytes 5 files changed, 415 insertions(+), 338 deletions(-) create mode 100644 OfflinePlusBuildPattern.cs create mode 100644 Resources/Icon_OfflineBuild.png diff --git a/AndroidPDACommand.csproj b/AndroidPDACommand.csproj index 13b436e..575c3e8 100644 --- a/AndroidPDACommand.csproj +++ b/AndroidPDACommand.csproj @@ -50,6 +50,7 @@ + @@ -166,6 +167,7 @@ + diff --git a/OfflinePlusAddPattern_Async.cs b/OfflinePlusAddPattern_Async.cs index fd5a6e7..8f7612d 100644 --- a/OfflinePlusAddPattern_Async.cs +++ b/OfflinePlusAddPattern_Async.cs @@ -1,7 +1,5 @@ -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.Linq; using GrapeCity.Forguncy.Commands; using GrapeCity.Forguncy.Plugin; @@ -11,24 +9,11 @@ namespace AndroidPDACommand [Category("活字格安卓容器(HAC)")] public class OfflinePlusAddPattern_Async : BaseAsyncCommand { - [FormulaProperty] - [DisplayName("项目编号")] - [Description("项目的唯一标识,全局唯一")] - public object PatternId { get; set; } - - [DisplayName("名称")] - [FormulaProperty] - public object Title { get; set; } - - [DisplayName("描述")] - [FormulaProperty] - public object Description { get; set; } - [Required] [FormulaProperty] - [DisplayName("版本号")] - [Description("同一项目编号下的表单定义版本,用于区分表单结构变更。")] - public string SchemaVersion { get; set; } = "1.0"; + [DisplayName("离线表单JSON")] + [Description("由“构建离线表单”命令生成的表单JSON。")] + public object PatternJson { get; set; } [FormulaProperty] [DisplayName("手册附件单元格")] @@ -40,299 +25,9 @@ namespace AndroidPDACommand [Description("选择保存员工签名附件的单元格。下载离线表单时会把签名文件随表单一起保存到离线设备。")] public object SignatureAttachmentCell { get; set; } - [DisplayName("步骤")] - [ObjectListProperty(ItemType = typeof(OfflineFormStep))] - public List Steps { get; set; } - public override string ToString() { return "下载离线表单"; } } - - public class OfflineFormStep : ObjectPropertyBase, INamedObject - { - public string Name { get; set; } - - [FormulaProperty] - [DisplayName("步骤编号")] - [Description("该步骤的唯一标识,同一个离线表单项目内必须唯一。")] - public object StepId { get; set; } - - [FormulaProperty] - [DisplayName("标题")] - public object Title { get; set; } - - [TreeProperty(NodeType = typeof(OfflineFormNode), DefaultNodeName = "节点")] - [DisplayName("页面内容")] - public List Items { get; set; } = new List(); - } - - public class OfflineFormNode : ObjectPropertyBase, ITreeNode - { - private static readonly string[] CommonFieldVisibleProperties = - { - nameof(ItemId), - nameof(ItemType), - nameof(Title), - nameof(Hint), - nameof(Required) - }; - - private static readonly HashSet FieldTypeControlledProperties = new HashSet(CommonFieldVisibleProperties) - { - nameof(Value), - nameof(CheckOptions), - nameof(SelectOptionsList), - nameof(IncludeSeconds), - nameof(MaxCount), - nameof(AllowImageUpload), - nameof(CompressionOptions), - nameof(WatermarkOptions), - nameof(FileItemConfig) - }; - - private static readonly Dictionary> FieldVisiblePropertiesByType = new Dictionary> - { - { "textItem", CreateFieldVisibleProperties(nameof(Value), nameof(CheckOptions)) }, - { "passwordItem", CreateFieldVisibleProperties(nameof(Value), nameof(CheckOptions)) }, - { "selectItem", CreateFieldVisibleProperties(nameof(Value), nameof(SelectOptionsList)) }, - { "datePicker", CreateFieldVisibleProperties(nameof(Value)) }, - { "timePicker", CreateFieldVisibleProperties(nameof(Value), nameof(IncludeSeconds)) }, - { "imageItem", CreateFieldVisibleProperties(nameof(MaxCount), nameof(AllowImageUpload), nameof(CompressionOptions), nameof(WatermarkOptions)) }, - { "fileItem", CreateFieldVisibleProperties(nameof(FileItemConfig)) } - }; - - [DisplayName("节点名称")] - public string Text { get; set; } - - [Browsable(false)] - public IEnumerable Children { get; set; } = new List(); - - [DisplayName("节点类型")] - [ComboProperty(ValueList = "group|text|field", DisplayList = "分组|普通文本|表单项")] - public string NodeType { get; set; } = "group"; - - [FormulaProperty] - [DisplayName("说明内容")] - public object Content { get; set; } - - [DisplayName("默认折叠")] - public bool DefaultCollapsed { get; set; } - - [FormulaProperty] - [DisplayName("编号")] - [Description("该表单项的唯一标识,全局唯一,无论是同一项目内,还是跨项目,都必须唯一,更建议使用项目编号作为前缀拼接;例如项目为Project01,则该表单项编号为Project01_Name")] - public object ItemId { get; set; } - - [DisplayName("类型")] - [ComboProperty(ValueList = "textItem|passwordItem|selectItem|datePicker|timePicker|imageItem|fileItem", DisplayList = "文本框|密码框|选择框|日期选择|时间选择|图片列表|文件上传")] - public string ItemType { get; set; } = "textItem"; - - [FormulaProperty] - [DisplayName("标题")] - public object Title { get; set; } - - [FormulaProperty] - [DisplayName("水印")] - public object Hint { get; set; } - - [DisplayName("必填")] - public bool Required { get; set; } - - [FormulaProperty] - [DisplayName("默认值")] - public object Value { get; set; } - - [DisplayName("包含秒")] - [Description("仅时间选择类型生效。启用后可选择时分秒;关闭时使用系统原生时分选择器。")] - public bool IncludeSeconds { get; set; } - - - [ObjectProperty(ObjType = typeof(CheckOptions))] - [DisplayName("校验规则")] - public CheckOptions CheckOptions { get; set; } - - [ListProperty] - [DisplayName("选则项")] - public List SelectOptionsList { get; set; } - - [FormulaProperty] - [DisplayName("图片最大数量")] - [Description("0表示不限制图片数量。")] - public object MaxCount { get; set; } = 0; - - [DisplayName("允许图片上传")] - [Description("仅图片列表类型生效。启用后填报时除拍照外还可以从本地选择图片。")] - public bool AllowImageUpload { get; set; } - - [ObjectProperty(ObjType = typeof(ImageCompressionOptions))] - [DisplayName("图片压缩设置")] - public ImageCompressionOptions CompressionOptions { get; set; } = new ImageCompressionOptions(); - - [ObjectProperty(ObjType = typeof(ImageWatermarkOptions))] - [DisplayName("拍照水印配置")] - public ImageWatermarkOptions WatermarkOptions { get; set; } = new ImageWatermarkOptions(); - - [ObjectProperty(ObjType = typeof(FileItemConfig))] - [DisplayName("文件上传配置")] - public FileItemConfig FileItemConfig { get; set; } = new FileItemConfig(); - - public override bool GetDesignerPropertyVisible(string propertyName) - { - var isField = NodeType == "field"; - var isText = NodeType == "text"; - var isGroup = NodeType == "group"; - - switch (propertyName) - { - case nameof(Content): - return isText; - case nameof(DefaultCollapsed): - return isGroup; - default: - if (FieldTypeControlledProperties.Contains(propertyName)) - { - return isField && IsFieldPropertyVisible(ItemType, propertyName); - } - return base.GetDesignerPropertyVisible(propertyName); - } - } - - public override object Clone() - { - return new OfflineFormNode - { - Text = Text, - Children = Children == null ? new List() : Children.ToList(), - NodeType = NodeType, - Content = Content, - DefaultCollapsed = DefaultCollapsed, - ItemId = ItemId, - ItemType = ItemType, - Title = Title, - Hint = Hint, - Required = Required, - Value = Value, - IncludeSeconds = IncludeSeconds, - CheckOptions = CheckOptions, - SelectOptionsList = SelectOptionsList, - MaxCount = MaxCount, - AllowImageUpload = AllowImageUpload, - CompressionOptions = CompressionOptions, - WatermarkOptions = WatermarkOptions, - FileItemConfig = FileItemConfig - }; - } - - private static HashSet CreateFieldVisibleProperties(params string[] propertyNames) - { - var visibleProperties = new HashSet(CommonFieldVisibleProperties); - foreach (var propertyName in propertyNames) - { - visibleProperties.Add(propertyName); - } - return visibleProperties; - } - - private static bool IsFieldPropertyVisible(string itemType, string propertyName) - { - HashSet visibleProperties; - if (!FieldVisiblePropertiesByType.TryGetValue(itemType ?? "textItem", out visibleProperties)) - { - visibleProperties = FieldVisiblePropertiesByType["textItem"]; - } - return visibleProperties.Contains(propertyName); - } - } - - public class CheckOptions : ObjectPropertyBase - { - [FormulaProperty] - [DisplayName("最小长度")] - public object MinLength { get; set; } - - [FormulaProperty] - [DisplayName("最大长度")] - public object MaxLength { get; set; } - - [FormulaProperty] - [DisplayName("正则表达式")] - public object RegexPattern { get; set; } - } - - public class SelectOptions : ObjectPropertyBase - { - [FormulaProperty] - [DisplayName("值")] - public object Value { get; set; } - - [FormulaProperty] - [DisplayName("显示文本")] - public object Label { get; set; } - } - - public class ImageCompressionOptions : ObjectPropertyBase - { - [DisplayName("启用压缩")] - [Description("默认关闭,关闭时按原图保存。启用后才使用下面的压缩参数。")] - public bool EnableCompression { get; set; } - - [FormulaProperty] - [DisplayName("最长边像素")] - public object MaxLongEdge { get; set; } = 1600; - - [FormulaProperty] - [DisplayName("JPEG质量")] - public object JpegQuality { get; set; } = 80; - - [FormulaProperty] - [DisplayName("目标大小KB")] - [Description("0表示不按文件大小继续压缩。")] - public object MaxFileSizeKb { get; set; } = 0; - - [FormulaProperty] - [DisplayName("最低JPEG质量")] - public object MinQuality { get; set; } = 60; - } - - public class ImageWatermarkOptions : ObjectPropertyBase - { - [DisplayName("时间戳水印")] - public bool EnableTimestamp { get; set; } = false; - - [ListProperty] - [DisplayName("自定义字段")] - public List Fields { get; set; } = new List - { - }; - } - - public class ImageWatermarkField : ObjectPropertyBase - { - [DisplayName("Key")] - public string Key { get; set; } - - [FormulaProperty] - [DisplayName("Value")] - public object Value { get; set; } - } - - public class FileItemConfig : ObjectPropertyBase - { - [FormulaProperty] - [DisplayName("最大数量")] - [Description("0表示不限制上传文件数量。")] - public object MaxCount { get; set; } = 0; - - [FormulaProperty] - [DisplayName("允许扩展名")] - [Description("逗号分隔,每个扩展名前带点;留空表示不限制,例如:.pdf,.doc,.docx。")] - public object AllowedExtensions { get; set; } - - [FormulaProperty] - [DisplayName("单文件最大大小KB")] - [Description("0表示不限制单文件大小。")] - public object MaxFileSizeKb { get; set; } = 0; - } } diff --git a/OfflinePlusBuildPattern.cs b/OfflinePlusBuildPattern.cs new file mode 100644 index 0000000..853e627 --- /dev/null +++ b/OfflinePlusBuildPattern.cs @@ -0,0 +1,332 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using GrapeCity.Forguncy.Commands; +using GrapeCity.Forguncy.Plugin; + +namespace AndroidPDACommand +{ + [Icon("pack://application:,,,/AndroidPDACommand;component/Resources/Icon_OfflineBuild.png")] + [Category("活字格安卓容器(HAC)")] + public class OfflinePlusBuildPattern : BaseCommand + { + [FormulaProperty] + [DisplayName("项目编号")] + [Description("项目的唯一标识,全局唯一")] + public object PatternId { get; set; } + + [DisplayName("名称")] + [FormulaProperty] + public object Title { get; set; } + + [DisplayName("描述")] + [FormulaProperty] + public object Description { get; set; } + + [Required] + [FormulaProperty] + [DisplayName("版本号")] + [Description("同一项目编号下的表单定义版本,用于区分表单结构变更。")] + public string SchemaVersion { get; set; } = "1.0"; + + [DisplayName("步骤")] + [ObjectListProperty(ItemType = typeof(OfflineFormStep))] + public List Steps { get; set; } + + [DisplayName("将离线表单JSON返回到变量")] + [ResultToProperty] + public string PatternJson { get; set; } = "OfflinePatternJson"; + + public override string ToString() + { + return "构建离线表单"; + } + } + + public class OfflineFormStep : ObjectPropertyBase, INamedObject + { + public string Name { get; set; } + + [FormulaProperty] + [DisplayName("步骤编号")] + [Description("该步骤的唯一标识,同一个离线表单项目内必须唯一。")] + public object StepId { get; set; } + + [FormulaProperty] + [DisplayName("标题")] + public object Title { get; set; } + + [TreeProperty(NodeType = typeof(OfflineFormNode), DefaultNodeName = "节点")] + [DisplayName("页面内容")] + public List Items { get; set; } = new List(); + } + + public class OfflineFormNode : ObjectPropertyBase, ITreeNode + { + private static readonly string[] CommonFieldVisibleProperties = + { + nameof(ItemId), + nameof(ItemType), + nameof(Title), + nameof(Hint), + nameof(Required) + }; + + private static readonly HashSet FieldTypeControlledProperties = new HashSet(CommonFieldVisibleProperties) + { + nameof(Value), + nameof(CheckOptions), + nameof(SelectOptionsList), + nameof(IncludeSeconds), + nameof(MaxCount), + nameof(AllowImageUpload), + nameof(CompressionOptions), + nameof(WatermarkOptions), + nameof(FileItemConfig) + }; + + private static readonly Dictionary> FieldVisiblePropertiesByType = new Dictionary> + { + { "textItem", CreateFieldVisibleProperties(nameof(Value), nameof(CheckOptions)) }, + { "passwordItem", CreateFieldVisibleProperties(nameof(Value), nameof(CheckOptions)) }, + { "selectItem", CreateFieldVisibleProperties(nameof(Value), nameof(SelectOptionsList)) }, + { "datePicker", CreateFieldVisibleProperties(nameof(Value)) }, + { "timePicker", CreateFieldVisibleProperties(nameof(Value), nameof(IncludeSeconds)) }, + { "imageItem", CreateFieldVisibleProperties(nameof(MaxCount), nameof(AllowImageUpload), nameof(CompressionOptions), nameof(WatermarkOptions)) }, + { "fileItem", CreateFieldVisibleProperties(nameof(FileItemConfig)) } + }; + + [DisplayName("节点名称")] + public string Text { get; set; } + + [Browsable(false)] + public IEnumerable Children { get; set; } = new List(); + + [DisplayName("节点类型")] + [ComboProperty(ValueList = "group|text|field", DisplayList = "分组|普通文本|表单项")] + public string NodeType { get; set; } = "group"; + + [FormulaProperty] + [DisplayName("说明内容")] + public object Content { get; set; } + + [DisplayName("默认折叠")] + public bool DefaultCollapsed { get; set; } + + [FormulaProperty] + [DisplayName("编号")] + [Description("该表单项的唯一标识,全局唯一,无论是同一项目内,还是跨项目,都必须唯一,更建议使用项目编号作为前缀拼接;例如项目为Project01,则该表单项编号为Project01_Name")] + public object ItemId { get; set; } + + [DisplayName("类型")] + [ComboProperty(ValueList = "textItem|passwordItem|selectItem|datePicker|timePicker|imageItem|fileItem", DisplayList = "文本框|密码框|选择框|日期选择|时间选择|图片列表|文件上传")] + public string ItemType { get; set; } = "textItem"; + + [FormulaProperty] + [DisplayName("标题")] + public object Title { get; set; } + + [FormulaProperty] + [DisplayName("水印")] + public object Hint { get; set; } + + [DisplayName("必填")] + public bool Required { get; set; } + + [FormulaProperty] + [DisplayName("默认值")] + public object Value { get; set; } + + [DisplayName("包含秒")] + [Description("仅时间选择类型生效。启用后可选择时分秒;关闭时使用系统原生时分选择器。")] + public bool IncludeSeconds { get; set; } + + + [ObjectProperty(ObjType = typeof(CheckOptions))] + [DisplayName("校验规则")] + public CheckOptions CheckOptions { get; set; } + + [ListProperty] + [DisplayName("选则项")] + public List SelectOptionsList { get; set; } + + [FormulaProperty] + [DisplayName("图片最大数量")] + [Description("0表示不限制图片数量。")] + public object MaxCount { get; set; } = 0; + + [DisplayName("允许图片上传")] + [Description("仅图片列表类型生效。启用后填报时除拍照外还可以从本地选择图片。")] + public bool AllowImageUpload { get; set; } + + [ObjectProperty(ObjType = typeof(ImageCompressionOptions))] + [DisplayName("图片压缩设置")] + public ImageCompressionOptions CompressionOptions { get; set; } = new ImageCompressionOptions(); + + [ObjectProperty(ObjType = typeof(ImageWatermarkOptions))] + [DisplayName("拍照水印配置")] + public ImageWatermarkOptions WatermarkOptions { get; set; } = new ImageWatermarkOptions(); + + [ObjectProperty(ObjType = typeof(FileItemConfig))] + [DisplayName("文件上传配置")] + public FileItemConfig FileItemConfig { get; set; } = new FileItemConfig(); + + public override bool GetDesignerPropertyVisible(string propertyName) + { + var isField = NodeType == "field"; + var isText = NodeType == "text"; + var isGroup = NodeType == "group"; + + switch (propertyName) + { + case nameof(Content): + return isText; + case nameof(DefaultCollapsed): + return isGroup; + default: + if (FieldTypeControlledProperties.Contains(propertyName)) + { + return isField && IsFieldPropertyVisible(ItemType, propertyName); + } + return base.GetDesignerPropertyVisible(propertyName); + } + } + + public override object Clone() + { + return new OfflineFormNode + { + Text = Text, + Children = Children == null ? new List() : Children.ToList(), + NodeType = NodeType, + Content = Content, + DefaultCollapsed = DefaultCollapsed, + ItemId = ItemId, + ItemType = ItemType, + Title = Title, + Hint = Hint, + Required = Required, + Value = Value, + IncludeSeconds = IncludeSeconds, + CheckOptions = CheckOptions, + SelectOptionsList = SelectOptionsList, + MaxCount = MaxCount, + AllowImageUpload = AllowImageUpload, + CompressionOptions = CompressionOptions, + WatermarkOptions = WatermarkOptions, + FileItemConfig = FileItemConfig + }; + } + + private static HashSet CreateFieldVisibleProperties(params string[] propertyNames) + { + var visibleProperties = new HashSet(CommonFieldVisibleProperties); + foreach (var propertyName in propertyNames) + { + visibleProperties.Add(propertyName); + } + return visibleProperties; + } + + private static bool IsFieldPropertyVisible(string itemType, string propertyName) + { + HashSet visibleProperties; + if (!FieldVisiblePropertiesByType.TryGetValue(itemType ?? "textItem", out visibleProperties)) + { + visibleProperties = FieldVisiblePropertiesByType["textItem"]; + } + return visibleProperties.Contains(propertyName); + } + } + + public class CheckOptions : ObjectPropertyBase + { + [FormulaProperty] + [DisplayName("最小长度")] + public object MinLength { get; set; } + + [FormulaProperty] + [DisplayName("最大长度")] + public object MaxLength { get; set; } + + [FormulaProperty] + [DisplayName("正则表达式")] + public object RegexPattern { get; set; } + } + + public class SelectOptions : ObjectPropertyBase + { + [FormulaProperty] + [DisplayName("值")] + public object Value { get; set; } + + [FormulaProperty] + [DisplayName("显示文本")] + public object Label { get; set; } + } + + public class ImageCompressionOptions : ObjectPropertyBase + { + [DisplayName("启用压缩")] + [Description("默认关闭,关闭时按原图保存。启用后才使用下面的压缩参数。")] + public bool EnableCompression { get; set; } + + [FormulaProperty] + [DisplayName("最长边像素")] + public object MaxLongEdge { get; set; } = 1600; + + [FormulaProperty] + [DisplayName("JPEG质量")] + public object JpegQuality { get; set; } = 80; + + [FormulaProperty] + [DisplayName("目标大小KB")] + [Description("0表示不按文件大小继续压缩。")] + public object MaxFileSizeKb { get; set; } = 0; + + [FormulaProperty] + [DisplayName("最低JPEG质量")] + public object MinQuality { get; set; } = 60; + } + + public class ImageWatermarkOptions : ObjectPropertyBase + { + [DisplayName("时间戳水印")] + public bool EnableTimestamp { get; set; } = false; + + [ListProperty] + [DisplayName("自定义字段")] + public List Fields { get; set; } = new List + { + }; + } + + public class ImageWatermarkField : ObjectPropertyBase + { + [DisplayName("Key")] + public string Key { get; set; } + + [FormulaProperty] + [DisplayName("Value")] + public object Value { get; set; } + } + + public class FileItemConfig : ObjectPropertyBase + { + [FormulaProperty] + [DisplayName("最大数量")] + [Description("0表示不限制上传文件数量。")] + public object MaxCount { get; set; } = 0; + + [FormulaProperty] + [DisplayName("允许扩展名")] + [Description("逗号分隔,每个扩展名前带点;留空表示不限制,例如:.pdf,.doc,.docx。")] + public object AllowedExtensions { get; set; } + + [FormulaProperty] + [DisplayName("单文件最大大小KB")] + [Description("0表示不限制单文件大小。")] + public object MaxFileSizeKb { get; set; } = 0; + } +} diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 6022923..6b22098 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2840,17 +2840,16 @@ var Biometric_Available_Async_Command = (function (_super) { Forguncy.CommandFactory.registerCommand("AndroidPDACommand.Biometric_Available_Async, AndroidPDACommand", Biometric_Available_Async_Command); -var OfflinePlusAddPattern_Async_Command = (function (_super) { - __extends(OfflinePlusAddPattern_Async_Command, _super); - function OfflinePlusAddPattern_Async_Command() { +var OfflinePlusBuildPattern_Command = (function (_super) { + __extends(OfflinePlusBuildPattern_Command, _super); + function OfflinePlusBuildPattern_Command() { return _super !== null && _super.apply(this, arguments) || this; } - OfflinePlusAddPattern_Async_Command.prototype.execute = async function () { + OfflinePlusBuildPattern_Command.prototype.execute = function () { let me = this; let params = this.CommandParam; - let patternParamJson = ""; function evaluate(value) { if (value === undefined || value === null) { @@ -3106,27 +3105,82 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { }); } - function getManualPdfUrl() { - if (params.ManualAttachmentCell === undefined || params.ManualAttachmentCell === null || params.ManualAttachmentCell === "") { - return undefined; + const steps = buildSteps(params.Steps); + let patternParam = { + patternId: this.evaluateFormula(params.PatternId), + schemaVersion: this.evaluateFormula(params.SchemaVersion), + title: this.evaluateFormula(params.Title), + description: this.evaluateFormula(params.Description), + steps: steps + }; + + var patternParamJson = JSON.stringify(patternParam, null, 2); + console.log(patternParam); + HAC_ReturnToParam(params.PatternJson, patternParamJson); + }; + + + return OfflinePlusBuildPattern_Command; +}(Forguncy.CommandBase)); + +Forguncy.CommandFactory.registerCommand("AndroidPDACommand.OfflinePlusBuildPattern, AndroidPDACommand", OfflinePlusBuildPattern_Command); + +var OfflinePlusAddPattern_Async_Command = (function (_super) { + __extends(OfflinePlusAddPattern_Async_Command, _super); + function OfflinePlusAddPattern_Async_Command() { + return _super !== null && _super.apply(this, arguments) || this; + } + + OfflinePlusAddPattern_Async_Command.prototype.execute = async function () { + + let me = this; + let params = this.CommandParam; + let finalPatternParamJson = ""; + + function evaluate(value) { + if (value === undefined || value === null) { + return value; } + return me.evaluateFormula(value); + } - var manualValue = evaluate(params.ManualAttachmentCell); + function getManualPdfUrl(manualValue) { if (!manualValue) { - return undefined; + return ""; } var fileId = (manualValue + "").split("|")[0]; if (!fileId) { console.warn("手册附件单元格没有文件标识。", manualValue); - return undefined; + return ""; } return Forguncy.Helper.SpecialPath.getFileDownloadUrl(fileId); } - let _ticket = HAC_GenerateCallbackTicket(me, function (payload, payload2) { - alert("离线表单项目设置成功\r\n\r\nJSON Schema:\r\n" + patternParamJson); + var patternJson = evaluate(params.PatternJson); + if (!patternJson) { + alert("离线表单JSON不能为空。"); + return; + } + + var patternParam; + try { + patternParam = typeof patternJson === "string" ? JSON.parse(patternJson) : patternJson; + } catch (e) { + alert("离线表单JSON格式不正确:" + (e ? e.message : "")); + return; + } + + var manualAttachmentValue = params.ManualAttachmentCell === undefined || params.ManualAttachmentCell === null || params.ManualAttachmentCell === "" + ? "" + : evaluate(params.ManualAttachmentCell); + var signatureBase64 = params.SignatureAttachmentCell === undefined || params.SignatureAttachmentCell === null || params.SignatureAttachmentCell === "" + ? "" + : evaluate(params.SignatureAttachmentCell); + + let ticket = HAC_GenerateCallbackTicket(me, function (payload, payload2) { + alert("离线表单项目设置成功\r\n\r\nJSON Schema:\r\n" + finalPatternParamJson); me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { runTimePageName: me.CommandExecutingInfo.runTimePageName, commandID: new Date().valueOf().toString(), @@ -3136,27 +3190,21 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { }, locationString: "执行window.offlinePlus.offlinePlusAddPattern" }); - }, HAC_GetOfflinePlusDebugInfo("添加离线表单项目", "生成回调ticket")); - const steps = buildSteps(params.Steps); - let patternParam = { - ticket: _ticket, - patternId: this.evaluateFormula(params.PatternId), - schemaVersion: this.evaluateFormula(params.SchemaVersion), - title: this.evaluateFormula(params.Title), - description: this.evaluateFormula(params.Description), - steps: steps - }; - const signatureBase64 = params.SignatureAttachmentCell === undefined || params.SignatureAttachmentCell === null || params.SignatureAttachmentCell === "" - ? "" - : this.evaluateFormula(params.SignatureAttachmentCell); - var manualPdfUrl = getManualPdfUrl(); + }, HAC_GetOfflinePlusDebugInfo("下载离线表单", "生成回调ticket")); + if (!ticket) { + console.error(HAC_GetOfflinePlusDebugInfo("下载离线表单", "生成回调ticket失败")); + return; + } + + var manualPdfUrl = getManualPdfUrl(manualAttachmentValue); + patternParam.ticket = ticket; + finalPatternParamJson = JSON.stringify(patternParam, null, 2); - patternParamJson = JSON.stringify(patternParam, null, 2); console.log(patternParam); if (window.offlinePlus && window.offlinePlus.offlinePlusAddPatternAsync) { - window.offlinePlus.offlinePlusAddPatternAsync(patternParamJson, manualPdfUrl || "", signatureBase64 || ""); + window.offlinePlus.offlinePlusAddPatternAsync(finalPatternParamJson, manualPdfUrl || "", signatureBase64 || ""); } else { - HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("添加离线表单项目", "调用offlinePlusAddPatternAsync")); + HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("下载离线表单", "调用offlinePlusAddPatternAsync")); } }; diff --git a/Resources/Icon_OfflineBuild.png b/Resources/Icon_OfflineBuild.png new file mode 100644 index 0000000000000000000000000000000000000000..9430bea9db1a5a7f715d3ae3259bade60d8e00cb GIT binary patch literal 295 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc!9AWXjv*HQ$q5V8E@;P=M{DL^t#^`G!WXMA z(V(*b?>~Qm<|_+dr(W3pUHk&a`eOk-yv Date: Wed, 24 Jun 2026 13:42:24 +0800 Subject: [PATCH 28/51] =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=86=85=E5=AE=B9=E5=85=AC=E5=BC=8F=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OfflinePlusBuildPattern.cs:在离线表单步骤上新增“使用页面内容公式”开关,并保留两个“页面内容”输入入口;通过 GetDesignerPropertyVisible 按开关隐藏固定页面内容树或公式输入,满足同一字段语义下切换输入方式的设计器配置需求。 Resources/AndroidPDACommand.js:调整构建步骤 JSON 的 items 来源;未勾选时继续使用原来的 buildNodes(step.Items),勾选时只 evaluate 页面内容公式并直接作为 step.items,避免公式模式再经过固定节点转换。 --- OfflinePlusBuildPattern.cs | 20 ++++++++++++++++++++ Resources/AndroidPDACommand.js | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/OfflinePlusBuildPattern.cs b/OfflinePlusBuildPattern.cs index 853e627..7be725a 100644 --- a/OfflinePlusBuildPattern.cs +++ b/OfflinePlusBuildPattern.cs @@ -57,9 +57,29 @@ namespace AndroidPDACommand [DisplayName("标题")] public object Title { get; set; } + [DisplayName("使用页面内容公式")] + public bool UseItemsFormula { get; set; } + [TreeProperty(NodeType = typeof(OfflineFormNode), DefaultNodeName = "节点")] [DisplayName("页面内容")] public List Items { get; set; } = new List(); + + [FormulaProperty] + [DisplayName("页面内容")] + public object ItemsFormula { get; set; } + + public override bool GetDesignerPropertyVisible(string propertyName) + { + switch (propertyName) + { + case nameof(Items): + return !UseItemsFormula; + case nameof(ItemsFormula): + return UseItemsFormula; + default: + return base.GetDesignerPropertyVisible(propertyName); + } + } } public class OfflineFormNode : ObjectPropertyBase, ITreeNode diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 6b22098..8eb00f1 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -3100,7 +3100,7 @@ var OfflinePlusBuildPattern_Command = (function (_super) { return { stepId: evaluate(step.StepId), title: evaluate(step.Title), - items: buildNodes(step.Items) + items: step.UseItemsFormula ? evaluate(step.ItemsFormula) : buildNodes(step.Items) }; }); } -- Gitee From 4705f60008737b66e23cd482ac1af72239414835 Mon Sep 17 00:00:00 2001 From: Rex Lei Date: Wed, 24 Jun 2026 14:37:18 +0800 Subject: [PATCH 29/51] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E7=A6=BB=E7=BA=BF=E9=A1=B5=E9=9D=A2=E5=86=85=E5=AE=B9=E5=91=BD?= =?UTF-8?q?=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 变更原因: - AndroidPDACommand.csproj:加入 OfflinePlusBuildFormItem.cs 编译项,并把 Icon_OfflineFormItem.png 注册为资源,确保新命令和图标会被插件打包。 - OfflinePlusBuildFormItem.cs:新增“构建离线页面内容”命令,复用 OfflineFormNode 的树形页面内容配置,返回单个页面内容对象供“页面内容”公式组合使用。 - Resources/AndroidPDACommand.js:抽出离线表单节点 JSON 构建逻辑供整表构建和单节点构建复用;新命令在单个根节点时直接返回该节点,多个根节点时自动包一层带时间戳 key/title 的 group,保留 children 支持。 - Resources/Icon_OfflineFormItem.png:新增新命令专用图标,避免和“构建离线表单”命令共用图标导致设计器命令识别不清。 验证: - node --check Resources/AndroidPDACommand.js - dotnet build AndroidPDACommand.csproj - git diff --cached --check --- AndroidPDACommand.csproj | 2 + OfflinePlusBuildFormItem.cs | 25 ++ Resources/AndroidPDACommand.js | 488 ++++++++++++++++------------- Resources/Icon_OfflineFormItem.png | Bin 0 -> 213 bytes 4 files changed, 296 insertions(+), 219 deletions(-) create mode 100644 OfflinePlusBuildFormItem.cs create mode 100644 Resources/Icon_OfflineFormItem.png diff --git a/AndroidPDACommand.csproj b/AndroidPDACommand.csproj index 575c3e8..fba84ea 100644 --- a/AndroidPDACommand.csproj +++ b/AndroidPDACommand.csproj @@ -50,6 +50,7 @@ + @@ -168,6 +169,7 @@ + diff --git a/OfflinePlusBuildFormItem.cs b/OfflinePlusBuildFormItem.cs new file mode 100644 index 0000000..2cf5b47 --- /dev/null +++ b/OfflinePlusBuildFormItem.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.ComponentModel; +using GrapeCity.Forguncy.Commands; +using GrapeCity.Forguncy.Plugin; + +namespace AndroidPDACommand +{ + [Icon("pack://application:,,,/AndroidPDACommand;component/Resources/Icon_OfflineFormItem.png")] + [Category("活字格安卓容器(HAC)")] + public class OfflinePlusBuildFormItem : BaseCommand + { + [TreeProperty(NodeType = typeof(OfflineFormNode), DefaultNodeName = "节点")] + [DisplayName("页面内容")] + public List Items { get; set; } = new List(); + + [DisplayName("将页面内容返回到变量")] + [ResultToProperty] + public string ItemResult { get; set; } = "OfflineFormItem"; + + public override string ToString() + { + return "构建离线页面内容"; + } + } +} diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 8eb00f1..e827326 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -2840,277 +2840,327 @@ var Biometric_Available_Async_Command = (function (_super) { Forguncy.CommandFactory.registerCommand("AndroidPDACommand.Biometric_Available_Async, AndroidPDACommand", Biometric_Available_Async_Command); -var OfflinePlusBuildPattern_Command = (function (_super) { - __extends(OfflinePlusBuildPattern_Command, _super); - function OfflinePlusBuildPattern_Command() { - return _super !== null && _super.apply(this, arguments) || this; +function HAC_OfflinePlusCreateFormNodeBuilder(context) { + let me = context; + + function evaluate(value) { + if (value === undefined || value === null) { + return value; + } + return me.evaluateFormula(value); } - OfflinePlusBuildPattern_Command.prototype.execute = function () { + function getList(value) { + return Array.isArray(value) ? value : []; + } - let me = this; - let params = this.CommandParam; + function buildSelectOptions(options) { + return getList(options).map(function (option) { + return { + value: evaluate(option.Value), + label: evaluate(option.Label) + }; + }); + } - function evaluate(value) { - if (value === undefined || value === null) { - return value; - } - return me.evaluateFormula(value); + function buildCheckOptions(options) { + if (!options) { + return undefined; } - function getList(value) { - return Array.isArray(value) ? value : []; + var checkOptions = {}; + var minLength = evaluate(options.MinLength); + var maxLength = evaluate(options.MaxLength); + var regexPattern = evaluate(options.RegexPattern); + if (minLength !== undefined && minLength !== null && minLength !== "") { + checkOptions.minLength = minLength; } - - function buildSelectOptions(options) { - return getList(options).map(function (option) { - return { - value: evaluate(option.Value), - label: evaluate(option.Label) - }; - }); + if (maxLength !== undefined && maxLength !== null && maxLength !== "") { + checkOptions.maxLength = maxLength; } + if (regexPattern !== undefined && regexPattern !== null && regexPattern !== "") { + checkOptions.regexPattern = regexPattern; + } + return Object.keys(checkOptions).length > 0 ? checkOptions : undefined; + } - function buildCheckOptions(options) { - if (!options) { - return undefined; - } + function buildImageCompression(options) { + if (!options) { + return undefined; + } - var checkOptions = {}; - var minLength = evaluate(options.MinLength); - var maxLength = evaluate(options.MaxLength); - var regexPattern = evaluate(options.RegexPattern); - if (minLength !== undefined && minLength !== null && minLength !== "") { - checkOptions.minLength = minLength; - } - if (maxLength !== undefined && maxLength !== null && maxLength !== "") { - checkOptions.maxLength = maxLength; - } - if (regexPattern !== undefined && regexPattern !== null && regexPattern !== "") { - checkOptions.regexPattern = regexPattern; - } - return Object.keys(checkOptions).length > 0 ? checkOptions : undefined; + var compression = {}; + var maxLongEdge = evaluate(options.MaxLongEdge); + var jpegQuality = evaluate(options.JpegQuality); + var maxFileSizeKb = evaluate(options.MaxFileSizeKb); + var minQuality = evaluate(options.MinQuality); + compression.enableCompression = !!options.EnableCompression; + if (maxLongEdge !== undefined && maxLongEdge !== null && maxLongEdge !== "") { + compression.maxLongEdge = maxLongEdge; + } + if (jpegQuality !== undefined && jpegQuality !== null && jpegQuality !== "") { + compression.jpegQuality = jpegQuality; + } + if (maxFileSizeKb !== undefined && maxFileSizeKb !== null && maxFileSizeKb !== "") { + compression.maxFileSizeKb = maxFileSizeKb; + } + if (minQuality !== undefined && minQuality !== null && minQuality !== "") { + compression.minQuality = minQuality; } + return Object.keys(compression).length > 0 ? compression : undefined; + } - function buildImageCompression(options) { - if (!options) { - return undefined; - } + function buildImageWatermark(options) { + if (!options) { + return undefined; + } - var compression = {}; - var maxLongEdge = evaluate(options.MaxLongEdge); - var jpegQuality = evaluate(options.JpegQuality); - var maxFileSizeKb = evaluate(options.MaxFileSizeKb); - var minQuality = evaluate(options.MinQuality); - compression.enableCompression = !!options.EnableCompression; - if (maxLongEdge !== undefined && maxLongEdge !== null && maxLongEdge !== "") { - compression.maxLongEdge = maxLongEdge; - } - if (jpegQuality !== undefined && jpegQuality !== null && jpegQuality !== "") { - compression.jpegQuality = jpegQuality; - } - if (maxFileSizeKb !== undefined && maxFileSizeKb !== null && maxFileSizeKb !== "") { - compression.maxFileSizeKb = maxFileSizeKb; - } - if (minQuality !== undefined && minQuality !== null && minQuality !== "") { - compression.minQuality = minQuality; - } - return Object.keys(compression).length > 0 ? compression : undefined; + var fields = getList(options.Fields); + if (!options.EnableTimestamp && fields.length === 0) { + return undefined; } - function buildImageWatermark(options) { - if (!options) { - return undefined; - } + var watermark = {}; + var items = fields.map(function (field) { + return { + key: field.Key, + value: evaluate(field.Value) + }; + }); - var fields = getList(options.Fields); - if (!options.EnableTimestamp && fields.length === 0) { - return undefined; - } + if (options.EnableTimestamp) { + watermark.enableTimestamp = true; + } + if (items.length > 0) { + watermark.items = items; + } + return Object.keys(watermark).length > 0 ? watermark : undefined; + } - var watermark = {}; - var items = fields.map(function (field) { - return { - key: field.Key, - value: evaluate(field.Value) - }; - }); + function buildFileItemConfig(options) { + if (!options) { + return undefined; + } - if (options.EnableTimestamp) { - watermark.enableTimestamp = true; - } - if (items.length > 0) { - watermark.items = items; - } - return Object.keys(watermark).length > 0 ? watermark : undefined; + var config = {}; + var maxCount = evaluate(options.MaxCount); + var allowedExtensions = evaluate(options.AllowedExtensions); + var maxFileSizeKb = evaluate(options.MaxFileSizeKb); + if (maxCount !== undefined && maxCount !== null && maxCount !== "") { + config.maxCount = maxCount; } + if (allowedExtensions !== undefined && allowedExtensions !== null && allowedExtensions !== "") { + config.allowedExtensions = allowedExtensions; + } + if (maxFileSizeKb !== undefined && maxFileSizeKb !== null && maxFileSizeKb !== "") { + config.maxFileSizeKb = maxFileSizeKb; + } + return Object.keys(config).length > 0 ? config : undefined; + } - function buildFileItemConfig(options) { - if (!options) { - return undefined; + var fieldTypeSpecs = { + textItem: { + apply: function (field, node) { + var checkOptions = buildCheckOptions(node.CheckOptions); + if (checkOptions) { + field.options = checkOptions; + } } - - var config = {}; - var maxCount = evaluate(options.MaxCount); - var allowedExtensions = evaluate(options.AllowedExtensions); - var maxFileSizeKb = evaluate(options.MaxFileSizeKb); - if (maxCount !== undefined && maxCount !== null && maxCount !== "") { - config.maxCount = maxCount; + }, + passwordItem: { + apply: function (field, node) { + var checkOptions = buildCheckOptions(node.CheckOptions); + if (checkOptions) { + field.options = checkOptions; + } } - if (allowedExtensions !== undefined && allowedExtensions !== null && allowedExtensions !== "") { - config.allowedExtensions = allowedExtensions; + }, + selectItem: { + apply: function (field, node) { + var selectOptions = buildSelectOptions(node.SelectOptionsList); + if (selectOptions.length > 0) { + field.options = { + selectOptions: selectOptions + }; + } } - if (maxFileSizeKb !== undefined && maxFileSizeKb !== null && maxFileSizeKb !== "") { - config.maxFileSizeKb = maxFileSizeKb; + }, + timePicker: { + apply: function (field, node) { + if (node.IncludeSeconds) { + field.options = { + includeSeconds: true + }; + } } - return Object.keys(config).length > 0 ? config : undefined; - } - - var fieldTypeSpecs = { - textItem: { - apply: function (field, node) { - var checkOptions = buildCheckOptions(node.CheckOptions); - if (checkOptions) { - field.options = checkOptions; - } + }, + imageItem: { + apply: function (field, node) { + var options = {}; + var maxCount = evaluate(node.MaxCount); + var compression = buildImageCompression(node.CompressionOptions); + var watermark = buildImageWatermark(node.WatermarkOptions); + if (maxCount !== undefined && maxCount !== null && maxCount !== "") { + options.maxCount = maxCount; } - }, - passwordItem: { - apply: function (field, node) { - var checkOptions = buildCheckOptions(node.CheckOptions); - if (checkOptions) { - field.options = checkOptions; - } + if (node.AllowImageUpload) { + options.allowImageUpload = true; } - }, - selectItem: { - apply: function (field, node) { - var selectOptions = buildSelectOptions(node.SelectOptionsList); - if (selectOptions.length > 0) { - field.options = { - selectOptions: selectOptions - }; - } + if (compression) { + options.compression = compression; } - }, - timePicker: { - apply: function (field, node) { - if (node.IncludeSeconds) { - field.options = { - includeSeconds: true - }; - } + if (watermark) { + options.watermark = watermark; } - }, - imageItem: { - apply: function (field, node) { - var options = {}; - var maxCount = evaluate(node.MaxCount); - var compression = buildImageCompression(node.CompressionOptions); - var watermark = buildImageWatermark(node.WatermarkOptions); - if (maxCount !== undefined && maxCount !== null && maxCount !== "") { - options.maxCount = maxCount; - } - if (node.AllowImageUpload) { - options.allowImageUpload = true; - } - if (compression) { - options.compression = compression; - } - if (watermark) { - options.watermark = watermark; - } - if (Object.keys(options).length > 0) { - field.options = options; - } + if (Object.keys(options).length > 0) { + field.options = options; } - }, - fileItem: { - apply: function (field, node) { - var config = buildFileItemConfig(node.FileItemConfig); - if (config) { - field.options = { - fileItemConfig: config - }; - } + } + }, + fileItem: { + apply: function (field, node) { + var config = buildFileItemConfig(node.FileItemConfig); + if (config) { + field.options = { + fileItemConfig: config + }; } } + } + }; + + function applyFieldTypeSpec(field, node) { + var spec = fieldTypeSpecs[node.ItemType]; + if (spec && spec.apply) { + spec.apply(field, node); + } + } + + function buildField(node) { + var field = { + itemId: evaluate(node.ItemId), + itemType: node.ItemType, + title: evaluate(node.Title), + hint: evaluate(node.Hint), + required: !!node.Required }; - function applyFieldTypeSpec(field, node) { - var spec = fieldTypeSpecs[node.ItemType]; - if (spec && spec.apply) { - spec.apply(field, node); - } + var value = evaluate(node.Value); + if (value !== undefined && value !== null && value !== "") { + field.value = value; } + applyFieldTypeSpec(field, node); + return field; + } - function buildField(node) { - var field = { - itemId: evaluate(node.ItemId), - itemType: node.ItemType, - title: evaluate(node.Title), - hint: evaluate(node.Hint), - required: !!node.Required - }; + function buildNode(node) { + var nodeType = node.NodeType || "group"; + var children = getList(node.Children); - var value = evaluate(node.Value); - if (value !== undefined && value !== null && value !== "") { - field.value = value; - } - applyFieldTypeSpec(field, node); - return field; + if (nodeType !== "group" && children.length > 0) { + console.warn("离线表单节点不是分组,但包含子节点。", node); } - function buildNodes(nodes) { - return getList(nodes).map(function (node) { - var nodeType = node.NodeType || "group"; - var children = getList(node.Children); + if (nodeType === "field") { + return { + nodeType: "field", + title: node.Text, + field: buildField(node) + }; + } - if (nodeType !== "group" && children.length > 0) { - console.warn("离线表单节点不是分组,但包含子节点。", node); - } + if (nodeType === "text") { + return { + nodeType: "text", + title: node.Text, + content: evaluate(node.Content) + }; + } - if (nodeType === "field") { - return { - nodeType: "field", - title: node.Text, - field: buildField(node) - }; - } + return { + nodeType: "group", + title: node.Text, + defaultCollapsed: !!node.DefaultCollapsed, + children: buildNodes(children) + }; + } - if (nodeType === "text") { - return { - nodeType: "text", - title: node.Text, - content: evaluate(node.Content) - }; - } + function buildNodes(nodes) { + return getList(nodes).map(function (node) { + return buildNode(node); + }); + } - return { - nodeType: "group", - title: node.Text, - defaultCollapsed: !!node.DefaultCollapsed, - children: buildNodes(children) - }; - }); + function buildFormItem(nodes) { + var items = buildNodes(nodes); + if (items.length <= 1) { + return items[0]; } + var timestamp = new Date().getTime(); + return { + nodeType: "group", + key: "offlineFormItemGroup_" + timestamp, + title: "页面内容分组_" + timestamp, + defaultCollapsed: false, + children: items + }; + } + + return { + evaluate: evaluate, + getList: getList, + buildNode: buildNode, + buildNodes: buildNodes, + buildFormItem: buildFormItem + }; +} + +var OfflinePlusBuildFormItem_Command = (function (_super) { + __extends(OfflinePlusBuildFormItem_Command, _super); + function OfflinePlusBuildFormItem_Command() { + return _super !== null && _super.apply(this, arguments) || this; + } + + OfflinePlusBuildFormItem_Command.prototype.execute = function () { + let params = this.CommandParam; + var formNodeBuilder = HAC_OfflinePlusCreateFormNodeBuilder(this); + var item = formNodeBuilder.buildFormItem(params.Items); + HAC_ReturnToParam(params.ItemResult, item); + }; + + return OfflinePlusBuildFormItem_Command; +}(Forguncy.CommandBase)); + +Forguncy.CommandFactory.registerCommand("AndroidPDACommand.OfflinePlusBuildFormItem, AndroidPDACommand", OfflinePlusBuildFormItem_Command); + +var OfflinePlusBuildPattern_Command = (function (_super) { + __extends(OfflinePlusBuildPattern_Command, _super); + function OfflinePlusBuildPattern_Command() { + return _super !== null && _super.apply(this, arguments) || this; + } + + OfflinePlusBuildPattern_Command.prototype.execute = function () { + + let params = this.CommandParam; + var formNodeBuilder = HAC_OfflinePlusCreateFormNodeBuilder(this); + function buildSteps(steps) { - return getList(steps).map(function (step) { + return formNodeBuilder.getList(steps).map(function (step) { return { - stepId: evaluate(step.StepId), - title: evaluate(step.Title), - items: step.UseItemsFormula ? evaluate(step.ItemsFormula) : buildNodes(step.Items) + stepId: formNodeBuilder.evaluate(step.StepId), + title: formNodeBuilder.evaluate(step.Title), + items: step.UseItemsFormula ? formNodeBuilder.evaluate(step.ItemsFormula) : formNodeBuilder.buildNodes(step.Items) }; }); } const steps = buildSteps(params.Steps); let patternParam = { - patternId: this.evaluateFormula(params.PatternId), - schemaVersion: this.evaluateFormula(params.SchemaVersion), - title: this.evaluateFormula(params.Title), - description: this.evaluateFormula(params.Description), + patternId: formNodeBuilder.evaluate(params.PatternId), + schemaVersion: formNodeBuilder.evaluate(params.SchemaVersion), + title: formNodeBuilder.evaluate(params.Title), + description: formNodeBuilder.evaluate(params.Description), steps: steps }; diff --git a/Resources/Icon_OfflineFormItem.png b/Resources/Icon_OfflineFormItem.png new file mode 100644 index 0000000000000000000000000000000000000000..26e42e6d02f98faf21b2d29902644cb786662b0b GIT binary patch literal 213 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc!7NV~#}EtuM-&X4F>C(h#_2e-C|7uXoEEl8fk(~wxw z>muEdXT3}NgH(er^Dc>8UWf1cWmnZDM3dnRUue?m~bf&{hUdS3j3^P6 Date: Wed, 1 Jul 2026 17:41:35 +0800 Subject: [PATCH 30/51] build with V12.0.2 --- AndroidPDACommand.csproj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/AndroidPDACommand.csproj b/AndroidPDACommand.csproj index fba84ea..c9cd4d7 100644 --- a/AndroidPDACommand.csproj +++ b/AndroidPDACommand.csproj @@ -113,19 +113,19 @@ - E:\forAgentA\forguncy\Forguncy.Server2\designerBin\Forguncy.Commands.dll + D:\Forguncy 12\WebSite\designerBin\Forguncy.Commands.dll False - E:\forAgentA\forguncy\Forguncy.Server2\designerBin\Forguncy.Commands.Design.dll + D:\Forguncy 12\WebSite\designerBin\Forguncy.Commands.Design.dll False - E:\forAgentA\forguncy\Forguncy.Server2\designerBin\GrapeCity.Forguncy.Plugin.dll + D:\Forguncy 12\WebSite\designerBin\GrapeCity.Forguncy.Plugin.dll False - E:\forAgentA\forguncy\Forguncy.Server2\bin\GrapeCity.Forguncy.ServerApi.dll + D:\Forguncy 12\WebSite\bin\GrapeCity.Forguncy.ServerApi.dll False @@ -174,6 +174,6 @@ - + -- Gitee From 1ac63c8114c61943ac5eba89608177d16ea8e1ab Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Fri, 3 Jul 2026 17:57:03 +0800 Subject: [PATCH 31/51] =?UTF-8?q?=E6=B3=A8=E9=87=8A=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Resources/AndroidPDACommand.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index e827326..54597d5 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -3230,7 +3230,7 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { : evaluate(params.SignatureAttachmentCell); let ticket = HAC_GenerateCallbackTicket(me, function (payload, payload2) { - alert("离线表单项目设置成功\r\n\r\nJSON Schema:\r\n" + finalPatternParamJson); + // alert("离线表单项目设置成功\r\n\r\nJSON Schema:\r\n" + finalPatternParamJson); me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { runTimePageName: me.CommandExecutingInfo.runTimePageName, commandID: new Date().valueOf().toString(), -- Gitee From 1a6d8706ae41a78af952072ba0192363f45722d5 Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Tue, 7 Jul 2026 12:24:58 +0800 Subject: [PATCH 32/51] =?UTF-8?q?=E6=89=B9=E9=87=8F=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OfflinePlusGetPatterns_Async.cs | 4 ++-- Resources/AndroidPDACommand.js | 37 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/OfflinePlusGetPatterns_Async.cs b/OfflinePlusGetPatterns_Async.cs index 3061322..0b73171 100644 --- a/OfflinePlusGetPatterns_Async.cs +++ b/OfflinePlusGetPatterns_Async.cs @@ -10,7 +10,7 @@ namespace AndroidPDACommand public class OfflinePlusGetPatterns_Async : BaseAsyncCommand { [DisplayName("动作")] - [ComboProperty(ValueList = "GetPatterns|ExportRecords|MarkRecordExported|DeleteReadRecords|DeleteProject", DisplayList = "获取设备上的离线表单项目编号列表|导出离线表单数据|标记离线记录已导出|删除已读离线记录|删除离线表单项目")] + [ComboProperty(ValueList = "GetPatterns|ExportRecords|MarkRecordExported|DeleteReadRecords|DeleteProject|BatchExportRecords", DisplayList = "获取设备上的离线表单项目编号列表|导出离线表单数据|标记离线记录已导出|删除已读离线记录|删除离线表单项目|批量导出离线表单数据")] public string Action { get; set; } = "GetPatterns"; [FormulaProperty] @@ -62,7 +62,7 @@ namespace AndroidPDACommand } if (propertyName == nameof(ExportResult)) { - return Action == "ExportRecords"; + return Action == "ExportRecords" || Action == "BatchExportRecords"; } if (propertyName == nameof(ProjectsResult)) { diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 54597d5..c8871ff 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -3397,6 +3397,43 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { return; } + if (action === "BatchExportRecords") { + let me = this; + let ticket = HAC_GenerateCallbackTicket(me, async function (payload, payload2) { + try { + HAC_ReturnToParam(params.ExportResult, payload); + me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { + runTimePageName: me.CommandExecutingInfo.runTimePageName, + commandID: new Date().valueOf().toString(), + initParams: { + "ExportResult": payload, + "IsSuccess": true, + "Error": "" + }, + locationString: "执行window.offlinePlus.offlinePlusBatchExportRecordsAsync" + }); + } catch (e) { + me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { + runTimePageName: me.CommandExecutingInfo.runTimePageName, + commandID: new Date().valueOf().toString(), + initParams: { + "ExportResult": payload, + "IsSuccess": false, + "Error": e ? e.toString() : "批量导出离线表单数据失败。" + }, + locationString: "批量导出离线表单数据时出错" + }); + } + }, HAC_GetOfflinePlusDebugInfo("批量导出离线表单", "生成批量导出回调ticket")); + + if (window.offlinePlus && window.offlinePlus.offlinePlusExportMultipleRecordsAsync) { + window.offlinePlus.offlinePlusExportMultipleRecordsAsync(ticket); + } else { + HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("批量导出离线表单", "offlinePlusExportMultipleRecordsAsync")); + } + return; + } + if (action !== "GetPatterns") { alert("不支持的离线表单上传动作:" + action); return; -- Gitee From f7eec639e7bb37c90f66f33a5cedee474e4c8afe Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Wed, 8 Jul 2026 14:12:55 +0800 Subject: [PATCH 33/51] =?UTF-8?q?=E8=BF=9B=E5=BA=A6=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Resources/AndroidPDACommand.js | 284 ++++++++++++++++++++++++++++++++- 1 file changed, 278 insertions(+), 6 deletions(-) diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index c8871ff..113f4b1 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -167,6 +167,155 @@ var HAC_OfflineUploadProgress = { this.documentOverflow = null; } }; + +/** + * 批量离线附件上传进度显示组件 - 支持多行显示多个表单状态 + */ +var HAC_BatchUploadProgress = { + element: null, + title: null, + listContainer: null, + itemMap: {}, + preventScrollHandler: null, + bodyOverflow: null, + documentOverflow: null, + ensure: function (itemCount) { + if (this.element) { + return; + } + var root = document.createElement("div"); + root.style.cssText = "position:fixed;left:0;top:0;right:0;bottom:0;z-index:2147483647;background:rgba(0,0,0,.28);display:flex;align-items:center;justify-content:center;font-family:Arial,'Microsoft YaHei',sans-serif;touch-action:none;overscroll-behavior:contain;padding:20px;box-sizing:border-box;"; + root.innerHTML = "
" + + "
正在导出离线表单数据
" + + "
" + + "
"; + this.bodyOverflow = document.body.style.overflow; + this.documentOverflow = document.documentElement.style.overflow; + document.body.style.overflow = "hidden"; + document.documentElement.style.overflow = "hidden"; + this.preventScrollHandler = function (event) { + if (event.target === root.querySelector("[data-role='list']")) { + return; + } + event.preventDefault(); + }; + root.addEventListener("touchmove", this.preventScrollHandler, { passive: false }); + document.body.appendChild(root); + this.element = root; + this.title = root.querySelector("[data-role='title']"); + this.listContainer = root.querySelector("[data-role='list']"); + }, + /** + * 初始化表单项 + * @param {*} itemKey 表单项唯一标识 + * @param {*} itemTitle 表单显示标题 + */ + initItem: function (itemKey, itemTitle) { + this.ensure(); + if (this.itemMap[itemKey]) { + return; + } + + var itemDiv = document.createElement("div"); + itemDiv.style.cssText = "display:flex;align-items:center;padding:12px;margin-bottom:8px;background:#f5f7fa;border-radius:6px;"; + itemDiv.innerHTML = "
" + + "
" + + "
" + + "
" + + "
"; + + this.listContainer.appendChild(itemDiv); + this.itemMap[itemKey] = { + element: itemDiv, + statusIcon: itemDiv.querySelector("[data-role='status']"), + title: itemDiv.querySelector("[data-role='title']"), + progress: itemDiv.querySelector("[data-role='progress']"), + state: 'pending' + }; + this.itemMap[itemKey].title.textContent = itemTitle || "离线表单"; + this.itemMap[itemKey].progress.textContent = "等待处理..."; + }, + /** + * 更新表单项状态 + * @param {*} itemKey 表单项唯一标识 + * @param {*} options 状态选项 { state, progress, detail, titleSuffix } + */ + updateItem: function (itemKey, options) { + this.ensure(); + var item = this.itemMap[itemKey]; + if (!item) { + return; + } + + if (options.state) { + item.state = options.state; + switch (options.state) { + case 'processing': + item.statusIcon.textContent = "⏳"; + item.statusIcon.style.color = "#2f7df6"; + break; + case 'completed': + item.statusIcon.textContent = "✓"; + item.statusIcon.style.color = "#52c41a"; + item.element.style.background = "#f6ffed"; + break; + case 'error': + item.statusIcon.textContent = "✗"; + item.statusIcon.style.color = "#ff4d4f"; + item.element.style.background = "#fff1f0"; + break; + } + } + + if (options.detail !== undefined) { + item.progress.textContent = options.detail; + } + + if (options.titleSuffix !== undefined) { + var baseTitle = item.baseTitle || item.title.textContent.split(" (")[0]; + item.baseTitle = baseTitle; + item.title.textContent = baseTitle + options.titleSuffix; + } + + if (options.progress !== undefined) { + if (item.state !== 'completed' && item.state !== 'error') { + item.progress.textContent = "处理中: " + Math.floor(options.progress) + "%"; + } + } + }, + /** + * 标记表单项完成 + * @param {*} itemKey 表单项唯一标识 + */ + completeItem: function (itemKey) { + this.updateItem(itemKey, { state: 'completed', detail: '已完成' }); + }, + /** + * 标记表单项错误 + * @param {*} itemKey 表单项唯一标识 + * @param {*} error 错误信息 + */ + errorItem: function (itemKey, error) { + this.updateItem(itemKey, { state: 'error', detail: error || '处理失败' }); + }, + close: function () { + if (this.element && this.preventScrollHandler) { + this.element.removeEventListener("touchmove", this.preventScrollHandler); + } + if (this.element && this.element.parentNode) { + this.element.parentNode.removeChild(this.element); + } + document.body.style.overflow = this.bodyOverflow || ""; + document.documentElement.style.overflow = this.documentOverflow || ""; + this.element = null; + this.title = null; + this.listContainer = null; + this.itemMap = {}; + this.preventScrollHandler = null; + this.bodyOverflow = null; + this.documentOverflow = null; + } +}; /** * 将文件上传到服务器临时目录,并返回服务器上的文件标识 * @param {*} file @@ -240,10 +389,10 @@ function HAC_GetOfflineAttachmentUploadFileName(attachment) { /** * 处理离线导出附件 - * @param {*} projectId - * @param {*} exportData - * @param {*} uploadLimitId - * @returns Promise,成功时返回处理后的导出数据,失败时返回错误信息 + * @param {*} projectId + * @param {*} exportData + * @param {*} uploadLimitId + * @returns Promise,成功时返回处理后的导出数据,失败时返回错误信息 */ async function HAC_ProcessOfflineExportAttachments(projectId, exportData, uploadLimitId) { if (!exportData) { @@ -307,6 +456,121 @@ async function HAC_ProcessOfflineExportAttachments(projectId, exportData, upload return exportData; } +/** + * 处理批量导出多条记录的附件 + * @param {*} exportDataArray 批量导出的数据数组,每项包含 { projectId, patternTitle, records, attachments } + * @param {*} uploadLimitId 上传限制ID + * @returns Promise,成功时返回处理后的导出数据数组 + */ +async function HAC_ProcessBatchExportAttachments(exportDataArray, uploadLimitId) { + if (!exportDataArray || !Array.isArray(exportDataArray) || exportDataArray.length === 0) { + return exportDataArray; + } + + if (!uploadLimitId) { + throw "请设置文件上传限制ID。"; + } + if (!window.offlinePlus || !window.offlinePlus.offlinePlusLoadAttachment) { + HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("批量导出离线表单", "调用offlinePlusLoadAttachment")); + throw "当前APP不支持读取离线附件。"; + } + + // 统计所有附件总数并初始化进度显示 + var totalAttachments = 0; + exportDataArray.forEach(function (item, index) { + if (item && item.attachments) { + totalAttachments += item.attachments.length; + } + // 初始化每个表单项的进度显示 + var itemKey = "item_" + index; + var itemTitle = item.patternTitle || ("离线表单 " + (index + 1)); + HAC_BatchUploadProgress.initItem(itemKey, itemTitle); + }); + + if (totalAttachments === 0) { + // 没有附件,直接标记所有项为完成 + exportDataArray.forEach(function (item, index) { + if (item) { + delete item.attachments; + HAC_BatchUploadProgress.completeItem("item_" + index); + } + }); + return exportDataArray; + } + + var uploadedValues = {}; + var currentAttachmentIndex = 0; + + HAC_BatchUploadProgress.updateItem("header", { detail: "共 " + totalAttachments + " 个附件待上传" }); + + for (var itemIndex = 0; itemIndex < exportDataArray.length; itemIndex++) { + var exportItem = exportDataArray[itemIndex]; + if (!exportItem) { + continue; + } + + var itemKey = "item_" + itemIndex; + var projectId = exportItem.projectId; + var records = exportItem.records || []; + var attachments = exportItem.attachments || []; + + // 更新标题显示进度(如 1/4) + HAC_BatchUploadProgress.updateItem(itemKey, { titleSuffix: " (" + (itemIndex + 1) + "/" + exportDataArray.length + ")" }); + + if (!attachments || attachments.length === 0) { + delete exportItem.attachments; + HAC_BatchUploadProgress.completeItem(itemKey); + continue; + } + + HAC_BatchUploadProgress.updateItem(itemKey, { state: 'processing', detail: '准备处理 ' + attachments.length + ' 个附件...' }); + + for (var i = 0; i < attachments.length; i++) { + var attachment = attachments[i]; + if (!attachment || !attachment.localName || !HAC_GetOfflineAttachmentRecordId(attachment) || !HAC_GetOfflineAttachmentFieldId(attachment)) { + continue; + } + + currentAttachmentIndex++; + var progressPercent = ((currentAttachmentIndex - 1) / totalAttachments) * 100; + + HAC_BatchUploadProgress.updateItem(itemKey, { + progress: progressPercent, + detail: "正在上传 " + currentAttachmentIndex + "/" + totalAttachments + ":" + (attachment.localName || "附件") + }); + + try { + var dataUrl = window.offlinePlus.offlinePlusLoadAttachment(projectId, attachment.localName); + var file = HAC_DataUrlToFile(dataUrl, HAC_GetOfflineAttachmentUploadFileName(attachment)); + if (!file) { + throw "读取离线附件失败:" + attachment.localName; + } + + var attachmentValue = await HAC_UploadFileToTemp(file, uploadLimitId, function (loaded, total, bytesPerSecond) { + var itemProgress = ((currentAttachmentIndex - 1 + (total > 0 ? loaded / total : 0)) / totalAttachments) * 100; + HAC_BatchUploadProgress.updateItem(itemKey, { + progress: itemProgress, + detail: "正在上传 " + HAC_FormatBytesAsMb(loaded) + "/" + HAC_FormatBytesAsMb(total) + ",速度:" + HAC_FormatBytesAsMb(bytesPerSecond) + "/s" + }); + }); + + var record = records.find(function (record) { + return record && record.recordId === HAC_GetOfflineAttachmentRecordId(attachment); + }); + HAC_ApplyUploadedAttachmentValue(record, attachment, attachmentValue, uploadedValues); + } catch (e) { + HAC_BatchUploadProgress.errorItem(itemKey, "附件上传失败: " + (attachment.localName || "未知")); + throw e; + } + } + + delete exportItem.attachments; + HAC_BatchUploadProgress.completeItem(itemKey); + } + + return exportDataArray; +} + var HAC_DothanPrinterOp = function (callback) { if (window.dothanPrinter) { var status = window.dothanPrinter.getStatus(); @@ -3398,9 +3662,15 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { } if (action === "BatchExportRecords") { + var fileUploadLimit = (params.ServerPropertiesId && params.ServerPropertiesId.FileUploadLimit) || ""; let me = this; let ticket = HAC_GenerateCallbackTicket(me, async function (payload, payload2) { try { + var exportDataArray = JSON.parse(payload); + if (exportDataArray && Array.isArray(exportDataArray)) { + exportDataArray = await HAC_ProcessBatchExportAttachments(exportDataArray, fileUploadLimit); + payload = JSON.stringify(exportDataArray); + } HAC_ReturnToParam(params.ExportResult, payload); me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { runTimePageName: me.CommandExecutingInfo.runTimePageName, @@ -3410,7 +3680,7 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { "IsSuccess": true, "Error": "" }, - locationString: "执行window.offlinePlus.offlinePlusBatchExportRecordsAsync" + locationString: "执行window.offlinePlus.offlinePlusExportMultipleRecordsAsync" }); } catch (e) { me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { @@ -3423,13 +3693,15 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { }, locationString: "批量导出离线表单数据时出错" }); + } finally { + HAC_BatchUploadProgress.close(); } }, HAC_GetOfflinePlusDebugInfo("批量导出离线表单", "生成批量导出回调ticket")); if (window.offlinePlus && window.offlinePlus.offlinePlusExportMultipleRecordsAsync) { window.offlinePlus.offlinePlusExportMultipleRecordsAsync(ticket); } else { - HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("批量导出离线表单", "offlinePlusExportMultipleRecordsAsync")); + HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("批量导出离线表单", "调用offlinePlusExportMultipleRecordsAsync")); } return; } -- Gitee From 3b4a2fc678dd1125ea002001c7d5d78e6d73e4ba Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Wed, 8 Jul 2026 17:44:43 +0800 Subject: [PATCH 34/51] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Resources/AndroidPDACommand.js | 269 ++++++++++++++++++++++++++++----- 1 file changed, 228 insertions(+), 41 deletions(-) diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 113f4b1..aec3dcb 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -174,9 +174,18 @@ var HAC_OfflineUploadProgress = { var HAC_BatchUploadProgress = { element: null, title: null, + countLabel: null, listContainer: null, + summaryLabel: null, + currentTitle: null, + percentLabel: null, + fill: null, + metaLeft: null, + metaRight: null, + confirmButton: null, itemMap: {}, preventScrollHandler: null, + buttonClickHandler: null, bodyOverflow: null, documentOverflow: null, ensure: function (itemCount) { @@ -184,17 +193,47 @@ var HAC_BatchUploadProgress = { return; } var root = document.createElement("div"); - root.style.cssText = "position:fixed;left:0;top:0;right:0;bottom:0;z-index:2147483647;background:rgba(0,0,0,.28);display:flex;align-items:center;justify-content:center;font-family:Arial,'Microsoft YaHei',sans-serif;touch-action:none;overscroll-behavior:contain;padding:20px;box-sizing:border-box;"; - root.innerHTML = "
" + - "
正在导出离线表单数据
" + - "
" + + root.style.cssText = "position:fixed;left:0;top:0;right:0;bottom:0;z-index:2147483647;background:#fff;display:flex;align-items:stretch;justify-content:center;font-family:Arial,'Microsoft YaHei',sans-serif;touch-action:none;overscroll-behavior:contain;box-sizing:border-box;"; + root.innerHTML = "" + + "
" + + "
" + + "
" + + "
批量导出离线表单
" + + "
批量上传已开始,状态会按处理进度实时更新。
" + + "
" + + "
" + + "
" + + "
" + + "正在导出:" + + "" + + "
" + + "0%" + + "
" + + "
" + + "
" + + "" + + "" + + "
" + + "
" + + "
" + + "
" + + "

单据列表

" + + "" + + "
" + + "
" + + "
" + + "
" + + "" + + "
" + + "
" + "
"; this.bodyOverflow = document.body.style.overflow; this.documentOverflow = document.documentElement.style.overflow; document.body.style.overflow = "hidden"; document.documentElement.style.overflow = "hidden"; this.preventScrollHandler = function (event) { - if (event.target === root.querySelector("[data-role='list']")) { + var list = root.querySelector("[data-role='list']"); + if (list && list.contains(event.target)) { return; } event.preventDefault(); @@ -203,7 +242,109 @@ var HAC_BatchUploadProgress = { document.body.appendChild(root); this.element = root; this.title = root.querySelector("[data-role='title']"); + this.countLabel = root.querySelector("[data-role='count']"); this.listContainer = root.querySelector("[data-role='list']"); + this.summaryLabel = root.querySelector("[data-role='summary-label']"); + this.currentTitle = root.querySelector("[data-role='current-title']"); + this.percentLabel = root.querySelector("[data-role='percent']"); + this.fill = root.querySelector("[data-role='fill']"); + this.metaLeft = root.querySelector("[data-role='meta-left']"); + this.metaRight = root.querySelector("[data-role='meta-right']"); + this.confirmButton = root.querySelector("[data-role='confirm']"); + this.buttonClickHandler = function () { + if (!this.confirmButton || this.confirmButton.disabled) { + return; + } + this.close(); + }.bind(this); + this.confirmButton.addEventListener("click", this.buttonClickHandler); + this.refreshCount(itemCount || 0); + this.updateSummary({ + percent: 0, + title: "准备导出", + metaLeft: "正在准备附件", + metaRight: "" + }); + }, + refreshCount: function (itemCount) { + if (!this.countLabel) { + return; + } + var count = itemCount || Object.keys(this.itemMap).length; + this.countLabel.textContent = "共 " + count + " 项"; + }, + renderStatus: function (statusIcon, state) { + if (!statusIcon) { + return; + } + switch (state) { + case 'processing': + statusIcon.style.color = "#2f7df6"; + statusIcon.innerHTML = ""; + break; + case 'completed': + statusIcon.style.color = "#52c41a"; + statusIcon.innerHTML = ""; + break; + case 'error': + statusIcon.style.color = "#ff4d4f"; + statusIcon.innerHTML = ""; + break; + default: + statusIcon.style.color = "#98a2b3"; + statusIcon.innerHTML = ""; + break; + } + }, + updateSummary: function (options) { + this.ensure(); + options = options || {}; + if (options.percent !== undefined) { + var percent = Math.max(0, Math.min(100, options.percent || 0)); + this.fill.style.width = percent.toFixed(1) + "%"; + this.percentLabel.textContent = Math.floor(percent) + "%"; + } + if (options.title !== undefined) { + this.currentTitle.textContent = options.title || "离线表单"; + } + if (options.metaLeft !== undefined) { + this.metaLeft.textContent = options.metaLeft || ""; + } + if (options.metaRight !== undefined) { + this.metaRight.textContent = options.metaRight || ""; + } + if (options.label !== undefined) { + this.summaryLabel.textContent = options.label || ""; + } + }, + finish: function (options) { + if (!this.element) { + return; + } + options = options || {}; + if (options.success === false) { + this.updateSummary({ + label: "导出结果:", + title: "处理完成", + metaLeft: "处理失败", + metaRight: options.error || "请查看失败项" + }); + } else { + this.updateSummary({ + label: "导出结果:", + percent: 100, + title: "处理完成", + metaLeft: "已完成全部动作", + metaRight: "可以关闭页面" + }); + } + if (this.confirmButton) { + this.confirmButton.disabled = false; + this.confirmButton.textContent = "确认"; + this.confirmButton.style.background = "#2f7df6"; + this.confirmButton.style.opacity = "1"; + this.confirmButton.style.cursor = "pointer"; + } }, /** * 初始化表单项 @@ -217,12 +358,12 @@ var HAC_BatchUploadProgress = { } var itemDiv = document.createElement("div"); - itemDiv.style.cssText = "display:flex;align-items:center;padding:12px;margin-bottom:8px;background:#f5f7fa;border-radius:6px;"; - itemDiv.innerHTML = "
" + - "
" + - "
" + - "
" + - "
"; + itemDiv.style.cssText = "display:flex;align-items:center;justify-content:space-between;gap:14px;padding:14px 2px;border-bottom:1px solid #e4e8f0;background:#fff;"; + itemDiv.innerHTML = "
" + + "" + + "" + + "
" + + "
"; this.listContainer.appendChild(itemDiv); this.itemMap[itemKey] = { @@ -230,10 +371,13 @@ var HAC_BatchUploadProgress = { statusIcon: itemDiv.querySelector("[data-role='status']"), title: itemDiv.querySelector("[data-role='title']"), progress: itemDiv.querySelector("[data-role='progress']"), + baseTitle: itemTitle || "离线表单", state: 'pending' }; this.itemMap[itemKey].title.textContent = itemTitle || "离线表单"; this.itemMap[itemKey].progress.textContent = "等待处理..."; + this.renderStatus(this.itemMap[itemKey].statusIcon, 'pending'); + this.refreshCount(); }, /** * 更新表单项状态 @@ -249,22 +393,7 @@ var HAC_BatchUploadProgress = { if (options.state) { item.state = options.state; - switch (options.state) { - case 'processing': - item.statusIcon.textContent = "⏳"; - item.statusIcon.style.color = "#2f7df6"; - break; - case 'completed': - item.statusIcon.textContent = "✓"; - item.statusIcon.style.color = "#52c41a"; - item.element.style.background = "#f6ffed"; - break; - case 'error': - item.statusIcon.textContent = "✗"; - item.statusIcon.style.color = "#ff4d4f"; - item.element.style.background = "#fff1f0"; - break; - } + this.renderStatus(item.statusIcon, options.state); } if (options.detail !== undefined) { @@ -272,9 +401,7 @@ var HAC_BatchUploadProgress = { } if (options.titleSuffix !== undefined) { - var baseTitle = item.baseTitle || item.title.textContent.split(" (")[0]; - item.baseTitle = baseTitle; - item.title.textContent = baseTitle + options.titleSuffix; + item.title.textContent = item.baseTitle + options.titleSuffix; } if (options.progress !== undefined) { @@ -302,6 +429,9 @@ var HAC_BatchUploadProgress = { if (this.element && this.preventScrollHandler) { this.element.removeEventListener("touchmove", this.preventScrollHandler); } + if (this.confirmButton && this.buttonClickHandler) { + this.confirmButton.removeEventListener("click", this.buttonClickHandler); + } if (this.element && this.element.parentNode) { this.element.parentNode.removeChild(this.element); } @@ -309,9 +439,18 @@ var HAC_BatchUploadProgress = { document.documentElement.style.overflow = this.documentOverflow || ""; this.element = null; this.title = null; + this.countLabel = null; this.listContainer = null; + this.summaryLabel = null; + this.currentTitle = null; + this.percentLabel = null; + this.fill = null; + this.metaLeft = null; + this.metaRight = null; + this.confirmButton = null; this.itemMap = {}; this.preventScrollHandler = null; + this.buttonClickHandler = null; this.bodyOverflow = null; this.documentOverflow = null; } @@ -477,15 +616,22 @@ async function HAC_ProcessBatchExportAttachments(exportDataArray, uploadLimitId) // 统计所有附件总数并初始化进度显示 var totalAttachments = 0; + HAC_BatchUploadProgress.ensure(exportDataArray.length); exportDataArray.forEach(function (item, index) { if (item && item.attachments) { totalAttachments += item.attachments.length; } // 初始化每个表单项的进度显示 var itemKey = "item_" + index; - var itemTitle = item.patternTitle || ("离线表单 " + (index + 1)); + var itemTitle = (item && item.projectId) || ("离线表单 " + (index + 1)); HAC_BatchUploadProgress.initItem(itemKey, itemTitle); }); + HAC_BatchUploadProgress.updateSummary({ + percent: 0, + title: "准备导出", + metaLeft: "已上传文件0/" + totalAttachments, + metaRight: totalAttachments > 0 ? ("共 " + totalAttachments + " 个附件待上传") : "无需上传附件" + }); if (totalAttachments === 0) { // 没有附件,直接标记所有项为完成 @@ -495,13 +641,25 @@ async function HAC_ProcessBatchExportAttachments(exportDataArray, uploadLimitId) HAC_BatchUploadProgress.completeItem("item_" + index); } }); + HAC_BatchUploadProgress.updateSummary({ + percent: 100, + title: "导出数据", + metaLeft: "无需上传附件", + metaRight: "共 " + exportDataArray.length + " 项" + }); return exportDataArray; } var uploadedValues = {}; var currentAttachmentIndex = 0; + var completedAttachmentCount = 0; - HAC_BatchUploadProgress.updateItem("header", { detail: "共 " + totalAttachments + " 个附件待上传" }); + HAC_BatchUploadProgress.updateSummary({ + percent: 0, + title: "准备上传附件", + metaLeft: "已上传文件0/" + totalAttachments, + metaRight: "共 " + totalAttachments + " 个附件待上传" + }); for (var itemIndex = 0; itemIndex < exportDataArray.length; itemIndex++) { var exportItem = exportDataArray[itemIndex]; @@ -513,9 +671,7 @@ async function HAC_ProcessBatchExportAttachments(exportDataArray, uploadLimitId) var projectId = exportItem.projectId; var records = exportItem.records || []; var attachments = exportItem.attachments || []; - - // 更新标题显示进度(如 1/4) - HAC_BatchUploadProgress.updateItem(itemKey, { titleSuffix: " (" + (itemIndex + 1) + "/" + exportDataArray.length + ")" }); + var currentItemTitle = exportItem.projectId || ("离线表单 " + (itemIndex + 1)); if (!attachments || attachments.length === 0) { delete exportItem.attachments; @@ -524,6 +680,12 @@ async function HAC_ProcessBatchExportAttachments(exportDataArray, uploadLimitId) } HAC_BatchUploadProgress.updateItem(itemKey, { state: 'processing', detail: '准备处理 ' + attachments.length + ' 个附件...' }); + HAC_BatchUploadProgress.updateSummary({ + percent: completedAttachmentCount / totalAttachments * 100, + title: currentItemTitle, + metaLeft: "已上传文件" + completedAttachmentCount + "/" + totalAttachments, + metaRight: "准备处理 " + attachments.length + " 个附件" + }); for (var i = 0; i < attachments.length; i++) { var attachment = attachments[i]; @@ -532,11 +694,18 @@ async function HAC_ProcessBatchExportAttachments(exportDataArray, uploadLimitId) } currentAttachmentIndex++; - var progressPercent = ((currentAttachmentIndex - 1) / totalAttachments) * 100; + var progressPercent = (completedAttachmentCount / totalAttachments) * 100; + var attachmentName = attachment.localName || "附件"; HAC_BatchUploadProgress.updateItem(itemKey, { progress: progressPercent, - detail: "正在上传 " + currentAttachmentIndex + "/" + totalAttachments + ":" + (attachment.localName || "附件") + detail: "正在上传 " + currentAttachmentIndex + "/" + totalAttachments + ":" + attachmentName + }); + HAC_BatchUploadProgress.updateSummary({ + percent: progressPercent, + title: currentItemTitle, + metaLeft: "已上传文件" + completedAttachmentCount + "/" + totalAttachments, + metaRight: attachmentName }); try { @@ -547,19 +716,32 @@ async function HAC_ProcessBatchExportAttachments(exportDataArray, uploadLimitId) } var attachmentValue = await HAC_UploadFileToTemp(file, uploadLimitId, function (loaded, total, bytesPerSecond) { - var itemProgress = ((currentAttachmentIndex - 1 + (total > 0 ? loaded / total : 0)) / totalAttachments) * 100; + var itemProgress = ((completedAttachmentCount + (total > 0 ? loaded / total : 0)) / totalAttachments) * 100; HAC_BatchUploadProgress.updateItem(itemKey, { progress: itemProgress, detail: "正在上传 " + HAC_FormatBytesAsMb(loaded) + "/" + HAC_FormatBytesAsMb(total) + ",速度:" + HAC_FormatBytesAsMb(bytesPerSecond) + "/s" }); + HAC_BatchUploadProgress.updateSummary({ + percent: itemProgress, + title: currentItemTitle, + metaLeft: "已上传文件" + completedAttachmentCount + "/" + totalAttachments, + metaRight: attachmentName + " · " + HAC_FormatBytesAsMb(bytesPerSecond) + "/s" + }); }); var record = records.find(function (record) { return record && record.recordId === HAC_GetOfflineAttachmentRecordId(attachment); }); HAC_ApplyUploadedAttachmentValue(record, attachment, attachmentValue, uploadedValues); + completedAttachmentCount++; + HAC_BatchUploadProgress.updateSummary({ + percent: completedAttachmentCount / totalAttachments * 100, + title: currentItemTitle, + metaLeft: "已上传文件" + completedAttachmentCount + "/" + totalAttachments, + metaRight: attachmentName + " 已上传" + }); } catch (e) { - HAC_BatchUploadProgress.errorItem(itemKey, "附件上传失败: " + (attachment.localName || "未知")); + HAC_BatchUploadProgress.errorItem(itemKey, "附件上传失败: " + (attachmentName || "未知")); throw e; } } @@ -3665,6 +3847,7 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { var fileUploadLimit = (params.ServerPropertiesId && params.ServerPropertiesId.FileUploadLimit) || ""; let me = this; let ticket = HAC_GenerateCallbackTicket(me, async function (payload, payload2) { + var batchExportError = ""; try { var exportDataArray = JSON.parse(payload); if (exportDataArray && Array.isArray(exportDataArray)) { @@ -3683,18 +3866,22 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { locationString: "执行window.offlinePlus.offlinePlusExportMultipleRecordsAsync" }); } catch (e) { + batchExportError = e ? e.toString() : "批量导出离线表单数据失败。"; me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { runTimePageName: me.CommandExecutingInfo.runTimePageName, commandID: new Date().valueOf().toString(), initParams: { "ExportResult": payload, "IsSuccess": false, - "Error": e ? e.toString() : "批量导出离线表单数据失败。" + "Error": batchExportError }, locationString: "批量导出离线表单数据时出错" }); } finally { - HAC_BatchUploadProgress.close(); + HAC_BatchUploadProgress.finish({ + success: !batchExportError, + error: batchExportError + }); } }, HAC_GetOfflinePlusDebugInfo("批量导出离线表单", "生成批量导出回调ticket")); -- Gitee From 6cc9229771a783d0d84b139a3e5bbecd70f9309f Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Wed, 8 Jul 2026 18:08:40 +0800 Subject: [PATCH 35/51] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Resources/AndroidPDACommand.js | 74 +++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index aec3dcb..164eabb 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -186,6 +186,7 @@ var HAC_BatchUploadProgress = { itemMap: {}, preventScrollHandler: null, buttonClickHandler: null, + confirmHandler: null, bodyOverflow: null, documentOverflow: null, ensure: function (itemCount) { @@ -255,7 +256,11 @@ var HAC_BatchUploadProgress = { if (!this.confirmButton || this.confirmButton.disabled) { return; } + var confirmHandler = this.confirmHandler; this.close(); + if (confirmHandler) { + confirmHandler(); + } }.bind(this); this.confirmButton.addEventListener("click", this.buttonClickHandler); this.refreshCount(itemCount || 0); @@ -318,10 +323,14 @@ var HAC_BatchUploadProgress = { } }, finish: function (options) { + options = options || {}; if (!this.element) { + if (options.onConfirm) { + options.onConfirm(); + } return; } - options = options || {}; + this.confirmHandler = options.onConfirm || null; if (options.success === false) { this.updateSummary({ label: "导出结果:", @@ -451,6 +460,7 @@ var HAC_BatchUploadProgress = { this.itemMap = {}; this.preventScrollHandler = null; this.buttonClickHandler = null; + this.confirmHandler = null; this.bodyOverflow = null; this.documentOverflow = null; } @@ -3848,41 +3858,49 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { let me = this; let ticket = HAC_GenerateCallbackTicket(me, async function (payload, payload2) { var batchExportError = ""; + var batchExportPayload = payload; + var batchExportSuccess = false; try { var exportDataArray = JSON.parse(payload); if (exportDataArray && Array.isArray(exportDataArray)) { exportDataArray = await HAC_ProcessBatchExportAttachments(exportDataArray, fileUploadLimit); - payload = JSON.stringify(exportDataArray); + batchExportPayload = JSON.stringify(exportDataArray); } - HAC_ReturnToParam(params.ExportResult, payload); - me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { - runTimePageName: me.CommandExecutingInfo.runTimePageName, - commandID: new Date().valueOf().toString(), - initParams: { - "ExportResult": payload, - "IsSuccess": true, - "Error": "" - }, - locationString: "执行window.offlinePlus.offlinePlusExportMultipleRecordsAsync" - }); + batchExportSuccess = true; } catch (e) { batchExportError = e ? e.toString() : "批量导出离线表单数据失败。"; - me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { - runTimePageName: me.CommandExecutingInfo.runTimePageName, - commandID: new Date().valueOf().toString(), - initParams: { - "ExportResult": payload, - "IsSuccess": false, - "Error": batchExportError - }, - locationString: "批量导出离线表单数据时出错" - }); - } finally { - HAC_BatchUploadProgress.finish({ - success: !batchExportError, - error: batchExportError - }); } + var continueBatchExport = function () { + if (batchExportSuccess) { + HAC_ReturnToParam(params.ExportResult, batchExportPayload); + me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { + runTimePageName: me.CommandExecutingInfo.runTimePageName, + commandID: new Date().valueOf().toString(), + initParams: { + "ExportResult": batchExportPayload, + "IsSuccess": true, + "Error": "" + }, + locationString: "执行window.offlinePlus.offlinePlusExportMultipleRecordsAsync" + }); + } else { + me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { + runTimePageName: me.CommandExecutingInfo.runTimePageName, + commandID: new Date().valueOf().toString(), + initParams: { + "ExportResult": batchExportPayload, + "IsSuccess": false, + "Error": batchExportError + }, + locationString: "批量导出离线表单数据时出错" + }); + } + }; + HAC_BatchUploadProgress.finish({ + success: batchExportSuccess, + error: batchExportError, + onConfirm: continueBatchExport + }); }, HAC_GetOfflinePlusDebugInfo("批量导出离线表单", "生成批量导出回调ticket")); if (window.offlinePlus && window.offlinePlus.offlinePlusExportMultipleRecordsAsync) { -- Gitee From e061d1f161e0113b6c05d6c86dded0d58bb0e7a0 Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Fri, 17 Jul 2026 09:28:11 +0800 Subject: [PATCH 36/51] =?UTF-8?q?=E7=A6=BB=E7=BA=BF=E8=A1=A8=E5=8D=95?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0List=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OfflinePlusBuildPattern.cs | 83 ++++++++++++++++++++++++++++++++-- Resources/AndroidPDACommand.js | 46 ++++++++++++++++++- 2 files changed, 123 insertions(+), 6 deletions(-) diff --git a/OfflinePlusBuildPattern.cs b/OfflinePlusBuildPattern.cs index 7be725a..e9ab83b 100644 --- a/OfflinePlusBuildPattern.cs +++ b/OfflinePlusBuildPattern.cs @@ -84,6 +84,11 @@ namespace AndroidPDACommand public class OfflineFormNode : ObjectPropertyBase, ITreeNode { + public const string BaseItemTypeValueList = "textItem|passwordItem|selectItem|datePicker|timePicker|imageItem|fileItem"; + public const string BaseItemTypeDisplayList = "文本框|密码框|选择框|日期选择|时间选择|图片列表|文件上传"; + public const string ItemTypeValueList = BaseItemTypeValueList + "|listItem"; + public const string ItemTypeDisplayList = BaseItemTypeDisplayList + "|列表"; + private static readonly string[] CommonFieldVisibleProperties = { nameof(ItemId), @@ -103,7 +108,8 @@ namespace AndroidPDACommand nameof(AllowImageUpload), nameof(CompressionOptions), nameof(WatermarkOptions), - nameof(FileItemConfig) + nameof(FileItemConfig), + nameof(ListItemOptions) }; private static readonly Dictionary> FieldVisiblePropertiesByType = new Dictionary> @@ -114,7 +120,8 @@ namespace AndroidPDACommand { "datePicker", CreateFieldVisibleProperties(nameof(Value)) }, { "timePicker", CreateFieldVisibleProperties(nameof(Value), nameof(IncludeSeconds)) }, { "imageItem", CreateFieldVisibleProperties(nameof(MaxCount), nameof(AllowImageUpload), nameof(CompressionOptions), nameof(WatermarkOptions)) }, - { "fileItem", CreateFieldVisibleProperties(nameof(FileItemConfig)) } + { "fileItem", CreateFieldVisibleProperties(nameof(FileItemConfig)) }, + { "listItem", CreateFieldVisibleProperties(nameof(ListItemOptions)) } }; [DisplayName("节点名称")] @@ -140,8 +147,8 @@ namespace AndroidPDACommand public object ItemId { get; set; } [DisplayName("类型")] - [ComboProperty(ValueList = "textItem|passwordItem|selectItem|datePicker|timePicker|imageItem|fileItem", DisplayList = "文本框|密码框|选择框|日期选择|时间选择|图片列表|文件上传")] - public string ItemType { get; set; } = "textItem"; + [ComboProperty(ValueList = ItemTypeValueList, DisplayList = ItemTypeDisplayList)] + public virtual string ItemType { get; set; } = "textItem"; [FormulaProperty] [DisplayName("标题")] @@ -192,6 +199,10 @@ namespace AndroidPDACommand [DisplayName("文件上传配置")] public FileItemConfig FileItemConfig { get; set; } = new FileItemConfig(); + [ObjectProperty(ObjType = typeof(ListItemOptions))] + [DisplayName("列表配置")] + public ListItemOptions ListItemOptions { get; set; } = new ListItemOptions(); + public override bool GetDesignerPropertyVisible(string propertyName) { var isField = NodeType == "field"; @@ -235,7 +246,8 @@ namespace AndroidPDACommand AllowImageUpload = AllowImageUpload, CompressionOptions = CompressionOptions, WatermarkOptions = WatermarkOptions, - FileItemConfig = FileItemConfig + FileItemConfig = FileItemConfig, + ListItemOptions = ListItemOptions }; } @@ -260,6 +272,40 @@ namespace AndroidPDACommand } } + public class OfflineListFormNode : OfflineFormNode + { + [DisplayName("类型")] + [ComboProperty(ValueList = BaseItemTypeValueList, DisplayList = BaseItemTypeDisplayList)] + public override string ItemType { get; set; } = "textItem"; + + public override object Clone() + { + return new OfflineListFormNode + { + Text = Text, + Children = Children == null ? new List() : Children.ToList(), + NodeType = NodeType, + Content = Content, + DefaultCollapsed = DefaultCollapsed, + ItemId = ItemId, + ItemType = ItemType, + Title = Title, + Hint = Hint, + Required = Required, + Value = Value, + IncludeSeconds = IncludeSeconds, + CheckOptions = CheckOptions, + SelectOptionsList = SelectOptionsList, + MaxCount = MaxCount, + AllowImageUpload = AllowImageUpload, + CompressionOptions = CompressionOptions, + WatermarkOptions = WatermarkOptions, + FileItemConfig = FileItemConfig, + ListItemOptions = ListItemOptions + }; + } + } + public class CheckOptions : ObjectPropertyBase { [FormulaProperty] @@ -349,4 +395,31 @@ namespace AndroidPDACommand [Description("0表示不限制单文件大小。")] public object MaxFileSizeKb { get; set; } = 0; } + + public class ListItemOptions : ObjectPropertyBase + { + [FormulaProperty] + [DisplayName("最小数量")] + public object MinCount { get; set; } + + [FormulaProperty] + [DisplayName("最大数量")] + public object MaxCount { get; set; } + + [FormulaProperty] + [DisplayName("默认数量")] + public object DefaultCount { get; set; } + + [FormulaProperty] + [DisplayName("添加按钮文本")] + public object AddButtonText { get; set; } + + [FormulaProperty] + [DisplayName("列表项标题")] + public object ItemTitle { get; set; } + + [TreeProperty(NodeType = typeof(OfflineListFormNode), DefaultNodeName = "节点")] + [DisplayName("列表子项")] + public List Children { get; set; } = new List(); + } } diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 164eabb..7be13e6 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -3414,6 +3414,41 @@ function HAC_OfflinePlusCreateFormNodeBuilder(context) { return Object.keys(config).length > 0 ? config : undefined; } + function buildListItemOptions(node) { + var options = {}; + var listOptions = node.ListItemOptions || {}; + var minCount = evaluate(listOptions.MinCount); + var maxCount = evaluate(listOptions.MaxCount); + var defaultCount = evaluate(listOptions.DefaultCount); + var addButtonText = evaluate(listOptions.AddButtonText); + var itemTitle = evaluate(listOptions.ItemTitle); + var children = buildNodes(listOptions.Children); + + if (children.length === 0 && getList(node.Children).length > 0) { + children = buildNodes(node.Children); + } + + if (minCount !== undefined && minCount !== null && minCount !== "") { + options.minCount = minCount; + } + if (maxCount !== undefined && maxCount !== null && maxCount !== "") { + options.maxCount = maxCount; + } + if (defaultCount !== undefined && defaultCount !== null && defaultCount !== "") { + options.defaultCount = defaultCount; + } + if (addButtonText !== undefined && addButtonText !== null && addButtonText !== "") { + options.addButtonText = addButtonText; + } + if (itemTitle !== undefined && itemTitle !== null && itemTitle !== "") { + options.itemTitle = itemTitle; + } + if (children.length > 0) { + options.children = children; + } + return Object.keys(options).length > 0 ? options : undefined; + } + var fieldTypeSpecs = { textItem: { apply: function (field, node) { @@ -3482,6 +3517,14 @@ function HAC_OfflinePlusCreateFormNodeBuilder(context) { }; } } + }, + listItem: { + apply: function (field, node) { + var options = buildListItemOptions(node); + if (options) { + field.options = options; + } + } } }; @@ -3512,8 +3555,9 @@ function HAC_OfflinePlusCreateFormNodeBuilder(context) { function buildNode(node) { var nodeType = node.NodeType || "group"; var children = getList(node.Children); + var isListField = nodeType === "field" && node.ItemType === "listItem"; - if (nodeType !== "group" && children.length > 0) { + if (nodeType !== "group" && !isListField && children.length > 0) { console.warn("离线表单节点不是分组,但包含子节点。", node); } -- Gitee From 4aa3774bff069401501033e45ee7670814eacc87 Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Thu, 23 Jul 2026 14:09:24 +0800 Subject: [PATCH 37/51] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=A6=BB=E7=BA=BFjson?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AndroidPDACommand.csproj | 1 + OfflinePlusValidatePatternJson.cs | 32 +++++ Resources/AndroidPDACommand.js | 216 ++++++++++++++++++++++++++++-- 3 files changed, 239 insertions(+), 10 deletions(-) create mode 100644 OfflinePlusValidatePatternJson.cs diff --git a/AndroidPDACommand.csproj b/AndroidPDACommand.csproj index c9cd4d7..5a89361 100644 --- a/AndroidPDACommand.csproj +++ b/AndroidPDACommand.csproj @@ -53,6 +53,7 @@ + diff --git a/OfflinePlusValidatePatternJson.cs b/OfflinePlusValidatePatternJson.cs new file mode 100644 index 0000000..b96687b --- /dev/null +++ b/OfflinePlusValidatePatternJson.cs @@ -0,0 +1,32 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using GrapeCity.Forguncy.Commands; +using GrapeCity.Forguncy.Plugin; + +namespace AndroidPDACommand +{ + [Icon("pack://application:,,,/AndroidPDACommand;component/Resources/Icon_OfflineDownload.png")] + [Category("活字格安卓容器(HAC)")] + public class OfflinePlusValidatePatternJson : BaseCommand + { + [Required] + [FormulaProperty] + [DisplayName("离线表单JSON")] + [Description("需要检测的JSON字符串或JSON对象。结构要求与“下载离线表单”命令的“离线表单JSON”参数一致。")] + public object PatternJson { get; set; } + + [DisplayName("将是否有效返回到变量")] + [ResultToProperty] + public string IsValidResult { get; set; } = "OfflinePatternJsonIsValid"; + + [DisplayName("将错误点返回到变量")] + [Description("校验成功时返回空字符串;失败时返回第一个错误的JSON路径和错误原因。")] + [ResultToProperty] + public string ErrorResult { get; set; } = "OfflinePatternJsonError"; + + public override string ToString() + { + return "检测离线表单JSON"; + } + } +} diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 7be13e6..cc9858f 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -3675,6 +3675,208 @@ var OfflinePlusBuildPattern_Command = (function (_super) { Forguncy.CommandFactory.registerCommand("AndroidPDACommand.OfflinePlusBuildPattern, AndroidPDACommand", OfflinePlusBuildPattern_Command); +function HAC_OfflinePlusValidatePatternJson(patternJson) { + function fail(path, message) { + return { isValid: false, error: path + ":" + message }; + } + + function isObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); + } + + function isBlank(value) { + return value === undefined || value === null || (typeof value === "string" && value.trim() === ""); + } + + function validateNonNegativeNumber(value, path) { + if (value !== undefined && value !== null && value !== "" && + (typeof value !== "number" || !isFinite(value) || value < 0)) { + return fail(path, "必须是大于或等于0的数字"); + } + return null; + } + + function validateNode(node, path, itemIds) { + if (!isObject(node)) { + return fail(path, "必须是对象"); + } + + var nodeType = node.nodeType; + if (["group", "text", "field"].indexOf(nodeType) < 0) { + return fail(path + ".nodeType", "必须是group、text或field"); + } + + if (nodeType === "group") { + if (!Array.isArray(node.children)) { + return fail(path + ".children", "必须是数组"); + } + for (var groupIndex = 0; groupIndex < node.children.length; groupIndex++) { + var groupError = validateNode(node.children[groupIndex], path + ".children[" + groupIndex + "]", itemIds); + if (groupError) { + return groupError; + } + } + return null; + } + + if (nodeType === "text") { + if (node.content === undefined || node.content === null) { + return fail(path + ".content", "缺少说明内容"); + } + return null; + } + + if (!isObject(node.field)) { + return fail(path + ".field", "必须是对象"); + } + + var field = node.field; + if (isBlank(field.itemId)) { + return fail(path + ".field.itemId", "不能为空"); + } + var itemIdKey = String(field.itemId); + if (itemIds[itemIdKey]) { + return fail(path + ".field.itemId", "与" + itemIds[itemIdKey] + "重复"); + } + itemIds[itemIdKey] = path + ".field.itemId"; + + var itemTypes = ["textItem", "passwordItem", "selectItem", "datePicker", "timePicker", "imageItem", "fileItem", "listItem"]; + if (itemTypes.indexOf(field.itemType) < 0) { + return fail(path + ".field.itemType", "是不支持的表单项类型"); + } + if (isBlank(field.title)) { + return fail(path + ".field.title", "不能为空"); + } + if (field.required !== undefined && typeof field.required !== "boolean") { + return fail(path + ".field.required", "必须是布尔值"); + } + if (field.options !== undefined && !isObject(field.options)) { + return fail(path + ".field.options", "必须是对象"); + } + + var options = field.options || {}; + var numberError; + if (field.itemType === "selectItem") { + if (!Array.isArray(options.selectOptions) || options.selectOptions.length === 0) { + return fail(path + ".field.options.selectOptions", "必须是非空数组"); + } + for (var optionIndex = 0; optionIndex < options.selectOptions.length; optionIndex++) { + var optionPath = path + ".field.options.selectOptions[" + optionIndex + "]"; + if (!isObject(options.selectOptions[optionIndex])) { + return fail(optionPath, "必须是对象"); + } + if (isBlank(options.selectOptions[optionIndex].value)) { + return fail(optionPath + ".value", "不能为空"); + } + if (isBlank(options.selectOptions[optionIndex].label)) { + return fail(optionPath + ".label", "不能为空"); + } + } + } else if (field.itemType === "listItem") { + if (!Array.isArray(options.children) || options.children.length === 0) { + return fail(path + ".field.options.children", "必须是非空数组"); + } + numberError = validateNonNegativeNumber(options.minCount, path + ".field.options.minCount") || + validateNonNegativeNumber(options.maxCount, path + ".field.options.maxCount") || + validateNonNegativeNumber(options.defaultCount, path + ".field.options.defaultCount"); + if (numberError) { + return numberError; + } + for (var listIndex = 0; listIndex < options.children.length; listIndex++) { + var listError = validateNode(options.children[listIndex], path + ".field.options.children[" + listIndex + "]", itemIds); + if (listError) { + return listError; + } + } + } else if (field.itemType === "imageItem") { + numberError = validateNonNegativeNumber(options.maxCount, path + ".field.options.maxCount"); + if (numberError) { + return numberError; + } + } + + return null; + } + + if (patternJson === undefined || patternJson === null || patternJson === "") { + return fail("$", "离线表单JSON不能为空"); + } + + var pattern; + try { + pattern = typeof patternJson === "string" ? JSON.parse(patternJson) : patternJson; + } catch (e) { + return fail("$", "JSON格式不正确" + (e && e.message ? "(" + e.message + ")" : "")); + } + + if (!isObject(pattern)) { + return fail("$", "根节点必须是对象"); + } + if (isBlank(pattern.patternId)) { + return fail("$.patternId", "不能为空"); + } + if (isBlank(pattern.schemaVersion)) { + return fail("$.schemaVersion", "不能为空"); + } + if (isBlank(pattern.title)) { + return fail("$.title", "不能为空"); + } + if (!Array.isArray(pattern.steps) || pattern.steps.length === 0) { + return fail("$.steps", "必须是非空数组"); + } + + var stepIds = Object.create(null); + var itemIds = Object.create(null); + for (var stepIndex = 0; stepIndex < pattern.steps.length; stepIndex++) { + var step = pattern.steps[stepIndex]; + var stepPath = "$.steps[" + stepIndex + "]"; + if (!isObject(step)) { + return fail(stepPath, "必须是对象"); + } + if (isBlank(step.stepId)) { + return fail(stepPath + ".stepId", "不能为空"); + } + var stepIdKey = String(step.stepId); + if (stepIds[stepIdKey]) { + return fail(stepPath + ".stepId", "与" + stepIds[stepIdKey] + "重复"); + } + stepIds[stepIdKey] = stepPath + ".stepId"; + if (isBlank(step.title)) { + return fail(stepPath + ".title", "不能为空"); + } + if (!Array.isArray(step.items) || step.items.length === 0) { + return fail(stepPath + ".items", "必须是非空数组"); + } + for (var itemIndex = 0; itemIndex < step.items.length; itemIndex++) { + var itemError = validateNode(step.items[itemIndex], stepPath + ".items[" + itemIndex + "]", itemIds); + if (itemError) { + return itemError; + } + } + } + + return { isValid: true, error: "", value: pattern }; +} + +var OfflinePlusValidatePatternJson_Command = (function (_super) { + __extends(OfflinePlusValidatePatternJson_Command, _super); + function OfflinePlusValidatePatternJson_Command() { + return _super !== null && _super.apply(this, arguments) || this; + } + + OfflinePlusValidatePatternJson_Command.prototype.execute = function () { + var params = this.CommandParam; + var patternJson = this.evaluateFormula(params.PatternJson); + var result = HAC_OfflinePlusValidatePatternJson(patternJson); + HAC_ReturnToParam(params.IsValidResult, result.isValid); + HAC_ReturnToParam(params.ErrorResult, result.error); + }; + + return OfflinePlusValidatePatternJson_Command; +}(Forguncy.CommandBase)); + +Forguncy.CommandFactory.registerCommand("AndroidPDACommand.OfflinePlusValidatePatternJson, AndroidPDACommand", OfflinePlusValidatePatternJson_Command); + var OfflinePlusAddPattern_Async_Command = (function (_super) { __extends(OfflinePlusAddPattern_Async_Command, _super); function OfflinePlusAddPattern_Async_Command() { @@ -3709,18 +3911,12 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { } var patternJson = evaluate(params.PatternJson); - if (!patternJson) { - alert("离线表单JSON不能为空。"); - return; - } - - var patternParam; - try { - patternParam = typeof patternJson === "string" ? JSON.parse(patternJson) : patternJson; - } catch (e) { - alert("离线表单JSON格式不正确:" + (e ? e.message : "")); + var validationResult = HAC_OfflinePlusValidatePatternJson(patternJson); + if (!validationResult.isValid) { + alert("离线表单JSON不正确:" + validationResult.error); return; } + var patternParam = validationResult.value; var manualAttachmentValue = params.ManualAttachmentCell === undefined || params.ManualAttachmentCell === null || params.ManualAttachmentCell === "" ? "" -- Gitee From bd86bcd70115c1011436b5a4e37e606a1e26aaf3 Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Mon, 10 Aug 2026 10:36:41 +0800 Subject: [PATCH 38/51] change version --- PluginConfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluginConfig.json b/PluginConfig.json index 2ac37f6..db67877 100644 --- a/PluginConfig.json +++ b/PluginConfig.json @@ -12,7 +12,7 @@ "name": "PDA(Android)交互命令", "pluginType": "command", "guid": "{16B8DF69-1379-41C1-9194-BE89BAF8BB32}", - "version": "1.22.0.0", + "version": "1.23.0.0", "dependenceVersion": "8.0.0.0", "bundleJavaScript": true, "bundleCSS": true -- Gitee From 59672fb1320cea9202c227757ffca09cb123cdc4 Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Tue, 18 Aug 2026 15:35:04 +0800 Subject: [PATCH 39/51] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=BC=82=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AndroidPDACommand.csproj | 3 + PluginConfig.json | 3 +- Resources/AndroidPDACommand.js | 439 ++++----------------- Resources/OfflinePlusExportProgress.js | 504 +++++++++++++++++++++++++ 4 files changed, 579 insertions(+), 370 deletions(-) create mode 100644 Resources/OfflinePlusExportProgress.js diff --git a/AndroidPDACommand.csproj b/AndroidPDACommand.csproj index fe1c5dd..0ec5be3 100644 --- a/AndroidPDACommand.csproj +++ b/AndroidPDACommand.csproj @@ -139,6 +139,9 @@ + + PreserveNewest + PreserveNewest diff --git a/PluginConfig.json b/PluginConfig.json index db67877..1775e55 100644 --- a/PluginConfig.json +++ b/PluginConfig.json @@ -3,6 +3,7 @@ "AndroidPDACommand.dll" ], "javascript": [ + "Resources/OfflinePlusExportProgress.js", "Resources/AndroidPDACommand.js", "Resources/LPAPI-0.4.20181022.js" ], @@ -16,4 +17,4 @@ "dependenceVersion": "8.0.0.0", "bundleJavaScript": true, "bundleCSS": true -} \ No newline at end of file +} diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 406049b..d313aff 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -37,7 +37,7 @@ var HAC_GenerateCellInfo = function (context, formula) { return JSON.stringify(cellLocation); }; -var HAC_GenerateCallbackTicket = function (context, onSuccess, debugInfo) { +var HAC_GenerateCallbackTicket = function (context, onSuccess, debugInfo, onError) { if (!window.HAC) { HAC_CallFunctionOutOfHAC(debugInfo || "window.HAC is undefined."); } else { @@ -51,6 +51,9 @@ var HAC_GenerateCallbackTicket = function (context, onSuccess, debugInfo) { } window.HAC.registryCallback(ticket, onSuccess, function (error) { + if (onError) { + onError(error); + } context.CommandExecutor.excuteCommand(context.CommandParam.CommandList, { runTimePageName: context.CommandExecutingInfo.runTimePageName, commandID: new Date().valueOf().toString(), @@ -101,370 +104,7 @@ var HAC_DataUrlToFile = function (dataUrl, fileName) { var HAC_FormatBytesAsMb = function (bytes) { return (bytes / 1024 / 1024).toFixed(2) + " MB"; }; -/** - * 离线附件上传进度显示组件 - */ -var HAC_OfflineUploadProgress = { - element: null, - fill: null, - title: null, - detail: null, - speed: null, - preventScrollHandler: null, - bodyOverflow: null, - documentOverflow: null, - ensure: function () { - if (this.element) { - return; - } - var root = document.createElement("div"); - root.style.cssText = "position:fixed;left:0;top:0;right:0;bottom:0;z-index:2147483647;background:rgba(0,0,0,.28);display:flex;align-items:center;justify-content:center;font-family:Arial,'Microsoft YaHei',sans-serif;touch-action:none;overscroll-behavior:contain;"; - root.innerHTML = "
" + - "
正在上传离线附件
" + - "
" + - "
" + - "
" + - "
"; - this.bodyOverflow = document.body.style.overflow; - this.documentOverflow = document.documentElement.style.overflow; - document.body.style.overflow = "hidden"; - document.documentElement.style.overflow = "hidden"; - this.preventScrollHandler = function (event) { - event.preventDefault(); - }; - root.addEventListener("touchmove", this.preventScrollHandler, { passive: false }); - document.body.appendChild(root); - this.element = root; - this.fill = root.querySelector("[data-role='fill']"); - this.title = root.querySelector("[data-role='title']"); - this.detail = root.querySelector("[data-role='detail']"); - this.speed = root.querySelector("[data-role='speed']"); - }, - update: function (options) { - this.ensure(); - var percent = Math.max(0, Math.min(100, options.percent || 0)); - this.fill.style.width = percent.toFixed(1) + "%"; - this.title.textContent = options.title || "正在上传离线附件"; - this.detail.textContent = options.detail || ""; - this.speed.textContent = options.speed || ""; - }, - close: function () { - if (this.element && this.preventScrollHandler) { - this.element.removeEventListener("touchmove", this.preventScrollHandler); - } - if (this.element && this.element.parentNode) { - this.element.parentNode.removeChild(this.element); - } - document.body.style.overflow = this.bodyOverflow || ""; - document.documentElement.style.overflow = this.documentOverflow || ""; - this.element = null; - this.fill = null; - this.title = null; - this.detail = null; - this.speed = null; - this.preventScrollHandler = null; - this.bodyOverflow = null; - this.documentOverflow = null; - } -}; - -/** - * 批量离线附件上传进度显示组件 - 支持多行显示多个表单状态 - */ -var HAC_BatchUploadProgress = { - element: null, - title: null, - countLabel: null, - listContainer: null, - summaryLabel: null, - currentTitle: null, - percentLabel: null, - fill: null, - metaLeft: null, - metaRight: null, - confirmButton: null, - itemMap: {}, - preventScrollHandler: null, - buttonClickHandler: null, - confirmHandler: null, - bodyOverflow: null, - documentOverflow: null, - ensure: function (itemCount) { - if (this.element) { - return; - } - var root = document.createElement("div"); - root.style.cssText = "position:fixed;left:0;top:0;right:0;bottom:0;z-index:2147483647;background:#fff;display:flex;align-items:stretch;justify-content:center;font-family:Arial,'Microsoft YaHei',sans-serif;touch-action:none;overscroll-behavior:contain;box-sizing:border-box;"; - root.innerHTML = "" + - "
" + - "
" + - "
" + - "
批量导出离线表单
" + - "
批量上传已开始,状态会按处理进度实时更新。
" + - "
" + - "
" + - "
" + - "
" + - "正在导出:" + - "" + - "
" + - "0%" + - "
" + - "
" + - "
" + - "" + - "" + - "
" + - "
" + - "
" + - "
" + - "

单据列表

" + - "" + - "
" + - "
" + - "
" + - "
" + - "" + - "
" + - "
" + - "
"; - this.bodyOverflow = document.body.style.overflow; - this.documentOverflow = document.documentElement.style.overflow; - document.body.style.overflow = "hidden"; - document.documentElement.style.overflow = "hidden"; - this.preventScrollHandler = function (event) { - var list = root.querySelector("[data-role='list']"); - if (list && list.contains(event.target)) { - return; - } - event.preventDefault(); - }; - root.addEventListener("touchmove", this.preventScrollHandler, { passive: false }); - document.body.appendChild(root); - this.element = root; - this.title = root.querySelector("[data-role='title']"); - this.countLabel = root.querySelector("[data-role='count']"); - this.listContainer = root.querySelector("[data-role='list']"); - this.summaryLabel = root.querySelector("[data-role='summary-label']"); - this.currentTitle = root.querySelector("[data-role='current-title']"); - this.percentLabel = root.querySelector("[data-role='percent']"); - this.fill = root.querySelector("[data-role='fill']"); - this.metaLeft = root.querySelector("[data-role='meta-left']"); - this.metaRight = root.querySelector("[data-role='meta-right']"); - this.confirmButton = root.querySelector("[data-role='confirm']"); - this.buttonClickHandler = function () { - if (!this.confirmButton || this.confirmButton.disabled) { - return; - } - var confirmHandler = this.confirmHandler; - this.close(); - if (confirmHandler) { - confirmHandler(); - } - }.bind(this); - this.confirmButton.addEventListener("click", this.buttonClickHandler); - this.refreshCount(itemCount || 0); - this.updateSummary({ - percent: 0, - title: "准备导出", - metaLeft: "正在准备附件", - metaRight: "" - }); - }, - refreshCount: function (itemCount) { - if (!this.countLabel) { - return; - } - var count = itemCount || Object.keys(this.itemMap).length; - this.countLabel.textContent = "共 " + count + " 项"; - }, - renderStatus: function (statusIcon, state) { - if (!statusIcon) { - return; - } - switch (state) { - case 'processing': - statusIcon.style.color = "#2f7df6"; - statusIcon.innerHTML = ""; - break; - case 'completed': - statusIcon.style.color = "#52c41a"; - statusIcon.innerHTML = ""; - break; - case 'error': - statusIcon.style.color = "#ff4d4f"; - statusIcon.innerHTML = ""; - break; - default: - statusIcon.style.color = "#98a2b3"; - statusIcon.innerHTML = ""; - break; - } - }, - updateSummary: function (options) { - this.ensure(); - options = options || {}; - if (options.percent !== undefined) { - var percent = Math.max(0, Math.min(100, options.percent || 0)); - this.fill.style.width = percent.toFixed(1) + "%"; - this.percentLabel.textContent = Math.floor(percent) + "%"; - } - if (options.title !== undefined) { - this.currentTitle.textContent = options.title || "离线表单"; - } - if (options.metaLeft !== undefined) { - this.metaLeft.textContent = options.metaLeft || ""; - } - if (options.metaRight !== undefined) { - this.metaRight.textContent = options.metaRight || ""; - } - if (options.label !== undefined) { - this.summaryLabel.textContent = options.label || ""; - } - }, - finish: function (options) { - options = options || {}; - if (!this.element) { - if (options.onConfirm) { - options.onConfirm(); - } - return; - } - this.confirmHandler = options.onConfirm || null; - if (options.success === false) { - this.updateSummary({ - label: "导出结果:", - title: "处理完成", - metaLeft: "处理失败", - metaRight: options.error || "请查看失败项" - }); - } else { - this.updateSummary({ - label: "导出结果:", - percent: 100, - title: "处理完成", - metaLeft: "已完成全部动作", - metaRight: "可以关闭页面" - }); - } - if (this.confirmButton) { - this.confirmButton.disabled = false; - this.confirmButton.textContent = "确认"; - this.confirmButton.style.background = "#2f7df6"; - this.confirmButton.style.opacity = "1"; - this.confirmButton.style.cursor = "pointer"; - } - }, - /** - * 初始化表单项 - * @param {*} itemKey 表单项唯一标识 - * @param {*} itemTitle 表单显示标题 - */ - initItem: function (itemKey, itemTitle) { - this.ensure(); - if (this.itemMap[itemKey]) { - return; - } - - var itemDiv = document.createElement("div"); - itemDiv.style.cssText = "display:flex;align-items:center;justify-content:space-between;gap:14px;padding:14px 2px;border-bottom:1px solid #e4e8f0;background:#fff;"; - itemDiv.innerHTML = "
" + - "" + - "" + - "
" + - "
"; - - this.listContainer.appendChild(itemDiv); - this.itemMap[itemKey] = { - element: itemDiv, - statusIcon: itemDiv.querySelector("[data-role='status']"), - title: itemDiv.querySelector("[data-role='title']"), - progress: itemDiv.querySelector("[data-role='progress']"), - baseTitle: itemTitle || "离线表单", - state: 'pending' - }; - this.itemMap[itemKey].title.textContent = itemTitle || "离线表单"; - this.itemMap[itemKey].progress.textContent = "等待处理..."; - this.renderStatus(this.itemMap[itemKey].statusIcon, 'pending'); - this.refreshCount(); - }, - /** - * 更新表单项状态 - * @param {*} itemKey 表单项唯一标识 - * @param {*} options 状态选项 { state, progress, detail, titleSuffix } - */ - updateItem: function (itemKey, options) { - this.ensure(); - var item = this.itemMap[itemKey]; - if (!item) { - return; - } - - if (options.state) { - item.state = options.state; - this.renderStatus(item.statusIcon, options.state); - } - if (options.detail !== undefined) { - item.progress.textContent = options.detail; - } - - if (options.titleSuffix !== undefined) { - item.title.textContent = item.baseTitle + options.titleSuffix; - } - - if (options.progress !== undefined) { - if (item.state !== 'completed' && item.state !== 'error') { - item.progress.textContent = "处理中: " + Math.floor(options.progress) + "%"; - } - } - }, - /** - * 标记表单项完成 - * @param {*} itemKey 表单项唯一标识 - */ - completeItem: function (itemKey) { - this.updateItem(itemKey, { state: 'completed', detail: '已完成' }); - }, - /** - * 标记表单项错误 - * @param {*} itemKey 表单项唯一标识 - * @param {*} error 错误信息 - */ - errorItem: function (itemKey, error) { - this.updateItem(itemKey, { state: 'error', detail: error || '处理失败' }); - }, - close: function () { - if (this.element && this.preventScrollHandler) { - this.element.removeEventListener("touchmove", this.preventScrollHandler); - } - if (this.confirmButton && this.buttonClickHandler) { - this.confirmButton.removeEventListener("click", this.buttonClickHandler); - } - if (this.element && this.element.parentNode) { - this.element.parentNode.removeChild(this.element); - } - document.body.style.overflow = this.bodyOverflow || ""; - document.documentElement.style.overflow = this.documentOverflow || ""; - this.element = null; - this.title = null; - this.countLabel = null; - this.listContainer = null; - this.summaryLabel = null; - this.currentTitle = null; - this.percentLabel = null; - this.fill = null; - this.metaLeft = null; - this.metaRight = null; - this.confirmButton = null; - this.itemMap = {}; - this.preventScrollHandler = null; - this.buttonClickHandler = null; - this.confirmHandler = null; - this.bodyOverflow = null; - this.documentOverflow = null; - } -}; /** * 将文件上传到服务器临时目录,并返回服务器上的文件标识 * @param {*} file @@ -3757,7 +3397,7 @@ function HAC_OfflinePlusValidatePatternJson(patternJson) { function validateNonNegativeNumber(value, path) { if (value !== undefined && value !== null && value !== "" && (typeof value !== "number" || !isFinite(value) || value < 0)) { - return fail(path, "必须是大于或等于0的数字"); + return fail(path, "必须是大于或等于0的数字:" + value); } return null; } @@ -3911,6 +3551,7 @@ function HAC_OfflinePlusValidatePatternJson(patternJson) { return fail(stepPath + ".title", "不能为空"); } if (!Array.isArray(step.items) || step.items.length === 0) { + console.log(Array.isArray(step.items) + "-" + step.items.length) return fail(stepPath + ".items", "必须是非空数组"); } for (var itemIndex = 0; itemIndex < step.items.length; itemIndex++) { @@ -4037,9 +3678,18 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { var action = params.Action || "GetPatterns"; if (action === "ExportRecords") { + if (!HAC_OfflineExportExecution.begin()) { + return; + } var projectId = this.evaluateFormula(params.ProjectId); var fileUploadLimit = (params.ServerPropertiesId && params.ServerPropertiesId.FileUploadLimit) || ""; let me = this; + HAC_OfflineUploadProgress.update({ + percent: 0, + title: "正在上传离线附件", + detail: "正在读取设备上的离线数据", + speed: "" + }); let ticket = HAC_GenerateCallbackTicket(me, async function (payload, payload2) { try { var exportData = JSON.parse(payload); @@ -4071,13 +3721,31 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { }); } finally { HAC_OfflineUploadProgress.close(); + HAC_OfflineExportExecution.complete(); } - }, HAC_GetOfflinePlusDebugInfo("上传离线表单", "生成导出数据回调ticket")); + }, HAC_GetOfflinePlusDebugInfo("上传离线表单", "生成导出数据回调ticket"), function () { + HAC_OfflineUploadProgress.close(); + HAC_OfflineExportExecution.complete(); + }); + + if (!ticket) { + HAC_OfflineUploadProgress.close(); + HAC_OfflineExportExecution.complete(); + return; + } if (window.offlinePlus && window.offlinePlus.offlinePlusExportRecordsAsync) { - window.offlinePlus.offlinePlusExportRecordsAsync(projectId, ticket); + try { + window.offlinePlus.offlinePlusExportRecordsAsync(projectId, ticket); + } catch (e) { + HAC_OfflineUploadProgress.close(); + HAC_OfflineExportExecution.complete(); + throw e; + } } else { HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("上传离线表单", "调用offlinePlusExportRecordsAsync")); + HAC_OfflineUploadProgress.close(); + HAC_OfflineExportExecution.complete(); } return; } @@ -4160,8 +3828,18 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { } if (action === "BatchExportRecords") { + if (!HAC_OfflineExportExecution.begin()) { + return; + } var fileUploadLimit = (params.ServerPropertiesId && params.ServerPropertiesId.FileUploadLimit) || ""; let me = this; + HAC_BatchUploadProgress.ensure(0); + HAC_BatchUploadProgress.updateSummary({ + percent: 0, + title: "准备导出", + metaLeft: "正在读取设备数据", + metaRight: "" + }); let ticket = HAC_GenerateCallbackTicket(me, async function (payload, payload2) { var batchExportError = ""; var batchExportPayload = payload; @@ -4177,6 +3855,7 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { batchExportError = e ? e.toString() : "批量导出离线表单数据失败。"; } var continueBatchExport = function () { + HAC_OfflineExportExecution.complete(); if (batchExportSuccess) { HAC_ReturnToParam(params.ExportResult, batchExportPayload); me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { @@ -4207,12 +3886,34 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { error: batchExportError, onConfirm: continueBatchExport }); - }, HAC_GetOfflinePlusDebugInfo("批量导出离线表单", "生成批量导出回调ticket")); + }, HAC_GetOfflinePlusDebugInfo("批量导出离线表单", "生成批量导出回调ticket"), function (error) { + HAC_BatchUploadProgress.finish({ + success: false, + error: error, + onConfirm: function () { + HAC_OfflineExportExecution.complete(); + } + }); + }); + + if (!ticket) { + HAC_BatchUploadProgress.close(); + HAC_OfflineExportExecution.complete(); + return; + } if (window.offlinePlus && window.offlinePlus.offlinePlusExportMultipleRecordsAsync) { - window.offlinePlus.offlinePlusExportMultipleRecordsAsync(ticket); + try { + window.offlinePlus.offlinePlusExportMultipleRecordsAsync(ticket); + } catch (e) { + HAC_BatchUploadProgress.close(); + HAC_OfflineExportExecution.complete(); + throw e; + } } else { HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("批量导出离线表单", "调用offlinePlusExportMultipleRecordsAsync")); + HAC_BatchUploadProgress.close(); + HAC_OfflineExportExecution.complete(); } return; } diff --git a/Resources/OfflinePlusExportProgress.js b/Resources/OfflinePlusExportProgress.js new file mode 100644 index 0000000..2012c63 --- /dev/null +++ b/Resources/OfflinePlusExportProgress.js @@ -0,0 +1,504 @@ +/** + * 离线表单导出进度与执行状态组件。 + * 此文件必须先于 AndroidPDACommand.js 加载。 + */ +var HAC_OfflineExportExecution = { + isRunning: false, + begin: function () { + if (this.isRunning) { + alert("上一个导出动作尚未完成,请等待上一个导出完成。"); + return false; + } + this.isRunning = true; + return true; + }, + complete: function () { + this.isRunning = false; + } +}; +/** + * 离线附件上传进度显示组件 + */ +var HAC_OfflineUploadProgress = { + element: null, + panel: null, + collapseButton: null, + collapsedButton: null, + fill: null, + title: null, + detail: null, + speed: null, + preventScrollHandler: null, + collapseButtonHandler: null, + collapsedButtonHandler: null, + collapsed: false, + bodyOverflow: null, + documentOverflow: null, + ensure: function () { + if (this.element) { + return; + } + var root = document.createElement("div"); + root.style.cssText = "position:fixed;left:0;top:0;right:0;bottom:0;z-index:2147483647;background:rgba(0,0,0,.28);display:flex;align-items:center;justify-content:center;font-family:Arial,'Microsoft YaHei',sans-serif;touch-action:none;overscroll-behavior:contain;"; + root.innerHTML = "
" + + "
正在上传离线附件
" + + "
" + + "
" + + "
" + + "
" + + ""; + this.bodyOverflow = document.body.style.overflow; + this.documentOverflow = document.documentElement.style.overflow; + document.body.style.overflow = "hidden"; + document.documentElement.style.overflow = "hidden"; + this.preventScrollHandler = function (event) { + if (this.collapsed) { + return; + } + event.preventDefault(); + }.bind(this); + root.addEventListener("touchmove", this.preventScrollHandler, { passive: false }); + document.body.appendChild(root); + this.element = root; + this.panel = root.querySelector("[data-role='panel']"); + this.collapseButton = root.querySelector("[data-role='collapse']"); + this.collapsedButton = root.querySelector("[data-role='collapsed']"); + this.fill = root.querySelector("[data-role='fill']"); + this.title = root.querySelector("[data-role='title']"); + this.detail = root.querySelector("[data-role='detail']"); + this.speed = root.querySelector("[data-role='speed']"); + this.collapseButtonHandler = function () { + this.collapse(); + }.bind(this); + this.collapsedButtonHandler = function () { + this.expand(); + }.bind(this); + this.collapseButton.addEventListener("click", this.collapseButtonHandler); + this.collapsedButton.addEventListener("click", this.collapsedButtonHandler); + }, + lockPage: function () { + document.body.style.overflow = "hidden"; + document.documentElement.style.overflow = "hidden"; + }, + unlockPage: function () { + document.body.style.overflow = this.bodyOverflow || ""; + document.documentElement.style.overflow = this.documentOverflow || ""; + }, + collapse: function () { + this.ensure(); + this.collapsed = true; + this.panel.style.display = "none"; + this.collapsedButton.style.display = "block"; + this.element.style.background = "transparent"; + this.element.style.pointerEvents = "none"; + this.unlockPage(); + }, + expand: function () { + this.ensure(); + this.collapsed = false; + this.panel.style.display = "block"; + this.collapsedButton.style.display = "none"; + this.element.style.background = "rgba(0,0,0,.28)"; + this.element.style.pointerEvents = "auto"; + this.lockPage(); + }, + update: function (options) { + this.ensure(); + var percent = Math.max(0, Math.min(100, options.percent || 0)); + this.fill.style.width = percent.toFixed(1) + "%"; + this.title.textContent = options.title || "正在上传离线附件"; + this.detail.textContent = options.detail || ""; + this.speed.textContent = options.speed || ""; + this.collapsedButton.querySelector("[data-role='collapsed-title']").textContent = options.title || "正在上传离线附件"; + this.collapsedButton.querySelector("[data-role='collapsed-percent']").textContent = Math.floor(percent) + "%"; + }, + close: function () { + if (this.element && this.preventScrollHandler) { + this.element.removeEventListener("touchmove", this.preventScrollHandler); + } + if (this.collapseButton && this.collapseButtonHandler) { + this.collapseButton.removeEventListener("click", this.collapseButtonHandler); + } + if (this.collapsedButton && this.collapsedButtonHandler) { + this.collapsedButton.removeEventListener("click", this.collapsedButtonHandler); + } + if (this.element && this.element.parentNode) { + this.element.parentNode.removeChild(this.element); + } + this.unlockPage(); + this.element = null; + this.panel = null; + this.collapseButton = null; + this.collapsedButton = null; + this.fill = null; + this.title = null; + this.detail = null; + this.speed = null; + this.preventScrollHandler = null; + this.collapseButtonHandler = null; + this.collapsedButtonHandler = null; + this.collapsed = false; + this.bodyOverflow = null; + this.documentOverflow = null; + } +}; + +/** + * 批量离线附件上传进度显示组件 - 支持多行显示多个表单状态 + */ +var HAC_BatchUploadProgress = { + element: null, + panel: null, + collapseButton: null, + collapsedButton: null, + title: null, + countLabel: null, + listContainer: null, + summaryLabel: null, + currentTitle: null, + percentLabel: null, + fill: null, + metaLeft: null, + metaRight: null, + confirmButton: null, + itemMap: {}, + preventScrollHandler: null, + buttonClickHandler: null, + collapseButtonHandler: null, + collapsedButtonHandler: null, + confirmHandler: null, + collapsed: false, + bodyOverflow: null, + documentOverflow: null, + ensure: function (itemCount) { + if (this.element) { + return; + } + var root = document.createElement("div"); + root.style.cssText = "position:fixed;left:0;top:0;right:0;bottom:0;z-index:2147483647;background:#fff;display:flex;align-items:stretch;justify-content:center;font-family:Arial,'Microsoft YaHei',sans-serif;touch-action:none;overscroll-behavior:contain;box-sizing:border-box;"; + root.innerHTML = "" + + "
" + + "
" + + "
" + + "
批量导出离线表单
" + + "
批量上传已开始,状态会按处理进度实时更新。
" + + "
" + + "
" + + "
" + + "
" + + "正在导出:" + + "" + + "
" + + "0%" + + "
" + + "
" + + "
" + + "" + + "" + + "
" + + "
" + + "
" + + "
" + + "

单据列表

" + + "" + + "
" + + "
" + + "
" + + "
" + + "" + + "
" + + "
" + + "
" + + ""; + this.bodyOverflow = document.body.style.overflow; + this.documentOverflow = document.documentElement.style.overflow; + document.body.style.overflow = "hidden"; + document.documentElement.style.overflow = "hidden"; + this.preventScrollHandler = function (event) { + if (this.collapsed) { + return; + } + var list = root.querySelector("[data-role='list']"); + if (list && list.contains(event.target)) { + return; + } + event.preventDefault(); + }.bind(this); + root.addEventListener("touchmove", this.preventScrollHandler, { passive: false }); + document.body.appendChild(root); + this.element = root; + this.panel = root.querySelector("[data-role='panel']"); + this.collapseButton = root.querySelector("[data-role='collapse']"); + this.collapsedButton = root.querySelector("[data-role='collapsed']"); + this.title = root.querySelector("[data-role='title']"); + this.countLabel = root.querySelector("[data-role='count']"); + this.listContainer = root.querySelector("[data-role='list']"); + this.summaryLabel = root.querySelector("[data-role='summary-label']"); + this.currentTitle = root.querySelector("[data-role='current-title']"); + this.percentLabel = root.querySelector("[data-role='percent']"); + this.fill = root.querySelector("[data-role='fill']"); + this.metaLeft = root.querySelector("[data-role='meta-left']"); + this.metaRight = root.querySelector("[data-role='meta-right']"); + this.confirmButton = root.querySelector("[data-role='confirm']"); + this.buttonClickHandler = function () { + if (!this.confirmButton || this.confirmButton.disabled) { + return; + } + var confirmHandler = this.confirmHandler; + this.close(); + if (confirmHandler) { + confirmHandler(); + } + }.bind(this); + this.confirmButton.addEventListener("click", this.buttonClickHandler); + this.collapseButtonHandler = function () { + this.collapse(); + }.bind(this); + this.collapsedButtonHandler = function () { + this.expand(); + }.bind(this); + this.collapseButton.addEventListener("click", this.collapseButtonHandler); + this.collapsedButton.addEventListener("click", this.collapsedButtonHandler); + this.refreshCount(itemCount || 0); + this.updateSummary({ + percent: 0, + title: "准备导出", + metaLeft: "正在准备附件", + metaRight: "" + }); + }, + lockPage: function () { + document.body.style.overflow = "hidden"; + document.documentElement.style.overflow = "hidden"; + }, + unlockPage: function () { + document.body.style.overflow = this.bodyOverflow || ""; + document.documentElement.style.overflow = this.documentOverflow || ""; + }, + collapse: function () { + this.ensure(); + this.collapsed = true; + this.panel.style.display = "none"; + this.collapsedButton.style.display = "block"; + this.element.style.background = "transparent"; + this.element.style.pointerEvents = "none"; + this.unlockPage(); + }, + expand: function () { + this.ensure(); + this.collapsed = false; + this.panel.style.display = "flex"; + this.collapsedButton.style.display = "none"; + this.element.style.background = "#fff"; + this.element.style.pointerEvents = "auto"; + this.lockPage(); + }, + refreshCount: function (itemCount) { + if (!this.countLabel) { + return; + } + var count = itemCount || Object.keys(this.itemMap).length; + this.countLabel.textContent = "共 " + count + " 项"; + }, + renderStatus: function (statusIcon, state) { + if (!statusIcon) { + return; + } + switch (state) { + case 'processing': + statusIcon.style.color = "#2f7df6"; + statusIcon.innerHTML = ""; + break; + case 'completed': + statusIcon.style.color = "#52c41a"; + statusIcon.innerHTML = ""; + break; + case 'error': + statusIcon.style.color = "#ff4d4f"; + statusIcon.innerHTML = ""; + break; + default: + statusIcon.style.color = "#98a2b3"; + statusIcon.innerHTML = ""; + break; + } + }, + updateSummary: function (options) { + this.ensure(); + options = options || {}; + if (options.percent !== undefined) { + var percent = Math.max(0, Math.min(100, options.percent || 0)); + this.fill.style.width = percent.toFixed(1) + "%"; + this.percentLabel.textContent = Math.floor(percent) + "%"; + } + if (options.title !== undefined) { + this.currentTitle.textContent = options.title || "离线表单"; + this.collapsedButton.querySelector("[data-role='collapsed-title']").textContent = options.title || "离线表单"; + } + if (options.metaLeft !== undefined) { + this.metaLeft.textContent = options.metaLeft || ""; + } + if (options.metaRight !== undefined) { + this.metaRight.textContent = options.metaRight || ""; + } + if (options.label !== undefined) { + this.summaryLabel.textContent = options.label || ""; + } + if (options.percent !== undefined) { + this.collapsedButton.querySelector("[data-role='collapsed-percent']").textContent = Math.floor(percent) + "%"; + } + }, + finish: function (options) { + options = options || {}; + if (!this.element) { + if (options.onConfirm) { + options.onConfirm(); + } + return; + } + this.confirmHandler = options.onConfirm || null; + if (options.success === false) { + this.updateSummary({ + label: "导出结果:", + title: "处理完成", + metaLeft: "处理失败", + metaRight: options.error || "请查看失败项" + }); + } else { + this.updateSummary({ + label: "导出结果:", + percent: 100, + title: "处理完成", + metaLeft: "已完成全部动作", + metaRight: "可以关闭页面" + }); + } + if (this.confirmButton) { + this.confirmButton.disabled = false; + this.confirmButton.textContent = "确认"; + this.confirmButton.style.background = "#2f7df6"; + this.confirmButton.style.opacity = "1"; + this.confirmButton.style.cursor = "pointer"; + } + }, + /** + * 初始化表单项 + * @param {*} itemKey 表单项唯一标识 + * @param {*} itemTitle 表单显示标题 + */ + initItem: function (itemKey, itemTitle) { + this.ensure(); + if (this.itemMap[itemKey]) { + return; + } + + var itemDiv = document.createElement("div"); + itemDiv.style.cssText = "display:flex;align-items:center;justify-content:space-between;gap:18px;padding:17px 4px;border-bottom:1px solid #e4e8f0;background:#fff;"; + itemDiv.innerHTML = "
" + + "" + + "" + + "
" + + "
"; + + this.listContainer.appendChild(itemDiv); + this.itemMap[itemKey] = { + element: itemDiv, + statusIcon: itemDiv.querySelector("[data-role='status']"), + title: itemDiv.querySelector("[data-role='title']"), + progress: itemDiv.querySelector("[data-role='progress']"), + baseTitle: itemTitle || "离线表单", + state: 'pending' + }; + this.itemMap[itemKey].title.textContent = itemTitle || "离线表单"; + this.itemMap[itemKey].progress.textContent = "等待处理..."; + this.renderStatus(this.itemMap[itemKey].statusIcon, 'pending'); + this.refreshCount(); + }, + /** + * 更新表单项状态 + * @param {*} itemKey 表单项唯一标识 + * @param {*} options 状态选项 { state, progress, detail, titleSuffix } + */ + updateItem: function (itemKey, options) { + this.ensure(); + var item = this.itemMap[itemKey]; + if (!item) { + return; + } + + if (options.state) { + item.state = options.state; + this.renderStatus(item.statusIcon, options.state); + } + + if (options.detail !== undefined) { + item.progress.textContent = options.detail; + } + + if (options.titleSuffix !== undefined) { + item.title.textContent = item.baseTitle + options.titleSuffix; + } + + if (options.progress !== undefined) { + if (item.state !== 'completed' && item.state !== 'error') { + item.progress.textContent = "处理中: " + Math.floor(options.progress) + "%"; + } + } + }, + /** + * 标记表单项完成 + * @param {*} itemKey 表单项唯一标识 + */ + completeItem: function (itemKey) { + this.updateItem(itemKey, { state: 'completed', detail: '已完成' }); + }, + /** + * 标记表单项错误 + * @param {*} itemKey 表单项唯一标识 + * @param {*} error 错误信息 + */ + errorItem: function (itemKey, error) { + this.updateItem(itemKey, { state: 'error', detail: error || '处理失败' }); + }, + close: function () { + if (this.element && this.preventScrollHandler) { + this.element.removeEventListener("touchmove", this.preventScrollHandler); + } + if (this.confirmButton && this.buttonClickHandler) { + this.confirmButton.removeEventListener("click", this.buttonClickHandler); + } + if (this.collapseButton && this.collapseButtonHandler) { + this.collapseButton.removeEventListener("click", this.collapseButtonHandler); + } + if (this.collapsedButton && this.collapsedButtonHandler) { + this.collapsedButton.removeEventListener("click", this.collapsedButtonHandler); + } + if (this.element && this.element.parentNode) { + this.element.parentNode.removeChild(this.element); + } + this.unlockPage(); + this.element = null; + this.panel = null; + this.collapseButton = null; + this.collapsedButton = null; + this.title = null; + this.countLabel = null; + this.listContainer = null; + this.summaryLabel = null; + this.currentTitle = null; + this.percentLabel = null; + this.fill = null; + this.metaLeft = null; + this.metaRight = null; + this.confirmButton = null; + this.itemMap = {}; + this.preventScrollHandler = null; + this.buttonClickHandler = null; + this.collapseButtonHandler = null; + this.collapsedButtonHandler = null; + this.confirmHandler = null; + this.collapsed = false; + this.bodyOverflow = null; + this.documentOverflow = null; + } +}; -- Gitee From 23bbf072d668b37e6f709035790566566fa2f146 Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Wed, 19 Aug 2026 10:17:31 +0800 Subject: [PATCH 40/51] =?UTF-8?q?=E7=AD=BE=E5=90=8D=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OfflinePlusBuildPattern.cs | 8 +++++--- Resources/AndroidPDACommand.js | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/OfflinePlusBuildPattern.cs b/OfflinePlusBuildPattern.cs index e9ab83b..101e58e 100644 --- a/OfflinePlusBuildPattern.cs +++ b/OfflinePlusBuildPattern.cs @@ -84,8 +84,8 @@ namespace AndroidPDACommand public class OfflineFormNode : ObjectPropertyBase, ITreeNode { - public const string BaseItemTypeValueList = "textItem|passwordItem|selectItem|datePicker|timePicker|imageItem|fileItem"; - public const string BaseItemTypeDisplayList = "文本框|密码框|选择框|日期选择|时间选择|图片列表|文件上传"; + public const string BaseItemTypeValueList = "textItem|passwordItem|selectItem|datePicker|timePicker|imageItem|fileItem|signatureItem"; + public const string BaseItemTypeDisplayList = "文本框|密码框|选择框|日期选择|时间选择|图片列表|文件上传|签名"; public const string ItemTypeValueList = BaseItemTypeValueList + "|listItem"; public const string ItemTypeDisplayList = BaseItemTypeDisplayList + "|列表"; @@ -121,6 +121,7 @@ namespace AndroidPDACommand { "timePicker", CreateFieldVisibleProperties(nameof(Value), nameof(IncludeSeconds)) }, { "imageItem", CreateFieldVisibleProperties(nameof(MaxCount), nameof(AllowImageUpload), nameof(CompressionOptions), nameof(WatermarkOptions)) }, { "fileItem", CreateFieldVisibleProperties(nameof(FileItemConfig)) }, + { "signatureItem", CreateFieldVisibleProperties(nameof(Value), nameof(WatermarkOptions)) }, { "listItem", CreateFieldVisibleProperties(nameof(ListItemOptions)) } }; @@ -192,7 +193,8 @@ namespace AndroidPDACommand public ImageCompressionOptions CompressionOptions { get; set; } = new ImageCompressionOptions(); [ObjectProperty(ObjType = typeof(ImageWatermarkOptions))] - [DisplayName("拍照水印配置")] + [DisplayName("图片/签名水印配置")] + [Description("仅图片列表和签名类型生效。")] public ImageWatermarkOptions WatermarkOptions { get; set; } = new ImageWatermarkOptions(); [ObjectProperty(ObjType = typeof(FileItemConfig))] diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index d313aff..584d57e 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -3072,7 +3072,7 @@ function HAC_OfflinePlusCreateFormNodeBuilder(context) { return Object.keys(compression).length > 0 ? compression : undefined; } - function buildImageWatermark(options) { + function buildWatermark(options) { if (!options) { return undefined; } @@ -3196,7 +3196,7 @@ function HAC_OfflinePlusCreateFormNodeBuilder(context) { var options = {}; var maxCount = evaluate(node.MaxCount); var compression = buildImageCompression(node.CompressionOptions); - var watermark = buildImageWatermark(node.WatermarkOptions); + var watermark = buildWatermark(node.WatermarkOptions); if (maxCount !== undefined && maxCount !== null && maxCount !== "") { options.maxCount = maxCount; } @@ -3224,6 +3224,16 @@ function HAC_OfflinePlusCreateFormNodeBuilder(context) { } } }, + signatureItem: { + apply: function (field, node) { + var watermark = buildWatermark(node.WatermarkOptions); + if (watermark) { + field.options = { + watermark: watermark + }; + } + } + }, listItem: { apply: function (field, node) { var options = buildListItemOptions(node); @@ -3446,7 +3456,7 @@ function HAC_OfflinePlusValidatePatternJson(patternJson) { } itemIds[itemIdKey] = path + ".field.itemId"; - var itemTypes = ["textItem", "passwordItem", "selectItem", "datePicker", "timePicker", "imageItem", "fileItem", "listItem"]; + var itemTypes = ["textItem", "passwordItem", "selectItem", "datePicker", "timePicker", "imageItem", "fileItem", "signatureItem", "listItem"]; if (itemTypes.indexOf(field.itemType) < 0) { return fail(path + ".field.itemType", "是不支持的表单项类型"); } -- Gitee From 00f3ba3cc4c24fbb335bdf88609237c6b199d4d5 Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Thu, 20 Aug 2026 11:11:16 +0800 Subject: [PATCH 41/51] =?UTF-8?q?=E6=9C=80=E5=B0=8F=E5=8D=95=E4=BD=8D?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Resources/AndroidPDACommand.js | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 584d57e..f3538cd 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -3627,6 +3627,43 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { return Forguncy.Helper.SpecialPath.getFileDownloadUrl(fileId); } + function applyFieldUpdateTime(pattern) { + var now = new Date(); + var updateTime = (now.getTime() - now.getTimezoneOffset() * 60000) / 86400000 + 25569; + + function applyToNodes(nodes) { + if (!Array.isArray(nodes)) { + return; + } + + nodes.forEach(function (node) { + if (!node || typeof node !== "object") { + return; + } + + if (node.nodeType === "field" && node.field && typeof node.field === "object") { + node.field.updateTime = updateTime; + if (node.field.itemType === "listItem" && node.field.options) { + applyToNodes(node.field.options.children); + } + return; + } + + if (node.nodeType === "group") { + applyToNodes(node.children); + } + }); + } + + if (pattern && Array.isArray(pattern.steps)) { + pattern.steps.forEach(function (step) { + if (step && typeof step === "object") { + applyToNodes(step.items); + } + }); + } + } + var patternJson = evaluate(params.PatternJson); var validationResult = HAC_OfflinePlusValidatePatternJson(patternJson); if (!validationResult.isValid) { @@ -3660,6 +3697,7 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { } var manualPdfUrl = getManualPdfUrl(manualAttachmentValue); + applyFieldUpdateTime(patternParam); patternParam.ticket = ticket; finalPatternParamJson = JSON.stringify(patternParam, null, 2); -- Gitee From 3026dd6224fd823e8bbabfbf5e2fd969fe0bf6be Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Thu, 20 Aug 2026 16:10:55 +0800 Subject: [PATCH 42/51] =?UTF-8?q?=E7=AD=BE=E5=90=8D=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OfflinePlusBuildPattern.cs | 27 ++++++- Resources/AndroidPDACommand.js | 125 ++++++++++++++++++++++++++++++--- 2 files changed, 141 insertions(+), 11 deletions(-) diff --git a/OfflinePlusBuildPattern.cs b/OfflinePlusBuildPattern.cs index 101e58e..da8ea32 100644 --- a/OfflinePlusBuildPattern.cs +++ b/OfflinePlusBuildPattern.cs @@ -109,6 +109,7 @@ namespace AndroidPDACommand nameof(CompressionOptions), nameof(WatermarkOptions), nameof(FileItemConfig), + nameof(SignatureOptions), nameof(ListItemOptions) }; @@ -121,7 +122,7 @@ namespace AndroidPDACommand { "timePicker", CreateFieldVisibleProperties(nameof(Value), nameof(IncludeSeconds)) }, { "imageItem", CreateFieldVisibleProperties(nameof(MaxCount), nameof(AllowImageUpload), nameof(CompressionOptions), nameof(WatermarkOptions)) }, { "fileItem", CreateFieldVisibleProperties(nameof(FileItemConfig)) }, - { "signatureItem", CreateFieldVisibleProperties(nameof(Value), nameof(WatermarkOptions)) }, + { "signatureItem", CreateFieldVisibleProperties(nameof(Value), nameof(SignatureOptions)) }, { "listItem", CreateFieldVisibleProperties(nameof(ListItemOptions)) } }; @@ -193,14 +194,17 @@ namespace AndroidPDACommand public ImageCompressionOptions CompressionOptions { get; set; } = new ImageCompressionOptions(); [ObjectProperty(ObjType = typeof(ImageWatermarkOptions))] - [DisplayName("图片/签名水印配置")] - [Description("仅图片列表和签名类型生效。")] + [DisplayName("拍照水印配置")] public ImageWatermarkOptions WatermarkOptions { get; set; } = new ImageWatermarkOptions(); [ObjectProperty(ObjType = typeof(FileItemConfig))] [DisplayName("文件上传配置")] public FileItemConfig FileItemConfig { get; set; } = new FileItemConfig(); + [ObjectProperty(ObjType = typeof(SignatureItemOptions))] + [DisplayName("签名配置")] + public SignatureItemOptions SignatureOptions { get; set; } = new SignatureItemOptions(); + [ObjectProperty(ObjType = typeof(ListItemOptions))] [DisplayName("列表配置")] public ListItemOptions ListItemOptions { get; set; } = new ListItemOptions(); @@ -249,6 +253,7 @@ namespace AndroidPDACommand CompressionOptions = CompressionOptions, WatermarkOptions = WatermarkOptions, FileItemConfig = FileItemConfig, + SignatureOptions = SignatureOptions, ListItemOptions = ListItemOptions }; } @@ -303,6 +308,7 @@ namespace AndroidPDACommand CompressionOptions = CompressionOptions, WatermarkOptions = WatermarkOptions, FileItemConfig = FileItemConfig, + SignatureOptions = SignatureOptions, ListItemOptions = ListItemOptions }; } @@ -398,6 +404,21 @@ namespace AndroidPDACommand public object MaxFileSizeKb { get; set; } = 0; } + public class SignatureItemOptions : ObjectPropertyBase + { + [ListProperty] + [DisplayName("签名人员")] + [Description("按签名顺序配置人员,生成JSON中的options.users字符串数组。")] + public List Users { get; set; } = new List(); + } + + public class SignatureUser : ObjectPropertyBase + { + [FormulaProperty] + [DisplayName("人员")] + public object Name { get; set; } + } + public class ListItemOptions : ObjectPropertyBase { [FormulaProperty] diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index f3538cd..f6a624f 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -142,8 +142,88 @@ function HAC_UploadFileToTemp(file, uploadLimitId, onProgress) { xhr.send(formData); }); } +function HAC_GetOfflineRecordFieldValue(record, fieldId) { + var fieldValue = record && record.values && record.values[fieldId]; + if (fieldValue && typeof fieldValue === "object" && !Array.isArray(fieldValue) && + Object.prototype.hasOwnProperty.call(fieldValue, "value")) { + return fieldValue.value; + } + return fieldValue; +} + +function HAC_SetOfflineRecordFieldValue(record, fieldId, value) { + var fieldValue = record && record.values && record.values[fieldId]; + if (fieldValue && typeof fieldValue === "object" && !Array.isArray(fieldValue) && + Object.prototype.hasOwnProperty.call(fieldValue, "value")) { + // 只替换实际值,保留图片等字段的整体更新时间和其它元数据。 + fieldValue.value = value; + } else { + record.values[fieldId] = value; + } +} + +function HAC_ParseOfflineSignatureValues(record, fieldId) { + var fieldValue = HAC_GetOfflineRecordFieldValue(record, fieldId); + if (Array.isArray(fieldValue)) { + return fieldValue.slice(); + } + if (typeof fieldValue !== "string" || fieldValue.trim() === "") { + return []; + } + + try { + var values = JSON.parse(fieldValue); + return Array.isArray(values) ? values : []; + } catch (e) { + return []; + } +} + +function HAC_GetOfflineSignatureAttachmentIndex(record, attachment, fieldId) { + var path = attachment && attachment.path; + if (!Array.isArray(path) || path.length < 3) { + return -1; + } + + var signatureIndex = Number(path[2]); + if (!isFinite(signatureIndex) || signatureIndex < 0 || Math.floor(signatureIndex) !== signatureIndex) { + return -1; + } + + var signatureValues = HAC_ParseOfflineSignatureValues(record, fieldId); + var signatureValue = signatureValues[signatureIndex]; + var hasSignatureValue = signatureValue && typeof signatureValue === "object" && !Array.isArray(signatureValue) && + (Object.prototype.hasOwnProperty.call(signatureValue, "userName") || + Object.prototype.hasOwnProperty.call(signatureValue, "updateTime")); + var hasSignatureMetadata = attachment && attachment.updateTime !== undefined && attachment.updateTime !== null; + return hasSignatureValue || hasSignatureMetadata ? signatureIndex : -1; +} + +function HAC_ApplyUploadedSignatureValue(record, attachment, attachmentValue, fieldId, signatureIndex) { + var signatureValues = HAC_ParseOfflineSignatureValues(record, fieldId); + var oldSignatureValue = signatureValues[signatureIndex]; + var signatureValue = {}; + + if (oldSignatureValue && typeof oldSignatureValue === "object" && !Array.isArray(oldSignatureValue)) { + Object.keys(oldSignatureValue).forEach(function (key) { + signatureValue[key] = oldSignatureValue[key]; + }); + } + + if (!signatureValue.userName && attachment.originalName) { + signatureValue.userName = attachment.originalName; + } + signatureValue.fileName = attachmentValue; + if (attachment.updateTime !== undefined && attachment.updateTime !== null) { + signatureValue.updateTime = attachment.updateTime; + } + + signatureValues[signatureIndex] = signatureValue; + HAC_SetOfflineRecordFieldValue(record, fieldId, JSON.stringify(signatureValues)); +} + /** - * 将上传后的附件值应用到记录中,并更新记录的字段值 + * 将上传后的附件值应用到记录中。普通附件保留字段整体更新时间;签名附件额外保留每张签名的更新时间。 */ function HAC_ApplyUploadedAttachmentValue(record, attachment, attachmentValue, uploadedValues) { if (!record || !record.values || !attachment) { @@ -156,12 +236,18 @@ function HAC_ApplyUploadedAttachmentValue(record, attachment, attachmentValue, u return; } + var signatureIndex = HAC_GetOfflineSignatureAttachmentIndex(record, attachment, fieldId); + if (signatureIndex >= 0) { + HAC_ApplyUploadedSignatureValue(record, attachment, attachmentValue, fieldId, signatureIndex); + return; + } + var key = recordId + "|" + fieldId; if (!uploadedValues[key]) { uploadedValues[key] = []; } uploadedValues[key].push(attachmentValue); - record.values[fieldId] = uploadedValues[key].join("|"); + HAC_SetOfflineRecordFieldValue(record, fieldId, uploadedValues[key].join("|")); } function HAC_GetOfflineAttachmentRecordId(attachment) { @@ -300,7 +386,6 @@ async function HAC_ProcessBatchExportAttachments(exportDataArray, uploadLimitId) return exportDataArray; } - var uploadedValues = {}; var currentAttachmentIndex = 0; var completedAttachmentCount = 0; @@ -321,6 +406,7 @@ async function HAC_ProcessBatchExportAttachments(exportDataArray, uploadLimitId) var projectId = exportItem.projectId; var records = exportItem.records || []; var attachments = exportItem.attachments || []; + var uploadedValues = {}; var currentItemTitle = exportItem.projectId || ("离线表单 " + (itemIndex + 1)); if (!attachments || attachments.length === 0) { @@ -3072,7 +3158,7 @@ function HAC_OfflinePlusCreateFormNodeBuilder(context) { return Object.keys(compression).length > 0 ? compression : undefined; } - function buildWatermark(options) { + function buildImageWatermark(options) { if (!options) { return undefined; } @@ -3120,6 +3206,20 @@ function HAC_OfflinePlusCreateFormNodeBuilder(context) { return Object.keys(config).length > 0 ? config : undefined; } + function buildSignatureUsers(options) { + if (!options) { + return []; + } + + return getList(options.Users).map(function (user) { + return evaluate(user && user.Name); + }).filter(function (userName) { + return userName !== undefined && userName !== null && String(userName).trim() !== ""; + }).map(function (userName) { + return String(userName); + }); + } + function buildListItemOptions(node) { var options = {}; var listOptions = node.ListItemOptions || {}; @@ -3196,7 +3296,7 @@ function HAC_OfflinePlusCreateFormNodeBuilder(context) { var options = {}; var maxCount = evaluate(node.MaxCount); var compression = buildImageCompression(node.CompressionOptions); - var watermark = buildWatermark(node.WatermarkOptions); + var watermark = buildImageWatermark(node.WatermarkOptions); if (maxCount !== undefined && maxCount !== null && maxCount !== "") { options.maxCount = maxCount; } @@ -3226,10 +3326,10 @@ function HAC_OfflinePlusCreateFormNodeBuilder(context) { }, signatureItem: { apply: function (field, node) { - var watermark = buildWatermark(node.WatermarkOptions); - if (watermark) { + var users = buildSignatureUsers(node.SignatureOptions); + if (users.length > 0) { field.options = { - watermark: watermark + users: users }; } } @@ -3488,6 +3588,15 @@ function HAC_OfflinePlusValidatePatternJson(patternJson) { return fail(optionPath + ".label", "不能为空"); } } + } else if (field.itemType === "signatureItem") { + if (!Array.isArray(options.users) || options.users.length === 0) { + return fail(path + ".field.options.users", "必须是非空数组"); + } + for (var userIndex = 0; userIndex < options.users.length; userIndex++) { + if (typeof options.users[userIndex] !== "string" || isBlank(options.users[userIndex])) { + return fail(path + ".field.options.users[" + userIndex + "]", "必须是非空字符串"); + } + } } else if (field.itemType === "listItem") { if (!Array.isArray(options.children) || options.children.length === 0) { return fail(path + ".field.options.children", "必须是非空数组"); -- Gitee From 77d72abada00b6bc150767ceb8184beac2267aa6 Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Fri, 21 Aug 2026 10:17:34 +0800 Subject: [PATCH 43/51] =?UTF-8?q?=E6=8A=BD=E7=A6=BB=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E7=A6=BB=E7=BA=BF=E8=A1=A8=E5=8D=95=E8=AF=B4=E6=98=8EPDF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AndroidPDACommand.csproj | 1 + OfflinePlusAddPattern_Async.cs | 5 -- OfflinePlusDownloadManualPdf_Async.cs | 29 ++++++++++ Resources/AndroidPDACommand.js | 76 ++++++++++++++++++++------- 4 files changed, 87 insertions(+), 24 deletions(-) create mode 100644 OfflinePlusDownloadManualPdf_Async.cs diff --git a/AndroidPDACommand.csproj b/AndroidPDACommand.csproj index 0ec5be3..06ad00e 100644 --- a/AndroidPDACommand.csproj +++ b/AndroidPDACommand.csproj @@ -50,6 +50,7 @@ + diff --git a/OfflinePlusAddPattern_Async.cs b/OfflinePlusAddPattern_Async.cs index 8f7612d..75e7dc3 100644 --- a/OfflinePlusAddPattern_Async.cs +++ b/OfflinePlusAddPattern_Async.cs @@ -15,11 +15,6 @@ namespace AndroidPDACommand [Description("由“构建离线表单”命令生成的表单JSON。")] public object PatternJson { get; set; } - [FormulaProperty] - [DisplayName("手册附件单元格")] - [Description("选择保存 PDF 手册附件的单元格。下载离线表单时会把 PDF 手册随表单一起保存到离线设备。")] - public object ManualAttachmentCell { get; set; } - [FormulaProperty] [DisplayName("员工签名单元格")] [Description("选择保存员工签名附件的单元格。下载离线表单时会把签名文件随表单一起保存到离线设备。")] diff --git a/OfflinePlusDownloadManualPdf_Async.cs b/OfflinePlusDownloadManualPdf_Async.cs new file mode 100644 index 0000000..1c127ab --- /dev/null +++ b/OfflinePlusDownloadManualPdf_Async.cs @@ -0,0 +1,29 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using GrapeCity.Forguncy.Commands; +using GrapeCity.Forguncy.Plugin; + +namespace AndroidPDACommand +{ + [Icon("pack://application:,,,/AndroidPDACommand;component/Resources/Icon_PDF.png")] + [Category("活字格安卓容器(HAC)")] + public class OfflinePlusDownloadManualPdf_Async : BaseAsyncCommand + { + [Required] + [FormulaProperty] + [DisplayName("项目编号")] + [Description("需要绑定说明 PDF 的离线表单项目编号。")] + public object PatternId { get; set; } + + [Required] + [FormulaProperty] + [DisplayName("说明 PDF 附件单元格")] + [Description("选择保存说明 PDF 附件的单元格。APP 会下载该 PDF 并绑定到指定的离线表单项目。")] + public object ManualAttachmentCell { get; set; } + + public override string ToString() + { + return "下载离线表单说明PDF"; + } + } +} diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index f6a624f..d0f9f95 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -19,6 +19,7 @@ var HAC_GetOfflinePlusDebugInfo = function (commandName, stage) { "window.HAC.registryCallback:" + (hac && hac.registryCallback ? typeof hac.registryCallback : "undefined"), "window.offlinePlus:" + (offlinePlus ? typeof offlinePlus : "undefined"), "window.offlinePlus.offlinePlusAddPatternAsync:" + (offlinePlus && offlinePlus.offlinePlusAddPatternAsync ? typeof offlinePlus.offlinePlusAddPatternAsync : "undefined"), + "window.offlinePlus.offlinePlusDownloadManualPdfAsync:" + (offlinePlus && offlinePlus.offlinePlusDownloadManualPdfAsync ? typeof offlinePlus.offlinePlusDownloadManualPdfAsync : "undefined"), "window.offlinePlus.offlinePlusGetPatternsAsync:" + (offlinePlus && offlinePlus.offlinePlusGetPatternsAsync ? typeof offlinePlus.offlinePlusGetPatternsAsync : "undefined"), "location:" + window.location.href ].join("\r\n"); @@ -76,6 +77,20 @@ var HAC_ReturnToParam = function (OutParamaterName, data) { console.log("OutParamaterName was not set, the value is: " + JSON.stringify(data)); } }; + +var HAC_GetOfflinePlusManualPdfUrl = function (manualAttachmentValue) { + if (!manualAttachmentValue) { + return ""; + } + + var fileId = (manualAttachmentValue + "").split("|")[0]; + if (!fileId) { + console.warn("说明 PDF 附件单元格没有文件标识。", manualAttachmentValue); + return ""; + } + + return Forguncy.Helper.SpecialPath.getFileDownloadUrl(fileId); +}; /** * 将DataURL转换为File对象 * @param {*} dataUrl @@ -3722,20 +3737,6 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { return me.evaluateFormula(value); } - function getManualPdfUrl(manualValue) { - if (!manualValue) { - return ""; - } - - var fileId = (manualValue + "").split("|")[0]; - if (!fileId) { - console.warn("手册附件单元格没有文件标识。", manualValue); - return ""; - } - - return Forguncy.Helper.SpecialPath.getFileDownloadUrl(fileId); - } - function applyFieldUpdateTime(pattern) { var now = new Date(); var updateTime = (now.getTime() - now.getTimezoneOffset() * 60000) / 86400000 + 25569; @@ -3781,9 +3782,6 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { } var patternParam = validationResult.value; - var manualAttachmentValue = params.ManualAttachmentCell === undefined || params.ManualAttachmentCell === null || params.ManualAttachmentCell === "" - ? "" - : evaluate(params.ManualAttachmentCell); var signatureBase64 = params.SignatureAttachmentCell === undefined || params.SignatureAttachmentCell === null || params.SignatureAttachmentCell === "" ? "" : evaluate(params.SignatureAttachmentCell); @@ -3805,14 +3803,13 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { return; } - var manualPdfUrl = getManualPdfUrl(manualAttachmentValue); applyFieldUpdateTime(patternParam); patternParam.ticket = ticket; finalPatternParamJson = JSON.stringify(patternParam, null, 2); console.log(patternParam); if (window.offlinePlus && window.offlinePlus.offlinePlusAddPatternAsync) { - window.offlinePlus.offlinePlusAddPatternAsync(finalPatternParamJson, manualPdfUrl || "", signatureBase64 || ""); + window.offlinePlus.offlinePlusAddPatternAsync(finalPatternParamJson, "", signatureBase64 || ""); } else { HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("下载离线表单", "调用offlinePlusAddPatternAsync")); } @@ -3824,6 +3821,47 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { Forguncy.CommandFactory.registerCommand("AndroidPDACommand.OfflinePlusAddPattern_Async, AndroidPDACommand", OfflinePlusAddPattern_Async_Command); +var OfflinePlusDownloadManualPdf_Async_Command = (function (_super) { + __extends(OfflinePlusDownloadManualPdf_Async_Command, _super); + function OfflinePlusDownloadManualPdf_Async_Command() { + return _super !== null && _super.apply(this, arguments) || this; + } + + OfflinePlusDownloadManualPdf_Async_Command.prototype.execute = function () { + var me = this; + var params = this.CommandParam; + var patternId = this.evaluateFormula(params.PatternId); + var manualAttachmentValue = this.evaluateFormula(params.ManualAttachmentCell); + var manualPdfUrl = HAC_GetOfflinePlusManualPdfUrl(manualAttachmentValue); + + var ticket = HAC_GenerateCallbackTicket(me, function (payload, payload2) { + me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { + runTimePageName: me.CommandExecutingInfo.runTimePageName, + commandID: new Date().valueOf().toString(), + initParams: { + "IsSuccess": true, + "Error": "" + }, + locationString: "执行window.offlinePlus.offlinePlusDownloadManualPdfAsync" + }); + }, HAC_GetOfflinePlusDebugInfo("下载绑定说明PDF", "生成回调ticket")); + if (!ticket) { + console.error(HAC_GetOfflinePlusDebugInfo("下载绑定说明PDF", "生成回调ticket失败")); + return; + } + + if (window.offlinePlus && window.offlinePlus.offlinePlusDownloadManualPdfAsync) { + window.offlinePlus.offlinePlusDownloadManualPdfAsync(patternId, manualPdfUrl, ticket); + } else { + HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("下载绑定说明PDF", "调用offlinePlusDownloadManualPdfAsync")); + } + }; + + return OfflinePlusDownloadManualPdf_Async_Command; +}(Forguncy.CommandBase)); + +Forguncy.CommandFactory.registerCommand("AndroidPDACommand.OfflinePlusDownloadManualPdf_Async, AndroidPDACommand", OfflinePlusDownloadManualPdf_Async_Command); + var OfflinePlusGetPatterns_Async_Command = (function (_super) { __extends(OfflinePlusGetPatterns_Async_Command, _super); function OfflinePlusGetPatterns_Async_Command() { -- Gitee From 2a482e2b37e530d2c5878d704b72f45f9fccfb45 Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Fri, 21 Aug 2026 14:21:09 +0800 Subject: [PATCH 44/51] radio --- OfflinePlusBuildPattern.cs | 12 +++++- Resources/AndroidPDACommand.js | 75 ++++++++++++++++++++++++++-------- 2 files changed, 67 insertions(+), 20 deletions(-) diff --git a/OfflinePlusBuildPattern.cs b/OfflinePlusBuildPattern.cs index da8ea32..0497b77 100644 --- a/OfflinePlusBuildPattern.cs +++ b/OfflinePlusBuildPattern.cs @@ -84,8 +84,8 @@ namespace AndroidPDACommand public class OfflineFormNode : ObjectPropertyBase, ITreeNode { - public const string BaseItemTypeValueList = "textItem|passwordItem|selectItem|datePicker|timePicker|imageItem|fileItem|signatureItem"; - public const string BaseItemTypeDisplayList = "文本框|密码框|选择框|日期选择|时间选择|图片列表|文件上传|签名"; + public const string BaseItemTypeValueList = "textItem|passwordItem|selectItem|radioItem|datePicker|timePicker|imageItem|fileItem|signatureItem"; + public const string BaseItemTypeDisplayList = "文本框|密码框|选择框|单选框|日期选择|时间选择|图片列表|文件上传|签名"; public const string ItemTypeValueList = BaseItemTypeValueList + "|listItem"; public const string ItemTypeDisplayList = BaseItemTypeDisplayList + "|列表"; @@ -103,6 +103,7 @@ namespace AndroidPDACommand nameof(Value), nameof(CheckOptions), nameof(SelectOptionsList), + nameof(RadioDirection), nameof(IncludeSeconds), nameof(MaxCount), nameof(AllowImageUpload), @@ -118,6 +119,7 @@ namespace AndroidPDACommand { "textItem", CreateFieldVisibleProperties(nameof(Value), nameof(CheckOptions)) }, { "passwordItem", CreateFieldVisibleProperties(nameof(Value), nameof(CheckOptions)) }, { "selectItem", CreateFieldVisibleProperties(nameof(Value), nameof(SelectOptionsList)) }, + { "radioItem", CreateFieldVisibleProperties(nameof(Value), nameof(RadioDirection), nameof(SelectOptionsList)) }, { "datePicker", CreateFieldVisibleProperties(nameof(Value)) }, { "timePicker", CreateFieldVisibleProperties(nameof(Value), nameof(IncludeSeconds)) }, { "imageItem", CreateFieldVisibleProperties(nameof(MaxCount), nameof(AllowImageUpload), nameof(CompressionOptions), nameof(WatermarkOptions)) }, @@ -180,6 +182,10 @@ namespace AndroidPDACommand [DisplayName("选则项")] public List SelectOptionsList { get; set; } + [DisplayName("排列方向")] + [ComboProperty(ValueList = "vertical|horizontal", DisplayList = "垂直|水平")] + public string RadioDirection { get; set; } = "horizontal"; + [FormulaProperty] [DisplayName("图片最大数量")] [Description("0表示不限制图片数量。")] @@ -248,6 +254,7 @@ namespace AndroidPDACommand IncludeSeconds = IncludeSeconds, CheckOptions = CheckOptions, SelectOptionsList = SelectOptionsList, + RadioDirection = RadioDirection, MaxCount = MaxCount, AllowImageUpload = AllowImageUpload, CompressionOptions = CompressionOptions, @@ -303,6 +310,7 @@ namespace AndroidPDACommand IncludeSeconds = IncludeSeconds, CheckOptions = CheckOptions, SelectOptionsList = SelectOptionsList, + RadioDirection = RadioDirection, MaxCount = MaxCount, AllowImageUpload = AllowImageUpload, CompressionOptions = CompressionOptions, diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index d0f9f95..5716550 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -3297,6 +3297,22 @@ function HAC_OfflinePlusCreateFormNodeBuilder(context) { } } }, + radioItem: { + apply: function (field, node) { + var options = {}; + var direction = node.RadioDirection || "horizontal"; + var selectOptions = buildSelectOptions(node.SelectOptionsList); + if (direction) { + options.direction = direction; + } + if (selectOptions.length > 0) { + options.selectOptions = selectOptions; + } + if (Object.keys(options).length > 0) { + field.options = options; + } + } + }, timePicker: { apply: function (field, node) { if (node.IncludeSeconds) { @@ -3527,7 +3543,30 @@ function HAC_OfflinePlusValidatePatternJson(patternJson) { return null; } - function validateNode(node, path, itemIds) { + function validateSelectOptions(selectOptions, path) { + if (!Array.isArray(selectOptions) || selectOptions.length === 0) { + return fail(path, "必须是非空数组"); + } + for (var optionIndex = 0; optionIndex < selectOptions.length; optionIndex++) { + var optionPath = path + "[" + optionIndex + "]"; + if (!isObject(selectOptions[optionIndex])) { + return fail(optionPath, "必须是对象"); + } + if (isBlank(selectOptions[optionIndex].value)) { + return fail(optionPath + ".value", "不能为空"); + } + if (isBlank(selectOptions[optionIndex].label)) { + return fail(optionPath + ".label", "不能为空"); + } + } + return null; + } + + function validateNode(node, path, itemIds, allowListItem) { + if (allowListItem === undefined) { + allowListItem = true; + } + if (!isObject(node)) { return fail(path, "必须是对象"); } @@ -3542,7 +3581,7 @@ function HAC_OfflinePlusValidatePatternJson(patternJson) { return fail(path + ".children", "必须是数组"); } for (var groupIndex = 0; groupIndex < node.children.length; groupIndex++) { - var groupError = validateNode(node.children[groupIndex], path + ".children[" + groupIndex + "]", itemIds); + var groupError = validateNode(node.children[groupIndex], path + ".children[" + groupIndex + "]", itemIds, allowListItem); if (groupError) { return groupError; } @@ -3571,7 +3610,10 @@ function HAC_OfflinePlusValidatePatternJson(patternJson) { } itemIds[itemIdKey] = path + ".field.itemId"; - var itemTypes = ["textItem", "passwordItem", "selectItem", "datePicker", "timePicker", "imageItem", "fileItem", "signatureItem", "listItem"]; + var itemTypes = ["textItem", "passwordItem", "selectItem", "radioItem", "datePicker", "timePicker", "imageItem", "fileItem", "signatureItem"]; + if (allowListItem) { + itemTypes.push("listItem"); + } if (itemTypes.indexOf(field.itemType) < 0) { return fail(path + ".field.itemType", "是不支持的表单项类型"); } @@ -3588,20 +3630,17 @@ function HAC_OfflinePlusValidatePatternJson(patternJson) { var options = field.options || {}; var numberError; if (field.itemType === "selectItem") { - if (!Array.isArray(options.selectOptions) || options.selectOptions.length === 0) { - return fail(path + ".field.options.selectOptions", "必须是非空数组"); + var selectError = validateSelectOptions(options.selectOptions, path + ".field.options.selectOptions"); + if (selectError) { + return selectError; } - for (var optionIndex = 0; optionIndex < options.selectOptions.length; optionIndex++) { - var optionPath = path + ".field.options.selectOptions[" + optionIndex + "]"; - if (!isObject(options.selectOptions[optionIndex])) { - return fail(optionPath, "必须是对象"); - } - if (isBlank(options.selectOptions[optionIndex].value)) { - return fail(optionPath + ".value", "不能为空"); - } - if (isBlank(options.selectOptions[optionIndex].label)) { - return fail(optionPath + ".label", "不能为空"); - } + } else if (field.itemType === "radioItem") { + if (options.direction !== undefined && ["horizontal", "vertical"].indexOf(options.direction) < 0) { + return fail(path + ".field.options.direction", "必须是horizontal或vertical"); + } + var radioError = validateSelectOptions(options.selectOptions, path + ".field.options.selectOptions"); + if (radioError) { + return radioError; } } else if (field.itemType === "signatureItem") { if (!Array.isArray(options.users) || options.users.length === 0) { @@ -3623,7 +3662,7 @@ function HAC_OfflinePlusValidatePatternJson(patternJson) { return numberError; } for (var listIndex = 0; listIndex < options.children.length; listIndex++) { - var listError = validateNode(options.children[listIndex], path + ".field.options.children[" + listIndex + "]", itemIds); + var listError = validateNode(options.children[listIndex], path + ".field.options.children[" + listIndex + "]", itemIds, false); if (listError) { return listError; } @@ -3689,7 +3728,7 @@ function HAC_OfflinePlusValidatePatternJson(patternJson) { return fail(stepPath + ".items", "必须是非空数组"); } for (var itemIndex = 0; itemIndex < step.items.length; itemIndex++) { - var itemError = validateNode(step.items[itemIndex], stepPath + ".items[" + itemIndex + "]", itemIds); + var itemError = validateNode(step.items[itemIndex], stepPath + ".items[" + itemIndex + "]", itemIds, true); if (itemError) { return itemError; } -- Gitee From ab93e01940ea1fb31708f2e9581c62b767322ff6 Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Fri, 21 Aug 2026 17:38:13 +0800 Subject: [PATCH 45/51] 1.23.1build --- PluginConfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluginConfig.json b/PluginConfig.json index 1775e55..5829942 100644 --- a/PluginConfig.json +++ b/PluginConfig.json @@ -13,7 +13,7 @@ "name": "PDA(Android)交互命令", "pluginType": "command", "guid": "{16B8DF69-1379-41C1-9194-BE89BAF8BB32}", - "version": "1.23.0.0", + "version": "1.23.1.0", "dependenceVersion": "8.0.0.0", "bundleJavaScript": true, "bundleCSS": true -- Gitee From a9467c2b6d07a1a83b98f5509a56bf8d9c272c9c Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Tue, 1 Sep 2026 13:50:43 +0800 Subject: [PATCH 46/51] =?UTF-8?q?=E7=AD=BE=E5=90=8D=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OfflinePlusBuildPattern.cs | 27 +++++++++++++++++++++++++++ PluginConfig.json | 2 +- Resources/AndroidPDACommand.js | 23 ++++++++++++++++++++++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/OfflinePlusBuildPattern.cs b/OfflinePlusBuildPattern.cs index 0497b77..83339f7 100644 --- a/OfflinePlusBuildPattern.cs +++ b/OfflinePlusBuildPattern.cs @@ -414,10 +414,37 @@ namespace AndroidPDACommand public class SignatureItemOptions : ObjectPropertyBase { + [DisplayName("使用人员数组公式")] + [Description("启用后直接使用下面公式返回的字符串数组作为签名人员。")] + public bool UseUsersFormula { get; set; } + [ListProperty] [DisplayName("签名人员")] [Description("按签名顺序配置人员,生成JSON中的options.users字符串数组。")] public List Users { get; set; } = new List(); + + [FormulaProperty] + [DisplayName("人员数组公式")] + [Description("返回签名人员字符串数组,例如:[\"张三\",\"李四\"]。")] + public object UsersFormula { get; set; } + + [FormulaProperty] + [DisplayName("免责声明")] + [Description("签名前展示的确认说明,生成JSON中的options.disclaimer。")] + public object Disclaimer { get; set; } + + public override bool GetDesignerPropertyVisible(string propertyName) + { + switch (propertyName) + { + case nameof(Users): + return !UseUsersFormula; + case nameof(UsersFormula): + return UseUsersFormula; + default: + return base.GetDesignerPropertyVisible(propertyName); + } + } } public class SignatureUser : ObjectPropertyBase diff --git a/PluginConfig.json b/PluginConfig.json index 5829942..85f3a6f 100644 --- a/PluginConfig.json +++ b/PluginConfig.json @@ -13,7 +13,7 @@ "name": "PDA(Android)交互命令", "pluginType": "command", "guid": "{16B8DF69-1379-41C1-9194-BE89BAF8BB32}", - "version": "1.23.1.0", + "version": "1.23.2.0", "dependenceVersion": "8.0.0.0", "bundleJavaScript": true, "bundleCSS": true diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 5716550..a8a36ad 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -3226,6 +3226,19 @@ function HAC_OfflinePlusCreateFormNodeBuilder(context) { return []; } + if (options.UseUsersFormula) { + return getList(evaluate(options.UsersFormula)).map(function (user) { + if (user && typeof user === "object" && !Array.isArray(user)) { + return evaluate(user.Name); + } + return evaluate(user); + }).filter(function (userName) { + return userName !== undefined && userName !== null && String(userName).trim() !== ""; + }).map(function (userName) { + return String(userName); + }); + } + return getList(options.Users).map(function (user) { return evaluate(user && user.Name); }).filter(function (userName) { @@ -3357,11 +3370,16 @@ function HAC_OfflinePlusCreateFormNodeBuilder(context) { }, signatureItem: { apply: function (field, node) { - var users = buildSignatureUsers(node.SignatureOptions); + var signatureOptions = node.SignatureOptions || {}; + var users = buildSignatureUsers(signatureOptions); + var disclaimer = evaluate(signatureOptions.Disclaimer); if (users.length > 0) { field.options = { users: users }; + if (disclaimer !== undefined && disclaimer !== null) { + field.options.disclaimer = String(disclaimer); + } } } }, @@ -3651,6 +3669,9 @@ function HAC_OfflinePlusValidatePatternJson(patternJson) { return fail(path + ".field.options.users[" + userIndex + "]", "必须是非空字符串"); } } + if (options.disclaimer !== undefined && typeof options.disclaimer !== "string") { + return fail(path + ".field.options.disclaimer", "必须是字符串"); + } } else if (field.itemType === "listItem") { if (!Array.isArray(options.children) || options.children.length === 0) { return fail(path + ".field.options.children", "必须是非空数组"); -- Gitee From 9fe9afdd4e743e869662e3837cd40f9315fd9480 Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Thu, 3 Sep 2026 16:23:27 +0800 Subject: [PATCH 47/51] =?UTF-8?q?=E5=A1=AB=E6=8A=A5=E8=BF=9B=E5=BA=A6?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OfflinePlusBuildPattern.cs | 16 ++++++++++++ Resources/AndroidPDACommand.js | 48 ++++++++++++++++++++++------------ 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/OfflinePlusBuildPattern.cs b/OfflinePlusBuildPattern.cs index 83339f7..43313cd 100644 --- a/OfflinePlusBuildPattern.cs +++ b/OfflinePlusBuildPattern.cs @@ -145,6 +145,14 @@ namespace AndroidPDACommand [DisplayName("默认折叠")] public bool DefaultCollapsed { get; set; } + [DisplayName("启用填报进度")] + [Description("仅步骤中的最外层节点生效。启用后该节点作为一个填报进度统计单位。")] + public bool EnableProgress { get; set; } + + [DisplayName("填报进度标识项")] + [Description("仅最外层分组的直接表单项生效。启用后该表单项作为该填报单位的完成标识。")] + public bool ProgressIdentifier { get; set; } + [FormulaProperty] [DisplayName("编号")] [Description("该表单项的唯一标识,全局唯一,无论是同一项目内,还是跨项目,都必须唯一,更建议使用项目编号作为前缀拼接;例如项目为Project01,则该表单项编号为Project01_Name")] @@ -227,6 +235,10 @@ namespace AndroidPDACommand return isText; case nameof(DefaultCollapsed): return isGroup; + case nameof(EnableProgress): + return isGroup; + case nameof(ProgressIdentifier): + return isField; default: if (FieldTypeControlledProperties.Contains(propertyName)) { @@ -245,6 +257,8 @@ namespace AndroidPDACommand NodeType = NodeType, Content = Content, DefaultCollapsed = DefaultCollapsed, + EnableProgress = EnableProgress, + ProgressIdentifier = ProgressIdentifier, ItemId = ItemId, ItemType = ItemType, Title = Title, @@ -301,6 +315,8 @@ namespace AndroidPDACommand NodeType = NodeType, Content = Content, DefaultCollapsed = DefaultCollapsed, + EnableProgress = EnableProgress, + ProgressIdentifier = ProgressIdentifier, ItemId = ItemId, ItemType = ItemType, Title = Title, diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index a8a36ad..cc739f4 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -3417,7 +3417,8 @@ function HAC_OfflinePlusCreateFormNodeBuilder(context) { return field; } - function buildNode(node) { + function buildNode(node, options) { + options = options || {}; var nodeType = node.NodeType || "group"; var children = getList(node.Children); var isListField = nodeType === "field" && node.ItemType === "listItem"; @@ -3426,38 +3427,47 @@ function HAC_OfflinePlusCreateFormNodeBuilder(context) { console.warn("离线表单节点不是分组,但包含子节点。", node); } + var result; if (nodeType === "field") { - return { + result = { nodeType: "field", title: node.Text, field: buildField(node) }; - } - - if (nodeType === "text") { - return { + } else if (nodeType === "text") { + result = { nodeType: "text", title: node.Text, content: evaluate(node.Content) }; + } else { + result = { + nodeType: "group", + title: node.Text, + defaultCollapsed: !!node.DefaultCollapsed, + children: buildNodes(children, { + allowProgressIdentifier: !!options.isStepRoot && !!node.EnableProgress + }) + }; } - return { - nodeType: "group", - title: node.Text, - defaultCollapsed: !!node.DefaultCollapsed, - children: buildNodes(children) - }; + if (options.isStepRoot && nodeType === "group" && node.EnableProgress) { + result.enableProgress = true; + } + if (nodeType === "field" && (options.isStepRoot || options.allowProgressIdentifier) && node.ProgressIdentifier) { + result.progressIdentifier = true; + } + return result; } - function buildNodes(nodes) { + function buildNodes(nodes, options) { return getList(nodes).map(function (node) { - return buildNode(node); + return buildNode(node, options); }); } function buildFormItem(nodes) { - var items = buildNodes(nodes); + var items = buildNodes(nodes, { isStepRoot: true }); if (items.length <= 1) { return items[0]; } @@ -3515,7 +3525,7 @@ var OfflinePlusBuildPattern_Command = (function (_super) { return { stepId: formNodeBuilder.evaluate(step.StepId), title: formNodeBuilder.evaluate(step.Title), - items: step.UseItemsFormula ? formNodeBuilder.evaluate(step.ItemsFormula) : formNodeBuilder.buildNodes(step.Items) + items: step.UseItemsFormula ? formNodeBuilder.evaluate(step.ItemsFormula) : formNodeBuilder.buildNodes(step.Items, { isStepRoot: true }) }; }); } @@ -3593,6 +3603,12 @@ function HAC_OfflinePlusValidatePatternJson(patternJson) { if (["group", "text", "field"].indexOf(nodeType) < 0) { return fail(path + ".nodeType", "必须是group、text或field"); } + if (node.enableProgress !== undefined && typeof node.enableProgress !== "boolean") { + return fail(path + ".enableProgress", "必须是布尔值"); + } + if (node.progressIdentifier !== undefined && typeof node.progressIdentifier !== "boolean") { + return fail(path + ".progressIdentifier", "必须是布尔值"); + } if (nodeType === "group") { if (!Array.isArray(node.children)) { -- Gitee From 41d2bfa22f49a4a03f350ac20a07a833e37daf1e Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Fri, 4 Sep 2026 10:59:18 +0800 Subject: [PATCH 48/51] =?UTF-8?q?=E5=A1=AB=E6=8A=A5=E8=BF=9B=E5=BA=A6?= =?UTF-8?q?=E5=8F=AF=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OfflinePlusBuildPattern.cs | 37 +++++++++++++++++++++++- Resources/AndroidPDACommand.js | 52 ++++++++++++++++++++++++++++++---- 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/OfflinePlusBuildPattern.cs b/OfflinePlusBuildPattern.cs index 43313cd..a31350c 100644 --- a/OfflinePlusBuildPattern.cs +++ b/OfflinePlusBuildPattern.cs @@ -142,6 +142,25 @@ namespace AndroidPDACommand [DisplayName("说明内容")] public object Content { get; set; } + [DisplayName("文本类型")] + [ComboProperty(ValueList = "plain|progress|document", DisplayList = "普通文本|填报进度|说明文档")] + public string TextMode { get; set; } = "plain"; + + [DefaultValue(true)] + [BoolProperty(IndentLevel = 1)] + [DisplayName("总填报项")] + public bool ShowProgressTotal { get; set; } = true; + + [DefaultValue(true)] + [BoolProperty(IndentLevel = 1)] + [DisplayName("已填报项")] + public bool ShowProgressCompleted { get; set; } = true; + + [DefaultValue(true)] + [BoolProperty(IndentLevel = 1)] + [DisplayName("完成率")] + public bool ShowProgressCompletionRate { get; set; } = true; + [DisplayName("默认折叠")] public bool DefaultCollapsed { get; set; } @@ -228,11 +247,19 @@ namespace AndroidPDACommand var isField = NodeType == "field"; var isText = NodeType == "text"; var isGroup = NodeType == "group"; + var isTextProgress = isText && TextMode == "progress"; + var isTextPlain = isText && TextMode != "progress" && TextMode != "document"; switch (propertyName) { - case nameof(Content): + case nameof(TextMode): return isText; + case nameof(Content): + return isTextPlain; + case nameof(ShowProgressTotal): + case nameof(ShowProgressCompleted): + case nameof(ShowProgressCompletionRate): + return isTextProgress; case nameof(DefaultCollapsed): return isGroup; case nameof(EnableProgress): @@ -256,6 +283,10 @@ namespace AndroidPDACommand Children = Children == null ? new List() : Children.ToList(), NodeType = NodeType, Content = Content, + TextMode = TextMode, + ShowProgressTotal = ShowProgressTotal, + ShowProgressCompleted = ShowProgressCompleted, + ShowProgressCompletionRate = ShowProgressCompletionRate, DefaultCollapsed = DefaultCollapsed, EnableProgress = EnableProgress, ProgressIdentifier = ProgressIdentifier, @@ -314,6 +345,10 @@ namespace AndroidPDACommand Children = Children == null ? new List() : Children.ToList(), NodeType = NodeType, Content = Content, + TextMode = TextMode, + ShowProgressTotal = ShowProgressTotal, + ShowProgressCompleted = ShowProgressCompleted, + ShowProgressCompletionRate = ShowProgressCompletionRate, DefaultCollapsed = DefaultCollapsed, EnableProgress = EnableProgress, ProgressIdentifier = ProgressIdentifier, diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index cc739f4..251544d 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -3435,11 +3435,32 @@ function HAC_OfflinePlusCreateFormNodeBuilder(context) { field: buildField(node) }; } else if (nodeType === "text") { - result = { - nodeType: "text", - title: node.Text, - content: evaluate(node.Content) - }; + var textMode = node.TextMode || "plain"; + if (textMode === "progress") { + result = { + nodeType: "text", + title: node.Text, + textMode: "progress", + progressDisplay: { + total: !!node.ShowProgressTotal, + completed: !!node.ShowProgressCompleted, + completionRate: !!node.ShowProgressCompletionRate + } + }; + } else if (textMode === "document") { + result = { + nodeType: "text", + title: node.Text, + textMode: "document" + }; + } else { + // Preserve the existing JSON shape for the default text mode. + result = { + nodeType: "text", + title: node.Text, + content: evaluate(node.Content) + }; + } } else { result = { nodeType: "group", @@ -3624,6 +3645,27 @@ function HAC_OfflinePlusValidatePatternJson(patternJson) { } if (nodeType === "text") { + var textMode = node.textMode || "plain"; + if (["plain", "progress", "document"].indexOf(textMode) < 0) { + return fail(path + ".textMode", "必须是plain、progress或document"); + } + if (textMode === "progress") { + if (!isObject(node.progressDisplay)) { + return fail(path + ".progressDisplay", "必须是对象"); + } + var progressDisplay = node.progressDisplay; + var progressDisplayProperties = ["total", "completed", "completionRate"]; + for (var progressDisplayIndex = 0; progressDisplayIndex < progressDisplayProperties.length; progressDisplayIndex++) { + var progressDisplayProperty = progressDisplayProperties[progressDisplayIndex]; + if (typeof progressDisplay[progressDisplayProperty] !== "boolean") { + return fail(path + ".progressDisplay." + progressDisplayProperty, "必须是布尔值"); + } + } + return null; + } + if (textMode === "document") { + return null; + } if (node.content === undefined || node.content === null) { return fail(path + ".content", "缺少说明内容"); } -- Gitee From ea126600f06db81ba7bd17a33bd78063b8499cde Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Mon, 7 Sep 2026 09:08:35 +0800 Subject: [PATCH 49/51] =?UTF-8?q?=E6=97=A5=E6=9C=9F=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OfflinePlusBuildPattern.cs | 9 ++++++++- Resources/AndroidPDACommand.js | 17 ++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/OfflinePlusBuildPattern.cs b/OfflinePlusBuildPattern.cs index a31350c..5cc270f 100644 --- a/OfflinePlusBuildPattern.cs +++ b/OfflinePlusBuildPattern.cs @@ -104,6 +104,7 @@ namespace AndroidPDACommand nameof(CheckOptions), nameof(SelectOptionsList), nameof(RadioDirection), + nameof(IncludeTime), nameof(IncludeSeconds), nameof(MaxCount), nameof(AllowImageUpload), @@ -120,7 +121,7 @@ namespace AndroidPDACommand { "passwordItem", CreateFieldVisibleProperties(nameof(Value), nameof(CheckOptions)) }, { "selectItem", CreateFieldVisibleProperties(nameof(Value), nameof(SelectOptionsList)) }, { "radioItem", CreateFieldVisibleProperties(nameof(Value), nameof(RadioDirection), nameof(SelectOptionsList)) }, - { "datePicker", CreateFieldVisibleProperties(nameof(Value)) }, + { "datePicker", CreateFieldVisibleProperties(nameof(Value), nameof(IncludeTime)) }, { "timePicker", CreateFieldVisibleProperties(nameof(Value), nameof(IncludeSeconds)) }, { "imageItem", CreateFieldVisibleProperties(nameof(MaxCount), nameof(AllowImageUpload), nameof(CompressionOptions), nameof(WatermarkOptions)) }, { "fileItem", CreateFieldVisibleProperties(nameof(FileItemConfig)) }, @@ -196,6 +197,10 @@ namespace AndroidPDACommand [DisplayName("默认值")] public object Value { get; set; } + [DisplayName("包含时间")] + [Description("仅日期选择类型生效。启用后可同时选择日期和时间。")] + public bool IncludeTime { get; set; } + [DisplayName("包含秒")] [Description("仅时间选择类型生效。启用后可选择时分秒;关闭时使用系统原生时分选择器。")] public bool IncludeSeconds { get; set; } @@ -296,6 +301,7 @@ namespace AndroidPDACommand Hint = Hint, Required = Required, Value = Value, + IncludeTime = IncludeTime, IncludeSeconds = IncludeSeconds, CheckOptions = CheckOptions, SelectOptionsList = SelectOptionsList, @@ -358,6 +364,7 @@ namespace AndroidPDACommand Hint = Hint, Required = Required, Value = Value, + IncludeTime = IncludeTime, IncludeSeconds = IncludeSeconds, CheckOptions = CheckOptions, SelectOptionsList = SelectOptionsList, diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index 251544d..ea28336 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -3326,6 +3326,13 @@ function HAC_OfflinePlusCreateFormNodeBuilder(context) { } } }, + datePicker: { + apply: function (field, node) { + field.options = { + includeTime: !!node.IncludeTime + }; + } + }, timePicker: { apply: function (field, node) { if (node.IncludeSeconds) { @@ -3705,7 +3712,15 @@ function HAC_OfflinePlusValidatePatternJson(patternJson) { var options = field.options || {}; var numberError; - if (field.itemType === "selectItem") { + if (field.itemType === "datePicker") { + if (options.includeTime !== undefined && typeof options.includeTime !== "boolean") { + return fail(path + ".field.options.includeTime", "必须是布尔值"); + } + } else if (field.itemType === "timePicker") { + if (options.includeSeconds !== undefined && typeof options.includeSeconds !== "boolean") { + return fail(path + ".field.options.includeSeconds", "必须是布尔值"); + } + } else if (field.itemType === "selectItem") { var selectError = validateSelectOptions(options.selectOptions, path + ".field.options.selectOptions"); if (selectError) { return selectError; -- Gitee From 5e8c07094c634fec91f6a12f683930fd97fed990 Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Tue, 8 Sep 2026 10:43:01 +0800 Subject: [PATCH 50/51] =?UTF-8?q?=E5=A4=9A=E8=AF=B4=E6=98=8E=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OfflinePlusDownloadManualPdf_Async.cs | 2 +- Resources/AndroidPDACommand.js | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/OfflinePlusDownloadManualPdf_Async.cs b/OfflinePlusDownloadManualPdf_Async.cs index 1c127ab..2930c44 100644 --- a/OfflinePlusDownloadManualPdf_Async.cs +++ b/OfflinePlusDownloadManualPdf_Async.cs @@ -18,7 +18,7 @@ namespace AndroidPDACommand [Required] [FormulaProperty] [DisplayName("说明 PDF 附件单元格")] - [Description("选择保存说明 PDF 附件的单元格。APP 会下载该 PDF 并绑定到指定的离线表单项目。")] + [Description("选择保存说明 PDF 附件的单元格。多个附件名称请使用英文竖线(|)分隔,APP 会下载全部 PDF 并绑定到指定的离线表单项目。")] public object ManualAttachmentCell { get; set; } public override string ToString() diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index ea28336..bd75ca9 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -80,16 +80,16 @@ var HAC_ReturnToParam = function (OutParamaterName, data) { var HAC_GetOfflinePlusManualPdfUrl = function (manualAttachmentValue) { if (!manualAttachmentValue) { - return ""; - } - - var fileId = (manualAttachmentValue + "").split("|")[0]; - if (!fileId) { - console.warn("说明 PDF 附件单元格没有文件标识。", manualAttachmentValue); - return ""; + return []; } - return Forguncy.Helper.SpecialPath.getFileDownloadUrl(fileId); + return (manualAttachmentValue + "").split("|").map(function (fileId) { + return fileId.trim(); + }).filter(function (fileId) { + return fileId !== ""; + }).map(function (fileId) { + return Forguncy.Helper.SpecialPath.getFileDownloadUrl(fileId); + }); }; /** * 将DataURL转换为File对象 -- Gitee From 88987d609b41db80577acb4597ff4180a3edbb2f Mon Sep 17 00:00:00 2001 From: NathanGuo Date: Tue, 8 Sep 2026 13:31:49 +0800 Subject: [PATCH 51/51] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OfflinePlusAddPattern_Async.cs | 5 ----- OfflinePlusDownloadManualPdf_Async.cs | 2 +- Resources/AndroidPDACommand.js | 7 +------ 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/OfflinePlusAddPattern_Async.cs b/OfflinePlusAddPattern_Async.cs index 75e7dc3..e43cbe2 100644 --- a/OfflinePlusAddPattern_Async.cs +++ b/OfflinePlusAddPattern_Async.cs @@ -15,11 +15,6 @@ namespace AndroidPDACommand [Description("由“构建离线表单”命令生成的表单JSON。")] public object PatternJson { get; set; } - [FormulaProperty] - [DisplayName("员工签名单元格")] - [Description("选择保存员工签名附件的单元格。下载离线表单时会把签名文件随表单一起保存到离线设备。")] - public object SignatureAttachmentCell { get; set; } - public override string ToString() { return "下载离线表单"; diff --git a/OfflinePlusDownloadManualPdf_Async.cs b/OfflinePlusDownloadManualPdf_Async.cs index 2930c44..0e6bd74 100644 --- a/OfflinePlusDownloadManualPdf_Async.cs +++ b/OfflinePlusDownloadManualPdf_Async.cs @@ -17,7 +17,7 @@ namespace AndroidPDACommand [Required] [FormulaProperty] - [DisplayName("说明 PDF 附件单元格")] + [DisplayName("说明 PDF 附件")] [Description("选择保存说明 PDF 附件的单元格。多个附件名称请使用英文竖线(|)分隔,APP 会下载全部 PDF 并绑定到指定的离线表单项目。")] public object ManualAttachmentCell { get; set; } diff --git a/Resources/AndroidPDACommand.js b/Resources/AndroidPDACommand.js index bd75ca9..920a28c 100644 --- a/Resources/AndroidPDACommand.js +++ b/Resources/AndroidPDACommand.js @@ -3915,10 +3915,6 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { } var patternParam = validationResult.value; - var signatureBase64 = params.SignatureAttachmentCell === undefined || params.SignatureAttachmentCell === null || params.SignatureAttachmentCell === "" - ? "" - : evaluate(params.SignatureAttachmentCell); - let ticket = HAC_GenerateCallbackTicket(me, function (payload, payload2) { // alert("离线表单项目设置成功\r\n\r\nJSON Schema:\r\n" + finalPatternParamJson); me.CommandExecutor.excuteCommand(me.CommandParam.CommandList, { @@ -3942,7 +3938,7 @@ var OfflinePlusAddPattern_Async_Command = (function (_super) { console.log(patternParam); if (window.offlinePlus && window.offlinePlus.offlinePlusAddPatternAsync) { - window.offlinePlus.offlinePlusAddPatternAsync(finalPatternParamJson, "", signatureBase64 || ""); + window.offlinePlus.offlinePlusAddPatternAsync(finalPatternParamJson); } else { HAC_CallFunctionOutOfHAC(HAC_GetOfflinePlusDebugInfo("下载离线表单", "调用offlinePlusAddPatternAsync")); } @@ -4278,4 +4274,3 @@ var OfflinePlusGetPatterns_Async_Command = (function (_super) { }(Forguncy.CommandBase)); Forguncy.CommandFactory.registerCommand("AndroidPDACommand.OfflinePlusGetPatterns_Async, AndroidPDACommand", OfflinePlusGetPatterns_Async_Command); - -- Gitee