using System; using System.Diagnostics; using System.Drawing; using System.IO; using System.Text; using System.Windows.Forms; internal static class Program { [STAThread] private static int Main(string[] args) { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); using (var window = new PackagerForm()) { if (args.Length == 1 && args[0] == "--smoke-test") return 0; Application.Run(window); } return 0; } } internal sealed class PackagerForm : Form { private readonly TextBox project = new TextBox(); private readonly TextBox flutter = new TextBox(); private readonly TextBox api = new TextBox(); private readonly TextBox output = new TextBox(); private readonly TextBox version = new TextBox(); private readonly TextBox code = new TextBox(); private readonly ComboBox mode = new ComboBox(); private readonly CheckBox tests = new CheckBox(); private readonly RichTextBox log = new RichTextBox(); private readonly Button build = new Button(); private readonly Button check = new Button(); private readonly Button install = new Button(); private readonly Label status = new Label(); private readonly ProgressBar progress = new ProgressBar(); private readonly string basePath = AppDomain.CurrentDomain.BaseDirectory; private Process running; private string apk = ""; private string lastOutput = ""; public PackagerForm() { Text = "跃动 · Android 应用打包工具"; Size = new Size(980, 790); MinimumSize = new Size(900, 720); StartPosition = FormStartPosition.CenterScreen; Font = new Font("Microsoft YaHei UI", 10F); BackColor = Color.FromArgb(244, 248, 255); AutoScaleMode = AutoScaleMode.Dpi; var root = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(22), ColumnCount = 1, RowCount = 5 }; root.RowStyles.Add(new RowStyle(SizeType.Absolute, 66)); root.RowStyles.Add(new RowStyle(SizeType.Absolute, 272)); root.RowStyles.Add(new RowStyle(SizeType.Absolute, 58)); root.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); root.RowStyles.Add(new RowStyle(SizeType.Absolute, 42)); Controls.Add(root); var heading = new Label { Text = "花样跳绳打分系统 / APK 打包\n选择配置 → 开始打包 → 获取 APK 或安装到设备", Dock = DockStyle.Fill, Font = new Font(Font.FontFamily, 13F, FontStyle.Bold), ForeColor = Color.FromArgb(20, 73, 136) }; root.Controls.Add(heading); var fields = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 3, RowCount = 7 }; fields.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110)); fields.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); fields.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 92)); for (int i=0;i<7;i++) fields.RowStyles.Add(new RowStyle(SizeType.Absolute, 38)); root.Controls.Add(fields); string inferred = Path.GetFullPath(Path.Combine(basePath, "..", "..")); project.Text = File.Exists(Path.Combine(inferred, "pubspec.yaml")) ? inferred : @"C:\Project\Event\YD_Pattern_scoring_system"; output.Text = Path.Combine(project.Text, "packages"); api.Text = "http://127.0.0.1:9996/api"; AddField(fields, 0, "项目目录", project, delegate { PickFolder(project); }); AddField(fields, 1, "Flutter 程序", flutter, delegate { using(var d=new OpenFileDialog { Filter="Flutter|flutter.bat", Title="选择 Flutter SDK 中的 bin\\flutter.bat" }) if(d.ShowDialog()==DialogResult.OK) flutter.Text=d.FileName; }); AddField(fields, 2, "接口地址", api, null); AddField(fields, 3, "输出目录", output, delegate { PickFolder(output); }); mode.DropDownStyle=ComboBoxStyle.DropDownList; mode.Items.AddRange(new object[]{"Debug(调试 / 支持 HTTP)", "Release(优化 / 要求 HTTPS)"}); mode.SelectedIndex=0; AddField(fields,4,"打包模式",mode,null); var versions = new FlowLayoutPanel { Dock=DockStyle.Fill, WrapContents=false }; versions.Controls.Add(new Label { Text="版本名",AutoSize=true,Margin=new Padding(0,5,8,0) }); version.Width=100; versions.Controls.Add(version); versions.Controls.Add(new Label { Text="版本号",AutoSize=true,Margin=new Padding(12,5,8,0) }); code.Width=90; versions.Controls.Add(code); versions.Controls.Add(new Label { Text="留空沿用项目配置",AutoSize=true,Margin=new Padding(10,5,0,0) }); AddField(fields,5,"版本(可选)",versions,null); tests.Text="打包前运行静态检查和全部测试"; tests.Checked=true; tests.AutoSize=true; AddField(fields,6,"质量检查",tests,null); var buttons=new FlowLayoutPanel { Dock=DockStyle.Fill, WrapContents=false }; root.Controls.Add(buttons); SetupButton(check,"检查环境",delegate { StartJob("check"); }); buttons.Controls.Add(check); SetupButton(build,"开始打包",delegate { StartJob("build"); }); build.BackColor=Color.FromArgb(0,130,246); build.ForeColor=Color.White; buttons.Controls.Add(build); var open=new Button(); SetupButton(open,"打开输出目录",delegate { string p=Directory.Exists(lastOutput)?lastOutput:output.Text; if(Directory.Exists(p)) Process.Start("explorer.exe",Quote(p)); else MessageBox.Show("输出目录尚未生成。"); }); buttons.Controls.Add(open); SetupButton(install,"安装刚生成的 APK",delegate { if(MessageBox.Show("将覆盖安装到唯一已授权的 Android 设备,保留应用数据。是否继续?","安装确认",MessageBoxButtons.YesNo)==DialogResult.Yes) StartJob("install"); }); install.Width=182; install.Enabled=false; buttons.Controls.Add(install); var help=new Button(); SetupButton(help,"使用说明",delegate { Process.Start("notepad.exe",Quote(Path.Combine(basePath,"README.txt"))); }); buttons.Controls.Add(help); log.Dock=DockStyle.Fill; log.ReadOnly=true; log.BackColor=Color.FromArgb(19,31,49); log.ForeColor=Color.FromArgb(220,234,249); log.Font=new Font("Consolas",10F); log.WordWrap=false; root.Controls.Add(log); var footer=new TableLayoutPanel{Dock=DockStyle.Fill,ColumnCount=2}; footer.ColumnStyles.Add(new ColumnStyle(SizeType.Percent,100)); footer.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute,150)); status.Dock=DockStyle.Fill; status.Text="就绪 · Flutter 留空时自动检测 · 无需管理员权限"; status.TextAlign=ContentAlignment.MiddleLeft; progress.Dock=DockStyle.Fill; progress.Style=ProgressBarStyle.Marquee; progress.Visible=false; footer.Controls.Add(status); footer.Controls.Add(progress); root.Controls.Add(footer); Append("提示:已有 App 内的接口地址配置优先于打包默认地址。\nRelease 当前仍为调试签名,不可直接用于正式商店发布。\n连接多台设备时请先断开其他设备,工具不会猜测安装目标。"); FormClosing += delegate(object sender, FormClosingEventArgs e) { if(running!=null) { e.Cancel=true; MessageBox.Show("任务仍在运行,请等待完成后关闭,避免留下不完整安装包。"); } }; } private void AddField(TableLayoutPanel p,int row,string label,Control input,Action browse) { p.Controls.Add(new Label{Text=label,Dock=DockStyle.Fill,TextAlign=ContentAlignment.MiddleLeft},0,row); input.Dock=DockStyle.Fill; input.Margin=new Padding(3,4,8,4); p.Controls.Add(input,1,row); if(browse!=null) { var b=new Button{Text="浏览…",Dock=DockStyle.Fill}; b.Click+=delegate { browse(); }; p.Controls.Add(b,2,row); } } private static void SetupButton(Button button,string text,Action action) { button.Text=text; button.Width=128; button.Height=40; button.FlatStyle=FlatStyle.Flat; button.Margin=new Padding(0,4,10,4); button.Click+=delegate { action(); }; } private void PickFolder(TextBox target) { using(var d=new FolderBrowserDialog { SelectedPath=target.Text }) if(d.ShowDialog()==DialogResult.OK) target.Text=d.SelectedPath; } private static string Quote(string value) { if(value.IndexOf('"')>=0 || value.IndexOf('\r')>=0 || value.IndexOf('\n')>=0) throw new ArgumentException("配置不能包含双引号或换行。"); return "\""+value.TrimEnd('\\')+"\""; } private void Append(string text) { log.AppendText(text+Environment.NewLine); log.SelectionStart=log.TextLength; log.ScrollToCaret(); } private void Ui(Action action) { if(!IsDisposed && IsHandleCreated) BeginInvoke(action); } private void StartJob(string kind) { if(running!=null) return; try { string script=Path.Combine(basePath,"build-app.ps1"); if(!File.Exists(script)) throw new FileNotFoundException("请保留 EXE 同目录下的 build-app.ps1 文件。"); if(kind=="build" && mode.SelectedIndex==1 && MessageBox.Show("当前 Release 使用调试签名,仅用于内部测试。正式发布须另行配置正式签名。是否继续?","Release 签名提醒",MessageBoxButtons.YesNo,MessageBoxIcon.Warning)!=DialogResult.Yes) return; var args=new StringBuilder("-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "+Quote(script)); args.Append(" -ProjectPath "+Quote(project.Text)+" -Mode "+(mode.SelectedIndex==1?"release":"debug")); args.Append(" -ApiUrl "+Quote(api.Text)+" -OutputPath "+Quote(output.Text)); if(flutter.Text.Trim().Length>0) args.Append(" -FlutterPath "+Quote(flutter.Text)); if(version.Text.Trim().Length>0) args.Append(" -BuildName "+Quote(version.Text)); if(code.Text.Trim().Length>0) args.Append(" -BuildNumber "+Quote(code.Text)); if(kind=="check") args.Append(" -CheckOnly"); else if(kind=="install") args.Append(" -InstallOnly -ApkPath "+Quote(apk)); else if(tests.Checked) args.Append(" -RunTests"); if(kind=="build") { apk=""; install.Enabled=false; } Append("\n===== "+DateTime.Now.ToString("HH:mm:ss")+" / "+kind+" ====="); var process=new Process { StartInfo=new ProcessStartInfo(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System),@"WindowsPowerShell\v1.0\powershell.exe"),args.ToString()) { UseShellExecute=false,CreateNoWindow=true,RedirectStandardOutput=true,RedirectStandardError=true,StandardOutputEncoding=Encoding.UTF8,StandardErrorEncoding=Encoding.UTF8,WorkingDirectory=basePath } }; process.OutputDataReceived+=delegate(object s,DataReceivedEventArgs e) { if(e.Data!=null) Ui(delegate { Append(e.Data); if(e.Data.StartsWith("PACKAGER_APK=")) apk=e.Data.Substring(13); if(e.Data.StartsWith("PACKAGER_OUTPUT=")) lastOutput=e.Data.Substring(16); }); }; process.ErrorDataReceived+=delegate(object s,DataReceivedEventArgs e) { if(e.Data!=null) Ui(delegate { Append(e.Data); }); }; running=process; build.Enabled=false; check.Enabled=false; install.Enabled=false; progress.Visible=true; status.Text="任务执行中,请勿关闭窗口…"; process.Start(); process.BeginOutputReadLine(); process.BeginErrorReadLine(); System.Threading.ThreadPool.QueueUserWorkItem(delegate { process.WaitForExit(); int result=process.ExitCode; Ui(delegate { running=null; process.Dispose(); build.Enabled=true; check.Enabled=true; install.Enabled=File.Exists(apk); progress.Visible=false; status.Text=result!=0?"执行失败 · 请查看日志中的 PACKAGER_ERROR":kind=="check"?"环境检查通过 · 可以开始打包":kind=="install"?"APK 安装完成":"打包完成 · 可打开输出目录查看 APK / 日志"; if(result==0 && kind=="build") MessageBox.Show("APK 已生成:\n"+apk,"打包成功",MessageBoxButtons.OK,MessageBoxIcon.Information); }); }); } catch(Exception e) { if(running!=null) running.Dispose(); running=null; build.Enabled=true; check.Enabled=true; progress.Visible=false; Append(e.Message); MessageBox.Show(e.Message,"无法启动",MessageBoxButtons.OK,MessageBoxIcon.Error); } } }