Explorar el Código

Initial commit

Matt Evan hace 1 semana
commit
24f20ab903

+ 2 - 0
.gitattributes

@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto

+ 32 - 0
.gitignore

@@ -0,0 +1,32 @@
+# If you prefer the allow list template instead of the deny list, see community template:
+# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
+#
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+.idea
+build/bin
+frontend/dist
+frontend/node_modules
+frontend/package.json.md5
+frontend/pnpm-lock.yaml
+frontend/pnpm-workspace.yaml
+
+# Go workspace file
+go.work
+go.work.sum
+
+# env file
+.env

+ 2 - 0
README.md

@@ -0,0 +1,2 @@
+# wcs-desktop
+

+ 51 - 0
app.go

@@ -0,0 +1,51 @@
+package main
+
+import (
+	"context"
+	"log"
+	"os"
+	
+	"SIMANC-WCS/app"
+	"SIMANC-WCS/config"
+)
+
+type App struct {
+	ctx context.Context
+	
+	config.Config
+}
+
+func NewApp() *App {
+	return &App{}
+}
+
+func (a *App) initApp() {
+	// 初始化上下文
+	app.Context = a.ctx
+	// 初始化资源数据目录
+	stat, err := os.Stat(app.DataPath)
+	if err == nil {
+		// 存在但不是文件夹时
+		if !stat.IsDir() {
+			log.Panicf("initApp: [%s] is not a directory", app.DataPath)
+		}
+		return
+	}
+	// 如果不是"不存在"的错误时, 即: 其他错误
+	if !os.IsNotExist(err) {
+		log.Panicf("initApp: %s", err)
+	}
+	if err = os.MkdirAll(app.DataPath, os.ModePerm); err != nil {
+		log.Panicf("initApp: %s", err)
+	}
+}
+
+// Exit 退出
+func (a *App) Exit() {
+	os.Exit(0)
+}
+
+func (a *App) startup(ctx context.Context) {
+	a.ctx = ctx
+	a.initApp()
+}

+ 16 - 0
app/global.go

@@ -0,0 +1,16 @@
+package app
+
+import (
+	"context"
+)
+
+var (
+	// Context 是 Wails 启用时传入的全局上下文, 客户端启动时被设定
+	Context context.Context
+)
+
+const (
+	// DataPath 数据目录. 客户端运行所产生的数据保存在此处, 例如配置文件
+	DataPath = "./data"
+)
+

+ 35 - 0
build/README.md

@@ -0,0 +1,35 @@
+# Build Directory
+
+The build directory is used to house all the build files and assets for your application. 
+
+The structure is:
+
+* bin - Output directory
+* darwin - macOS specific files
+* windows - Windows specific files
+
+## Mac
+
+The `darwin` directory holds files specific to Mac builds.
+These may be customised and used as part of the build. To return these files to the default state, simply delete them
+and
+build with `wails build`.
+
+The directory contains the following files:
+
+- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
+- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
+
+## Windows
+
+The `windows` directory contains the manifest and rc files used when building with `wails build`.
+These may be customised for your application. To return these files to the default state, simply delete them and
+build with `wails build`.
+
+- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
+  use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
+  will be created using the `appicon.png` file in the build directory.
+- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
+- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
+  as well as the application itself (right click the exe -> properties -> details)
+- `wails.exe.manifest` - The main application manifest file.

BIN
build/appicon.png


+ 68 - 0
build/darwin/Info.dev.plist

@@ -0,0 +1,68 @@
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+    <dict>
+        <key>CFBundlePackageType</key>
+        <string>APPL</string>
+        <key>CFBundleName</key>
+        <string>{{.Info.ProductName}}</string>
+        <key>CFBundleExecutable</key>
+        <string>{{.OutputFilename}}</string>
+        <key>CFBundleIdentifier</key>
+        <string>com.wails.{{.Name}}</string>
+        <key>CFBundleVersion</key>
+        <string>{{.Info.ProductVersion}}</string>
+        <key>CFBundleGetInfoString</key>
+        <string>{{.Info.Comments}}</string>
+        <key>CFBundleShortVersionString</key>
+        <string>{{.Info.ProductVersion}}</string>
+        <key>CFBundleIconFile</key>
+        <string>iconfile</string>
+        <key>LSMinimumSystemVersion</key>
+        <string>10.13.0</string>
+        <key>NSHighResolutionCapable</key>
+        <string>true</string>
+        <key>NSHumanReadableCopyright</key>
+        <string>{{.Info.Copyright}}</string>
+        {{if .Info.FileAssociations}}
+        <key>CFBundleDocumentTypes</key>
+        <array>
+          {{range .Info.FileAssociations}}
+          <dict>
+            <key>CFBundleTypeExtensions</key>
+            <array>
+              <string>{{.Ext}}</string>
+            </array>
+            <key>CFBundleTypeName</key>
+            <string>{{.Name}}</string>
+            <key>CFBundleTypeRole</key>
+            <string>{{.Role}}</string>
+            <key>CFBundleTypeIconFile</key>
+            <string>{{.IconName}}</string>
+          </dict>
+          {{end}}
+        </array>
+        {{end}}
+        {{if .Info.Protocols}}
+        <key>CFBundleURLTypes</key>
+        <array>
+          {{range .Info.Protocols}}
+            <dict>
+                <key>CFBundleURLName</key>
+                <string>com.wails.{{.Scheme}}</string>
+                <key>CFBundleURLSchemes</key>
+                <array>
+                    <string>{{.Scheme}}</string>
+                </array>
+                <key>CFBundleTypeRole</key>
+                <string>{{.Role}}</string>
+            </dict>
+          {{end}}
+        </array>
+        {{end}}
+        <key>NSAppTransportSecurity</key>
+        <dict>
+            <key>NSAllowsLocalNetworking</key>
+            <true/>
+        </dict>
+    </dict>
+</plist>

+ 63 - 0
build/darwin/Info.plist

@@ -0,0 +1,63 @@
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+    <dict>
+        <key>CFBundlePackageType</key>
+        <string>APPL</string>
+        <key>CFBundleName</key>
+        <string>{{.Info.ProductName}}</string>
+        <key>CFBundleExecutable</key>
+        <string>{{.OutputFilename}}</string>
+        <key>CFBundleIdentifier</key>
+        <string>com.wails.{{.Name}}</string>
+        <key>CFBundleVersion</key>
+        <string>{{.Info.ProductVersion}}</string>
+        <key>CFBundleGetInfoString</key>
+        <string>{{.Info.Comments}}</string>
+        <key>CFBundleShortVersionString</key>
+        <string>{{.Info.ProductVersion}}</string>
+        <key>CFBundleIconFile</key>
+        <string>iconfile</string>
+        <key>LSMinimumSystemVersion</key>
+        <string>10.13.0</string>
+        <key>NSHighResolutionCapable</key>
+        <string>true</string>
+        <key>NSHumanReadableCopyright</key>
+        <string>{{.Info.Copyright}}</string>
+        {{if .Info.FileAssociations}}
+        <key>CFBundleDocumentTypes</key>
+        <array>
+          {{range .Info.FileAssociations}}
+          <dict>
+            <key>CFBundleTypeExtensions</key>
+            <array>
+              <string>{{.Ext}}</string>
+            </array>
+            <key>CFBundleTypeName</key>
+            <string>{{.Name}}</string>
+            <key>CFBundleTypeRole</key>
+            <string>{{.Role}}</string>
+            <key>CFBundleTypeIconFile</key>
+            <string>{{.IconName}}</string>
+          </dict>
+          {{end}}
+        </array>
+        {{end}}
+        {{if .Info.Protocols}}
+        <key>CFBundleURLTypes</key>
+        <array>
+          {{range .Info.Protocols}}
+            <dict>
+                <key>CFBundleURLName</key>
+                <string>com.wails.{{.Scheme}}</string>
+                <key>CFBundleURLSchemes</key>
+                <array>
+                    <string>{{.Scheme}}</string>
+                </array>
+                <key>CFBundleTypeRole</key>
+                <string>{{.Role}}</string>
+            </dict>
+          {{end}}
+        </array>
+        {{end}}
+    </dict>
+</plist>

BIN
build/windows/icon.ico


+ 15 - 0
build/windows/info.json

@@ -0,0 +1,15 @@
+{
+	"fixed": {
+		"file_version": "{{.Info.ProductVersion}}"
+	},
+	"info": {
+		"0000": {
+			"ProductVersion": "{{.Info.ProductVersion}}",
+			"CompanyName": "{{.Info.CompanyName}}",
+			"FileDescription": "{{.Info.ProductName}}",
+			"LegalCopyright": "{{.Info.Copyright}}",
+			"ProductName": "{{.Info.ProductName}}",
+			"Comments": "{{.Info.Comments}}"
+		}
+	}
+}

+ 114 - 0
build/windows/installer/project.nsi

@@ -0,0 +1,114 @@
+Unicode true
+
+####
+## Please note: Template replacements don't work in this file. They are provided with default defines like
+## mentioned underneath.
+## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
+## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
+## from outside of Wails for debugging and development of the installer.
+##
+## For development first make a wails nsis build to populate the "wails_tools.nsh":
+## > wails build --target windows/amd64 --nsis
+## Then you can call makensis on this file with specifying the path to your binary:
+## For a AMD64 only installer:
+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
+## For a ARM64 only installer:
+## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
+## For a installer with both architectures:
+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
+####
+## The following information is taken from the ProjectInfo file, but they can be overwritten here.
+####
+## !define INFO_PROJECTNAME    "MyProject" # Default "{{.Name}}"
+## !define INFO_COMPANYNAME    "MyCompany" # Default "{{.Info.CompanyName}}"
+## !define INFO_PRODUCTNAME    "MyProduct" # Default "{{.Info.ProductName}}"
+## !define INFO_PRODUCTVERSION "1.0.0"     # Default "{{.Info.ProductVersion}}"
+## !define INFO_COPYRIGHT      "Copyright" # Default "{{.Info.Copyright}}"
+###
+## !define PRODUCT_EXECUTABLE  "Application.exe"      # Default "${INFO_PROJECTNAME}.exe"
+## !define UNINST_KEY_NAME     "UninstKeyInRegistry"  # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
+####
+## !define REQUEST_EXECUTION_LEVEL "admin"            # Default "admin"  see also https://nsis.sourceforge.io/Docs/Chapter4.html
+####
+## Include the wails tools
+####
+!include "wails_tools.nsh"
+
+# The version information for this two must consist of 4 parts
+VIProductVersion "${INFO_PRODUCTVERSION}.0"
+VIFileVersion    "${INFO_PRODUCTVERSION}.0"
+
+VIAddVersionKey "CompanyName"     "${INFO_COMPANYNAME}"
+VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
+VIAddVersionKey "ProductVersion"  "${INFO_PRODUCTVERSION}"
+VIAddVersionKey "FileVersion"     "${INFO_PRODUCTVERSION}"
+VIAddVersionKey "LegalCopyright"  "${INFO_COPYRIGHT}"
+VIAddVersionKey "ProductName"     "${INFO_PRODUCTNAME}"
+
+# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
+ManifestDPIAware true
+
+!include "MUI.nsh"
+
+!define MUI_ICON "..\icon.ico"
+!define MUI_UNICON "..\icon.ico"
+# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
+!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
+!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
+
+!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
+# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
+!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
+!insertmacro MUI_PAGE_INSTFILES # Installing page.
+!insertmacro MUI_PAGE_FINISH # Finished installation page.
+
+!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
+
+!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
+
+## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
+#!uninstfinalize 'signtool --file "%1"'
+#!finalize 'signtool --file "%1"'
+
+Name "${INFO_PRODUCTNAME}"
+OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
+InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
+ShowInstDetails show # This will always show the installation details.
+
+Function .onInit
+   !insertmacro wails.checkArchitecture
+FunctionEnd
+
+Section
+    !insertmacro wails.setShellContext
+
+    !insertmacro wails.webview2runtime
+
+    SetOutPath $INSTDIR
+
+    !insertmacro wails.files
+
+    CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+    CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+
+    !insertmacro wails.associateFiles
+    !insertmacro wails.associateCustomProtocols
+
+    !insertmacro wails.writeUninstaller
+SectionEnd
+
+Section "uninstall"
+    !insertmacro wails.setShellContext
+
+    RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
+
+    RMDir /r $INSTDIR
+
+    Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
+    Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
+
+    !insertmacro wails.unassociateFiles
+    !insertmacro wails.unassociateCustomProtocols
+
+    !insertmacro wails.deleteUninstaller
+SectionEnd

+ 249 - 0
build/windows/installer/wails_tools.nsh

@@ -0,0 +1,249 @@
+# DO NOT EDIT - Generated automatically by `wails build`
+
+!include "x64.nsh"
+!include "WinVer.nsh"
+!include "FileFunc.nsh"
+
+!ifndef INFO_PROJECTNAME
+    !define INFO_PROJECTNAME "{{.Name}}"
+!endif
+!ifndef INFO_COMPANYNAME
+    !define INFO_COMPANYNAME "{{.Info.CompanyName}}"
+!endif
+!ifndef INFO_PRODUCTNAME
+    !define INFO_PRODUCTNAME "{{.Info.ProductName}}"
+!endif
+!ifndef INFO_PRODUCTVERSION
+    !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
+!endif
+!ifndef INFO_COPYRIGHT
+    !define INFO_COPYRIGHT "{{.Info.Copyright}}"
+!endif
+!ifndef PRODUCT_EXECUTABLE
+    !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
+!endif
+!ifndef UNINST_KEY_NAME
+    !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
+!endif
+!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
+
+!ifndef REQUEST_EXECUTION_LEVEL
+    !define REQUEST_EXECUTION_LEVEL "admin"
+!endif
+
+RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
+
+!ifdef ARG_WAILS_AMD64_BINARY
+    !define SUPPORTS_AMD64
+!endif
+
+!ifdef ARG_WAILS_ARM64_BINARY
+    !define SUPPORTS_ARM64
+!endif
+
+!ifdef SUPPORTS_AMD64
+    !ifdef SUPPORTS_ARM64
+        !define ARCH "amd64_arm64"
+    !else
+        !define ARCH "amd64"
+    !endif
+!else
+    !ifdef SUPPORTS_ARM64
+        !define ARCH "arm64"
+    !else
+        !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
+    !endif
+!endif
+
+!macro wails.checkArchitecture
+    !ifndef WAILS_WIN10_REQUIRED
+        !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
+    !endif
+
+    !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
+        !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
+    !endif
+
+    ${If} ${AtLeastWin10}
+        !ifdef SUPPORTS_AMD64
+            ${if} ${IsNativeAMD64}
+                Goto ok
+            ${EndIf}
+        !endif
+
+        !ifdef SUPPORTS_ARM64
+            ${if} ${IsNativeARM64}
+                Goto ok
+            ${EndIf}
+        !endif
+
+        IfSilent silentArch notSilentArch
+        silentArch:
+            SetErrorLevel 65
+            Abort
+        notSilentArch:
+            MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
+            Quit
+    ${else}
+        IfSilent silentWin notSilentWin
+        silentWin:
+            SetErrorLevel 64
+            Abort
+        notSilentWin:
+            MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
+            Quit
+    ${EndIf}
+
+    ok:
+!macroend
+
+!macro wails.files
+    !ifdef SUPPORTS_AMD64
+        ${if} ${IsNativeAMD64}
+            File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
+        ${EndIf}
+    !endif
+
+    !ifdef SUPPORTS_ARM64
+        ${if} ${IsNativeARM64}
+            File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
+        ${EndIf}
+    !endif
+!macroend
+
+!macro wails.writeUninstaller
+    WriteUninstaller "$INSTDIR\uninstall.exe"
+
+    SetRegView 64
+    WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
+    WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
+    WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
+    WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+    WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
+    WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
+
+    ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
+    IntFmt $0 "0x%08X" $0
+    WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
+!macroend
+
+!macro wails.deleteUninstaller
+    Delete "$INSTDIR\uninstall.exe"
+
+    SetRegView 64
+    DeleteRegKey HKLM "${UNINST_KEY}"
+!macroend
+
+!macro wails.setShellContext
+    ${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
+        SetShellVarContext all
+    ${else}
+        SetShellVarContext current
+    ${EndIf}
+!macroend
+
+# Install webview2 by launching the bootstrapper
+# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
+!macro wails.webview2runtime
+    !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
+        !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
+    !endif
+
+    SetRegView 64
+	# If the admin key exists and is not empty then webview2 is already installed
+	ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
+    ${If} $0 != ""
+        Goto ok
+    ${EndIf}
+
+    ${If} ${REQUEST_EXECUTION_LEVEL} == "user"
+        # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
+	    ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
+        ${If} $0 != ""
+            Goto ok
+        ${EndIf}
+     ${EndIf}
+
+	SetDetailsPrint both
+    DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
+    SetDetailsPrint listonly
+
+    InitPluginsDir
+    CreateDirectory "$pluginsdir\webview2bootstrapper"
+    SetOutPath "$pluginsdir\webview2bootstrapper"
+    File "tmp\MicrosoftEdgeWebview2Setup.exe"
+    ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
+
+    SetDetailsPrint both
+    ok:
+!macroend
+
+# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
+!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
+  ; Backup the previously associated file class
+  ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
+  WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
+
+  WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
+
+  WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
+  WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
+  WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
+  WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
+  WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
+!macroend
+
+!macro APP_UNASSOCIATE EXT FILECLASS
+  ; Backup the previously associated file class
+  ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
+  WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
+
+  DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
+!macroend
+
+!macro wails.associateFiles
+    ; Create file associations
+    {{range .Info.FileAssociations}}
+      !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
+
+      File "..\{{.IconName}}.ico"
+    {{end}}
+!macroend
+
+!macro wails.unassociateFiles
+    ; Delete app associations
+    {{range .Info.FileAssociations}}
+      !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
+
+      Delete "$INSTDIR\{{.IconName}}.ico"
+    {{end}}
+!macroend
+
+!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
+  DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
+  WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
+  WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
+  WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
+  WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
+  WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
+  WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
+!macroend
+
+!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
+  DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
+!macroend
+
+!macro wails.associateCustomProtocols
+    ; Create custom protocols associations
+    {{range .Info.Protocols}}
+      !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
+
+    {{end}}
+!macroend
+
+!macro wails.unassociateCustomProtocols
+    ; Delete app custom protocol associations
+    {{range .Info.Protocols}}
+      !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
+    {{end}}
+!macroend

+ 15 - 0
build/windows/wails.exe.manifest

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
+    <assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
+    <dependency>
+        <dependentAssembly>
+            <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
+        </dependentAssembly>
+    </dependency>
+    <asmv3:application>
+        <asmv3:windowsSettings>
+            <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
+            <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
+        </asmv3:windowsSettings>
+    </asmv3:application>
+</assembly>

+ 94 - 0
config/config.go

@@ -0,0 +1,94 @@
+package config
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"log"
+	"net"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"SIMANC-WCS/app"
+	"SIMANC-WCS/lib/server"
+)
+
+// 配置文件名称
+const configName = "config.json"
+
+// ReadFile 从数据目录中读取 name
+func ReadFile(name string) ([]byte, error) {
+	fileName := filepath.Join(app.DataPath, name)
+	_, err := os.Stat(fileName)
+	if err != nil {
+		return nil, err
+	}
+	b, err := os.ReadFile(fileName)
+	if err != nil {
+		log.Println(err)
+		return nil, err
+	}
+	return b, nil
+}
+
+// Config 配置文件
+type Config struct {
+	// Servers 服务器地址, 其中第一个元素作为主服务器, 剩余作为备用服务器
+	Servers []string `json:"servers"`
+}
+
+func (c *Config) saveConfig(config Config) error {
+	b, err := json.Marshal(config)
+	if err != nil {
+		return err
+	}
+	if err = os.MkdirAll(app.DataPath, os.ModePerm); err != nil {
+		return err
+	}
+	dir := filepath.Join(app.DataPath, configName)
+	return os.WriteFile(dir, b, os.ModePerm)
+}
+
+// GetConfig 获取配置文件
+func (c *Config) GetConfig() (Config, error) {
+	b, err := ReadFile(configName)
+	if err != nil {
+		return Config{}, err
+	}
+	var config Config
+	if err = json.Unmarshal(b, &config); err != nil {
+		return Config{}, err
+	}
+	return config, nil
+}
+
+// SaveConfig 保存配置文件
+// 保存时需要校验所有配置项
+func (c *Config) SaveConfig(cfg Config) error {
+	if len(cfg.Servers) < 1 {
+		return errors.New("no servers")
+	}
+	for i, address := range cfg.Servers {
+		if !strings.Contains(address, ":") {
+			// 端口号不存在时添加默认端口
+			address = net.JoinHostPort(address, "80")
+		}
+		_, _, err := net.SplitHostPort(address)
+		if err != nil {
+			return fmt.Errorf("invalid server address: %s", err)
+		}
+		cfg.Servers[i] = address
+	}
+	// 测试服务器是否联通
+	if err := server.ConnectTest(app.Context, cfg.Servers); err != nil {
+		return err
+	}
+	// 保存配置文件
+	if err := c.saveConfig(cfg); err != nil {
+		return err
+	}
+	// 更新内存数据
+	c.Servers = cfg.Servers
+	return nil
+}

+ 1 - 0
data/config.json

@@ -0,0 +1 @@
+{"servers":["localhost:80"]}

+ 15 - 0
frontend/index.html

@@ -0,0 +1,15 @@
+<!doctype html>
+<html lang="zh">
+<head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
+    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+    <title>SIMANC - 开始</title>
+</head>
+<body>
+<div class="page page-center">
+    <div class="container container-tight py-1"></div>
+</div>
+<script src="./src/main.ts" type="module"></script>
+</body>
+</html>

+ 18 - 0
frontend/package.json

@@ -0,0 +1,18 @@
+{
+  "name": "frontend",
+  "private": true,
+  "version": "0.0.0",
+  "scripts": {
+    "dev": "vite",
+    "build": "tsc && vite build",
+    "preview": "vite preview"
+  },
+  "devDependencies": {
+    "typescript": "^5.9.3",
+    "vite": "^7.3.0"
+  },
+  "dependencies": {
+    "@tabler/core": "^1.4.0",
+    "@tabler/icons-webfont": "^3.36.0"
+  }
+}

+ 0 - 0
frontend/src/app.css


+ 36 - 0
frontend/src/assets/fonts/illustrations/download.svg

@@ -0,0 +1,36 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600" fill="none" viewBox="0 0 800 600">
+    <path d="M159.32 309.98C159.32 345.85 197.31 371.97 214.1 400.99C231.42 430.93 235.59 476.67 265.52 493.99C294.53 510.78 335.84 492 371.77 492C407.7 492 449.01 510.84 477.98 493.99C507.92 476.67 512.08 430.9 529.4 400.99C546.19 371.97 584.19 345.91 584.19 309.98C584.19 274.05 546.19 247.98 529.4 218.97C512.08 189.03 507.91 143.28 477.98 125.96C448.97 109.17 407.67 127.95 371.73 127.95C335.79 127.95 294.53 109.17 265.52 125.96C235.58 143.28 231.42 189.05 214.1 218.97C197.31 247.98 159.32 274.04 159.32 309.98Z" fill="#066FD1" opacity="0.02"/>
+    <path d="M253.63 354.48H338.14V367.73H253.63C249.97 367.73 247 364.76 247 361.1C247 357.44 249.97 354.47 253.63 354.47V354.48Z" fill="#066FD1"/>
+    <path d="M358.478 354.48H338.148V367.73H358.478C362.138 367.73 365.108 364.76 365.108 361.1C365.108 357.44 362.138 354.47 358.478 354.47V354.48Z" fill="#DADCE0"/>
+    <path d="M278.411 303.77L303.961 338.77C305.001 340.19 307.111 340.19 308.151 338.77L333.701 303.77C334.951 302.06 333.731 299.65 331.611 299.65H320.881C319.451 299.65 318.291 298.49 318.291 297.06V267.24C318.291 265.81 317.131 264.65 315.701 264.65H296.441C295.011 264.65 293.851 265.81 293.851 267.24V297.06C293.851 298.49 292.691 299.65 291.261 299.65H280.531C278.411 299.65 277.191 302.06 278.441 303.77H278.411Z" fill="#066FD1"/>
+    <path d="M373.331 440.81C485.737 440.81 576.861 439.413 576.861 437.69C576.861 435.967 485.737 434.57 373.331 434.57C260.924 434.57 169.801 435.967 169.801 437.69C169.801 439.413 260.924 440.81 373.331 440.81Z" fill="#D6D8E2" opacity="0.5"/>
+    <path d="M502.319 222.28H236.109C225.579 222.31 217.059 230.84 217.039 241.37V393.6C217.019 404.15 225.559 412.71 236.109 412.73H333.339V419.21C332.329 426.27 327.569 428.3 324.249 428.77H287.149C285.729 428.77 284.569 429.92 284.569 431.35C284.569 431.35 284.569 431.36 284.569 431.37V435.13C284.569 436.55 285.719 437.71 287.149 437.71H451.259C452.679 437.71 453.839 436.56 453.849 435.13V431.37C453.849 429.94 452.689 428.78 451.259 428.78H414.099C410.799 428.26 406.009 426.27 405.009 419.18V412.74H502.299C512.829 412.74 521.369 404.2 521.389 393.67V364.92H514.119V393.61C514.119 400.13 508.829 405.42 502.299 405.43H236.089C229.569 405.43 224.279 400.14 224.269 393.61V241.38C224.269 234.86 229.559 229.57 236.089 229.56H502.299C508.819 229.56 514.109 234.85 514.119 241.38V370.64H521.389V241.38C521.369 230.85 512.839 222.31 502.299 222.29L502.319 222.28Z" fill="#232B41"/>
+    <path d="M297.689 132.43L291.499 164.9C291.189 166.51 292.249 168.07 293.859 168.38L333.039 175.85C334.649 176.16 336.209 175.1 336.519 173.49L346.419 121.57C346.729 119.96 345.669 118.4 344.059 118.09L324.329 114.33L297.699 132.43H297.689Z" fill="#DADCE0"/>
+    <path d="M297.691 132.43L316.221 135.96C318.411 136.38 320.531 134.93 320.931 132.74L324.321 114.33L297.691 132.43Z" fill="#A6A9B3"/>
+    <path d="M307.587 147.5L331.8 152.119C332.972 152.342 334.103 151.574 334.327 150.402C334.55 149.23 333.781 148.099 332.61 147.875L308.396 143.257C307.224 143.033 306.093 143.802 305.87 144.974C305.646 146.145 306.415 147.276 307.587 147.5Z" fill="#232B41"/>
+    <path d="M306.005 155.793L330.218 160.412C331.39 160.636 332.521 159.867 332.745 158.695C332.968 157.523 332.199 156.392 331.028 156.169L306.814 151.55C305.642 151.326 304.511 152.095 304.288 153.267C304.064 154.439 304.833 155.57 306.005 155.793Z" fill="#A6A9B3"/>
+    <path d="M546.352 190.68L561.992 234.85C562.772 237.04 565.172 238.19 567.372 237.41L620.662 218.54C622.852 217.76 624.002 215.36 623.222 213.16L598.222 142.54C597.442 140.35 595.042 139.2 592.842 139.98L566.002 149.48L546.352 190.67V190.68Z" fill="#A6A9B3"/>
+    <path d="M546.352 190.68L571.562 181.76C574.542 180.7 576.092 177.42 575.022 174.45L566.012 149.49L546.362 190.68H546.352Z" fill="#DADCE0"/>
+    <path d="M569.238 202.112L602.174 190.449C603.767 189.885 604.601 188.136 604.037 186.543L604.034 186.534C603.47 184.941 601.721 184.107 600.128 184.671L567.192 196.334C565.599 196.898 564.765 198.647 565.329 200.24L565.332 200.249C565.896 201.842 567.645 202.677 569.238 202.112Z" fill="#DADCE0"/>
+    <path d="M573.238 213.405L606.174 201.742C607.767 201.178 608.601 199.429 608.037 197.836L608.034 197.827C607.47 196.233 605.721 195.399 604.128 195.964L571.192 207.627C569.599 208.191 568.765 209.94 569.329 211.533L569.332 211.542C569.896 213.135 571.645 213.969 573.238 213.405Z" fill="white"/>
+    <path d="M556.941 219.1C557.081 217.63 557.491 216.21 558.171 214.9C560.121 209.96 564.271 207.1 568.531 207.98C574.011 209.1 577.721 215.77 576.741 223.18C576.671 223.97 576.491 224.75 576.211 225.49C574.581 231.47 569.951 235.33 565.151 234.3C559.811 233.12 556.061 226.32 556.941 219.1Z" fill="#FFCB9D"/>
+    <path d="M556.938 219.1C557.078 217.63 557.488 216.21 558.168 214.9C560.348 216.05 562.048 217.01 563.248 217.64C566.588 219.42 567.868 220.06 569.858 221.29C571.448 222.28 572.688 223.15 573.508 223.74L575.348 224.96L576.298 225.31C574.668 231.29 570.038 235.15 565.258 234.12C559.828 233.14 556.068 226.33 556.948 219.11L556.938 219.1Z" fill="black" opacity="0.1"/>
+    <path d="M326.429 172.08C326.459 170.61 326.199 169.14 325.669 167.77C324.269 162.65 320.449 159.35 316.129 159.76C310.559 160.27 306.139 166.5 306.309 173.97C306.299 174.76 306.389 175.55 306.589 176.32C307.559 182.44 311.739 186.79 316.619 186.29C322.049 185.7 326.529 179.35 326.439 172.08H326.429Z" fill="#FFCB9D"/>
+    <path d="M326.432 172.08C326.462 170.61 326.202 169.14 325.672 167.77C323.382 168.68 321.582 169.45 320.322 169.94C316.812 171.34 315.462 171.84 313.352 172.85C311.662 173.66 310.332 174.39 309.452 174.88L307.492 175.89L306.512 176.13C307.482 182.25 311.662 186.6 316.522 186.09C322.032 185.7 326.512 179.35 326.422 172.08H326.432Z" fill="black" opacity="0.1"/>
+    <path d="M588.141 236.52C580.331 273.76 563.191 300.01 547.351 310.73C541.111 314.96 535.081 316.79 529.891 315.92C524.851 315.08 519.361 311.5 514.051 306.23C504.541 296.79 495.671 281.94 491.251 267.78C490.971 266.9 490.761 266.17 490.621 265.66C486.601 277.83 482.601 289.97 478.681 302.16C474.311 315.19 470.081 328.29 465.751 341.19C465.751 341.22 465.731 341.24 465.721 341.28L397.631 325.19C399.411 311.5 401.351 297.83 403.121 284.3C404.161 276.51 405.151 268.88 406.201 261.11L409.021 240.09V240.05C405.351 242.14 389.561 250.76 368.591 250.88C361.671 250.92 354.171 250.03 346.351 247.68C339.311 245.56 333.131 242.59 327.701 239.08C300.511 221.6 292.201 191.01 291.691 189.02C305.251 183.68 318.811 178.33 332.381 173C333.571 177.26 341.011 202.59 361.521 209.36C365.581 210.7 375.811 214.07 386.431 210.5C393.761 208.03 401.391 202.21 412.321 203.91C412.971 204.01 413.501 204.12 413.851 204.19C437.031 209.67 460.201 215.14 483.381 220.62C488.431 220.59 496.661 221.21 503.821 225.44H503.841C505.741 226.58 507.571 227.95 509.251 229.65C509.431 229.84 509.651 230.07 509.881 230.33C515.281 236.47 517.501 245.69 517.501 245.69C519.431 255.86 524.151 277.54 531.091 277.73C536.311 277.85 540.621 265.74 542.651 260.04C545.691 251.52 549.531 236.08 545.841 214.55C559.941 221.86 574.041 229.18 588.141 236.49V236.52Z" fill="#DADCE0"/>
+    <path d="M538.341 438.3L511.831 436.75C511.831 436.75 510.521 429.94 514.451 427.38C518.381 424.82 533.631 432.15 538.341 438.31V438.3Z" fill="#232B41"/>
+    <path d="M499.819 154.32C498.939 157.97 497.529 163.42 495.759 169.91C495.309 171.41 494.919 172.6 494.819 173.06C492.509 180.17 489.339 186.98 485.379 193.32C484.499 194.58 483.789 195.71 482.909 196.96C480.559 200.01 469.659 201.97 458.229 200.99C454.269 200.68 450.359 199.91 446.589 198.68C434.739 195.04 430.869 184.09 433.799 177.35C434.479 175.73 435.639 174.35 437.119 173.4C439.879 171.31 442.269 168.77 444.179 165.89C446.869 162.12 449.209 158.11 451.169 153.91C451.879 152.64 452.509 151.33 453.059 149.98L466.889 142.06L495.229 147.67C493.989 152.36 494.589 155.98 496.019 156.58C497.449 157.18 499.379 154.87 499.809 154.32H499.819Z" fill="#FFCB9D"/>
+    <path d="M499.82 154.32C498.94 157.97 497.53 163.42 495.76 169.91C495.31 171.41 494.92 172.6 494.82 173.06C492.51 180.17 489.34 186.98 485.38 193.32C483.67 192.34 482.15 191.07 480.88 189.56C479.54 187.86 478.52 185.93 477.86 183.87C477.25 181.65 476.99 179.35 477.06 177.05C477.04 175.6 477.19 174.04 477.3 172.64C465.73 174.14 453.99 171.69 443.98 165.7C446.67 161.93 449.01 157.92 450.97 153.72C451.68 152.46 452.31 151.15 452.86 149.81L466.71 141.92L495.05 147.55C493.79 152.24 494.41 155.84 495.84 156.46C497.27 157.08 499.32 154.93 499.75 154.39L499.81 154.32H499.82Z" fill="black" opacity="0.1"/>
+    <path d="M502.129 161.14C500.829 167.02 497.349 172.19 492.399 175.62C491.339 176.2 490.409 176.82 489.499 177.43C487.089 179.44 485.209 182.02 484.029 184.93C482.989 183.13 482.559 181.03 482.819 178.97C483.279 175.87 485.819 170.34 485.969 169.56L486.059 169.1C486.159 168.64 486.299 168.19 486.479 167.76C486.929 166.26 487.239 164.72 487.389 163.16L487.369 163.02C479.279 166.03 456.659 172.53 445.059 159.42C443.269 157.35 441.739 155.06 440.509 152.62C440.349 152.41 440.239 152.17 440.169 151.92C439.869 151.31 439.619 150.68 439.429 150.02L439.309 149.84C438.709 148.15 438.409 146.36 438.429 144.56L438.359 143.92C438.359 143.13 438.519 142.35 438.819 141.63C439.769 138.76 441.719 136.32 444.319 134.77C449.099 131.57 455.379 130.43 458.839 129.85C471.899 127.35 490.339 129.9 498.619 142.92C500.609 146.29 501.819 150.07 502.149 153.98C502.429 156.4 502.379 158.86 501.989 161.27L502.119 161.16L502.129 161.14Z" fill="#232B41"/>
+    <path d="M487.429 163.01C479.339 166.02 456.719 172.52 445.119 159.41C443.329 157.34 441.799 155.05 440.569 152.61C440.409 152.4 440.299 152.16 440.229 151.91C439.929 151.3 439.679 150.67 439.489 150.01C439.509 149.93 439.459 149.85 439.389 149.83C439.389 149.83 439.379 149.83 439.369 149.83C438.769 148.14 438.469 146.35 438.489 144.55C441.129 148.31 444.339 151.64 448.009 154.41C466.199 167.88 489.669 160.28 493.399 159.12L493.749 159.08C491.729 160.43 489.549 161.76 487.379 163.05L487.419 163.01H487.429Z" fill="black" opacity="0.5"/>
+    <path d="M409.011 240.11L406.191 261.13C405.141 268.9 404.151 276.53 403.111 284.31C401.351 297.84 399.411 311.51 397.621 325.2L465.721 341.29C465.721 341.29 465.741 341.23 465.751 341.19H465.741C410.731 308.81 409.041 241.2 409.011 240.1V240.11Z" fill="black" opacity="0.1"/>
+    <path d="M450.512 211.62C454.092 217.27 459.422 221.85 465.752 224.46L466.472 224.75L478.172 218.76L450.512 211.62Z" fill="#FFCB9D"/>
+    <path d="M463.879 222.08L482.609 213.39L485.579 218.15L471.149 232.78L463.879 222.08Z" fill="#066FD1"/>
+    <path d="M463.011 221.76L440.971 201.42L434.281 205.57L447.131 230.99L463.011 221.76Z" fill="#066FD1"/>
+    <path d="M463.879 222.08L482.609 213.39L485.579 218.15L471.149 232.78L463.879 222.08Z" fill="black" opacity="0.1"/>
+    <path d="M527.36 336.65C525.94 363.48 524.52 390.31 523.11 417.14C520.18 417.59 517.25 418.03 514.33 418.48C506.97 401.18 499.61 383.89 492.25 366.61C477.98 362.44 463.7 358.27 449.43 354.09C439.88 370.71 430.35 387.82 420.86 405.41H383.43L397.63 325.21C409.56 327.99 425.47 330.33 444.1 329.3C445.76 329.21 447.4 329.09 449 328.95C451.33 328.75 453.59 328.5 455.78 328.22H455.8C461.28 327.51 466.34 326.58 470.95 325.54C477.03 326.72 483.11 327.9 489.19 329.09C501.96 331.59 514.67 334.11 527.36 336.65Z" fill="#066FD1"/>
+    <path d="M527.362 336.65C525.942 363.48 524.532 390.32 523.112 417.14C520.182 417.59 517.262 418.03 514.332 418.48C506.972 401.19 499.622 383.89 492.252 366.61C477.972 362.44 463.702 358.27 449.422 354.1C451.092 349.48 452.692 344.22 453.972 338.38C454.752 334.82 455.342 331.42 455.772 328.23C460.832 327.34 465.882 326.44 470.942 325.55C477.022 326.72 483.092 327.91 489.182 329.1C501.952 331.6 514.662 334.12 527.352 336.66L527.362 336.65Z" fill="black" opacity="0.1"/>
+    <path d="M453.858 431.36V435.11C453.848 436.53 452.688 437.69 451.268 437.69H287.148C285.728 437.69 284.578 436.53 284.578 435.11V431.34C284.578 429.92 285.718 428.76 287.148 428.76H324.248C327.578 428.3 332.318 426.27 333.338 419.21V412.72H405.018V419.16C406.028 426.25 410.808 428.25 414.108 428.76H451.268C452.698 428.76 453.858 429.93 453.858 431.36Z" fill="black" opacity="0.15"/>
+    <path d="M368.589 250.9C364.369 251.67 355.929 252.63 346.019 249.6C336.919 246.82 330.809 241.96 327.699 239.1C331.609 238.19 338.509 237.14 346.749 238.91C358.289 241.39 365.639 247.94 368.589 250.89V250.9Z" fill="#232B41"/>
+    <path d="M547.341 310.74C546.651 311.49 540.081 318.34 530.061 317.25C519.381 316.09 514.431 306.96 514.051 306.24C519.771 305.52 527.041 305.79 534.771 307.27C539.331 308.14 543.581 309.33 547.341 310.74Z" fill="#232B41"/>
+</svg>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 2 - 0
frontend/src/assets/images/logo.svg


+ 162 - 0
frontend/src/main.ts

@@ -0,0 +1,162 @@
+import '@tabler/core/dist/css/tabler.css';
+import '@tabler/core/dist/js/tabler.min';
+import '@tabler/icons-webfont/dist/tabler-icons.min.css';
+import './app.css';
+import {Exit, GetConfig, SaveConfig} from '../wailsjs/go/main/App';
+import {config} from '../wailsjs/go/models';
+
+function getServers($form: HTMLFormElement) {
+    const formData = new FormData($form);
+    const entries = Object.fromEntries(formData.entries());
+    let servers = new Array<string>;
+    for (const key in entries) {
+        const address = entries[key] as string;
+        if (address) {
+            servers.push(address);
+        }
+    }
+    return servers
+}
+
+// 显示渲染异常
+function showPanicError() {
+    document.body.innerHTML = `<b>Panic Error</b>`;
+}
+
+// 显示引导设置完毕界面
+function showBootstrapDone($container: Element) {
+    $container.innerHTML = `
+        <div id="app-prepare-done">
+            <div class="card card-md">
+                <div class="card-body">
+                    <h2 class="mb-3">完成配置</h2>
+                    <p class="text-secondary mb-4">所有配置均已完成,客户端需要重新启动后才可正常使用。点击下方<b>退出</b>按钮继续
+                    </p>
+                    <div class="my-4">
+                        <button class="btn btn-success w-100" id="app-exit"> 退出 </button>
+                    </div>
+                    <p class="text-secondary">退出后请在桌面找到客户端图标重新打开即可</p>
+                </div>
+            </div>
+        </div>    
+    `
+    const $exitBtn = document.getElementById('app-exit') as HTMLElement;
+    $exitBtn?.addEventListener('click', (_e) => {
+        Exit().then()
+    })
+}
+
+// 显示引导设置界面
+function showBootstrapView($container: Element) {
+    $container.innerHTML = `
+<div id="app-prepare">
+   <div class="card card-md">
+      <div class="card-body text-center py-4 p-sm-5">
+         <img src="../src/assets/fonts/illustrations/download.svg" height="250" width="250" alt="index">
+         <h1 class="mt-3">快速开始</h1>
+         <p class="text-secondary">
+            欢迎使用山东西曼克技术有限公司 ( SIMANC ) 开发的穿梭车立体仓库调度系统 ( WCS )
+         </p>
+      </div>
+      <div class="hr-text hr-text-center hr-text-spaceless">设置</div>
+      <div class="card-body">
+         <form id="serverForm" novalidate>
+            <div class="mb-2">
+               <label class="form-label required" for="server1">主服务器地址</label>
+               <div class="input-group input-group-flat">
+                  <span class="input-group-text">http://</span>
+                  <input type="text" name="server1" id="server1" class="form-control ps-1" autocomplete="off" placeholder="192.168.111.200:80" required>
+               </div>
+               <div class="form-hint">
+                  请输入 WCS 部署在内网的服务器 IP 地址
+               </div>
+            </div>
+            <div class="accordion accordion-flush" id="accordion-flush">
+               <div class="accordion-item">
+                  <div class="accordion-header">
+                     <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-1-flush" aria-expanded="false">
+                     备用服务器(可选)
+                     <span class="accordion-button-toggle"><i class="ti ti-chevron-down"></i></span>
+                     </button>
+                  </div>
+                  <div id="collapse-1-flush" class="accordion-collapse collapse" data-bs-parent="#accordion-flush" style="">
+                     <div class="accordion-body">
+                        <div class="input-group input-group-flat">
+                           <span class="input-group-text">http:// </span>
+                           <input type="text" name="server2" id="server2" class="form-control ps-1" autocomplete="off" placeholder="192.168.111.201:80">
+                        </div>
+                     </div>
+                  </div>
+               </div>
+            </div>
+         </form>
+      </div>
+   </div>
+   <div class="row align-items-center mt-3">
+      <div class="col-4">
+         <div class="progress progress-1">
+            <div class="progress-bar" style="width: 25%" role="progressbar" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100" aria-label="25% Complete">
+               <span class="visually-hidden">25% Complete</span>
+            </div>
+         </div>
+      </div>
+      <div class="col">
+         <div class="btn-list justify-content-end">
+            <button class="btn btn-primary btn-2" id="formNext"> 下一步 </button>
+         </div>
+      </div>
+   </div>
+</div>    
+    `
+
+    const $form = $container.querySelector('#serverForm') as HTMLFormElement;
+    const $formNext = $container.querySelector('#formNext');
+    $formNext?.addEventListener('click', (_e) => {
+        if (!$form.reportValidity()) {
+            console.log('校验失败');
+            return;
+        }
+        const servers = getServers($form);
+        SaveConfig({ servers }).then(() => {
+            showBootstrapDone($container)
+        })
+    })
+}
+
+// 加载服务器时的 Loading
+function showServerLoading($container: Element) {
+    $container.innerHTML = `
+        <div id="app-loading">
+            <div class="text-center">
+                <div class="text-secondary mb-3">正在加载</div>
+                <div class="progress progress-sm">
+                    <div class="progress-bar progress-bar-indeterminate"></div>
+                </div>
+            </div>
+        </div>    
+    `
+}
+
+function setURL(server: string) {
+    document.location.replace(`http://${server}`)
+}
+
+// main 函数
+document.addEventListener('DOMContentLoaded', () => {
+    const $container = document.querySelector('.container');
+    if (!$container) {
+        showPanicError();
+        return;
+    }
+    // 先尝试获取配置文件, 如果配置文件不存在, 则表明为首次加载
+    GetConfig().then((config: config.Config) => {
+        console.log(config);
+        showServerLoading($container)
+        // TODO 需要选取服务器地址后再设定
+        setURL(config.servers[0])
+    }).catch((error: Error) => {
+        // 显示引导
+        console.log(`首次启动: ${error}`);
+        showBootstrapView($container)
+    })
+})

+ 12 - 0
frontend/src/types/tabler.d.ts

@@ -0,0 +1,12 @@
+declare module '@tabler/core/dist/js/tabler.min' {
+    const Tabler: Window['Tabler'];
+    export = Tabler;
+}
+
+declare global {
+    interface Window {
+        Tabler: any;
+    }
+}
+
+export {};

+ 1 - 0
frontend/src/vite-env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 26 - 0
frontend/tsconfig.json

@@ -0,0 +1,26 @@
+{
+  "compilerOptions": {
+    "target": "ESNext",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "lib": [
+      "ESNext",
+      "DOM",
+      "dom.iterable"
+    ],
+    "moduleResolution": "Node",
+    "strict": true,
+    "sourceMap": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "esModuleInterop": true,
+    "noEmit": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noImplicitReturns": true,
+    "skipLibCheck": true
+  },
+  "include": [
+    "src"
+  ]
+}

+ 9 - 0
frontend/wailsjs/go/main/App.d.ts

@@ -0,0 +1,9 @@
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+import {config} from '../models';
+
+export function Exit():Promise<void>;
+
+export function GetConfig():Promise<config.Config>;
+
+export function SaveConfig(arg1:config.Config):Promise<void>;

+ 15 - 0
frontend/wailsjs/go/main/App.js

@@ -0,0 +1,15 @@
+// @ts-check
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+export function Exit() {
+  return window['go']['main']['App']['Exit']();
+}
+
+export function GetConfig() {
+  return window['go']['main']['App']['GetConfig']();
+}
+
+export function SaveConfig(arg1) {
+  return window['go']['main']['App']['SaveConfig'](arg1);
+}

+ 17 - 0
frontend/wailsjs/go/models.ts

@@ -0,0 +1,17 @@
+export namespace config {
+	
+	export class Config {
+	    servers: string[];
+	
+	    static createFrom(source: any = {}) {
+	        return new Config(source);
+	    }
+	
+	    constructor(source: any = {}) {
+	        if ('string' === typeof source) source = JSON.parse(source);
+	        this.servers = source["servers"];
+	    }
+	}
+
+}
+

+ 24 - 0
frontend/wailsjs/runtime/package.json

@@ -0,0 +1,24 @@
+{
+  "name": "@wailsapp/runtime",
+  "version": "2.0.0",
+  "description": "Wails Javascript runtime library",
+  "main": "runtime.js",
+  "types": "runtime.d.ts",
+  "scripts": {
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/wailsapp/wails.git"
+  },
+  "keywords": [
+    "Wails",
+    "Javascript",
+    "Go"
+  ],
+  "author": "Lea Anthony <lea.anthony@gmail.com>",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/wailsapp/wails/issues"
+  },
+  "homepage": "https://github.com/wailsapp/wails#readme"
+}

+ 249 - 0
frontend/wailsjs/runtime/runtime.d.ts

@@ -0,0 +1,249 @@
+/*
+ _       __      _ __
+| |     / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__  )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export interface Position {
+    x: number;
+    y: number;
+}
+
+export interface Size {
+    w: number;
+    h: number;
+}
+
+export interface Screen {
+    isCurrent: boolean;
+    isPrimary: boolean;
+    width : number
+    height : number
+}
+
+// Environment information such as platform, buildtype, ...
+export interface EnvironmentInfo {
+    buildType: string;
+    platform: string;
+    arch: string;
+}
+
+// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
+// emits the given event. Optional data may be passed with the event.
+// This will trigger any event listeners.
+export function EventsEmit(eventName: string, ...data: any): void;
+
+// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
+export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
+
+// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
+// sets up a listener for the given event name, but will only trigger a given number times.
+export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
+
+// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
+// sets up a listener for the given event name, but will only trigger once.
+export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
+
+// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
+// unregisters the listener for the given event name.
+export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
+
+// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
+// unregisters all listeners.
+export function EventsOffAll(): void;
+
+// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
+// logs the given message as a raw message
+export function LogPrint(message: string): void;
+
+// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
+// logs the given message at the `trace` log level.
+export function LogTrace(message: string): void;
+
+// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
+// logs the given message at the `debug` log level.
+export function LogDebug(message: string): void;
+
+// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
+// logs the given message at the `error` log level.
+export function LogError(message: string): void;
+
+// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
+// logs the given message at the `fatal` log level.
+// The application will quit after calling this method.
+export function LogFatal(message: string): void;
+
+// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
+// logs the given message at the `info` log level.
+export function LogInfo(message: string): void;
+
+// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
+// logs the given message at the `warning` log level.
+export function LogWarning(message: string): void;
+
+// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
+// Forces a reload by the main application as well as connected browsers.
+export function WindowReload(): void;
+
+// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
+// Reloads the application frontend.
+export function WindowReloadApp(): void;
+
+// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
+// Sets the window AlwaysOnTop or not on top.
+export function WindowSetAlwaysOnTop(b: boolean): void;
+
+// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
+// *Windows only*
+// Sets window theme to system default (dark/light).
+export function WindowSetSystemDefaultTheme(): void;
+
+// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
+// *Windows only*
+// Sets window to light theme.
+export function WindowSetLightTheme(): void;
+
+// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
+// *Windows only*
+// Sets window to dark theme.
+export function WindowSetDarkTheme(): void;
+
+// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
+// Centers the window on the monitor the window is currently on.
+export function WindowCenter(): void;
+
+// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
+// Sets the text in the window title bar.
+export function WindowSetTitle(title: string): void;
+
+// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
+// Makes the window full screen.
+export function WindowFullscreen(): void;
+
+// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
+// Restores the previous window dimensions and position prior to full screen.
+export function WindowUnfullscreen(): void;
+
+// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
+// Returns the state of the window, i.e. whether the window is in full screen mode or not.
+export function WindowIsFullscreen(): Promise<boolean>;
+
+// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
+// Sets the width and height of the window.
+export function WindowSetSize(width: number, height: number): void;
+
+// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
+// Gets the width and height of the window.
+export function WindowGetSize(): Promise<Size>;
+
+// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
+// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMaxSize(width: number, height: number): void;
+
+// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
+// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMinSize(width: number, height: number): void;
+
+// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
+// Sets the window position relative to the monitor the window is currently on.
+export function WindowSetPosition(x: number, y: number): void;
+
+// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
+// Gets the window position relative to the monitor the window is currently on.
+export function WindowGetPosition(): Promise<Position>;
+
+// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
+// Hides the window.
+export function WindowHide(): void;
+
+// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
+// Shows the window, if it is currently hidden.
+export function WindowShow(): void;
+
+// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
+// Maximises the window to fill the screen.
+export function WindowMaximise(): void;
+
+// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
+// Toggles between Maximised and UnMaximised.
+export function WindowToggleMaximise(): void;
+
+// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
+// Restores the window to the dimensions and position prior to maximising.
+export function WindowUnmaximise(): void;
+
+// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
+// Returns the state of the window, i.e. whether the window is maximised or not.
+export function WindowIsMaximised(): Promise<boolean>;
+
+// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
+// Minimises the window.
+export function WindowMinimise(): void;
+
+// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
+// Restores the window to the dimensions and position prior to minimising.
+export function WindowUnminimise(): void;
+
+// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
+// Returns the state of the window, i.e. whether the window is minimised or not.
+export function WindowIsMinimised(): Promise<boolean>;
+
+// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
+// Returns the state of the window, i.e. whether the window is normal or not.
+export function WindowIsNormal(): Promise<boolean>;
+
+// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
+// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
+export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
+
+// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
+// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
+export function ScreenGetAll(): Promise<Screen[]>;
+
+// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
+// Opens the given URL in the system browser.
+export function BrowserOpenURL(url: string): void;
+
+// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
+// Returns information about the environment
+export function Environment(): Promise<EnvironmentInfo>;
+
+// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
+// Quits the application.
+export function Quit(): void;
+
+// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
+// Hides the application.
+export function Hide(): void;
+
+// [Show](https://wails.io/docs/reference/runtime/intro#show)
+// Shows the application.
+export function Show(): void;
+
+// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
+// Returns the current text stored on clipboard
+export function ClipboardGetText(): Promise<string>;
+
+// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
+// Sets a text on the clipboard
+export function ClipboardSetText(text: string): Promise<boolean>;
+
+// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
+// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
+export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
+
+// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
+// OnFileDropOff removes the drag and drop listeners and handlers.
+export function OnFileDropOff() :void
+
+// Check if the file path resolver is available
+export function CanResolveFilePaths(): boolean;
+
+// Resolves file paths for an array of files
+export function ResolveFilePaths(files: File[]): void

+ 242 - 0
frontend/wailsjs/runtime/runtime.js

@@ -0,0 +1,242 @@
+/*
+ _       __      _ __
+| |     / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__  )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export function LogPrint(message) {
+    window.runtime.LogPrint(message);
+}
+
+export function LogTrace(message) {
+    window.runtime.LogTrace(message);
+}
+
+export function LogDebug(message) {
+    window.runtime.LogDebug(message);
+}
+
+export function LogInfo(message) {
+    window.runtime.LogInfo(message);
+}
+
+export function LogWarning(message) {
+    window.runtime.LogWarning(message);
+}
+
+export function LogError(message) {
+    window.runtime.LogError(message);
+}
+
+export function LogFatal(message) {
+    window.runtime.LogFatal(message);
+}
+
+export function EventsOnMultiple(eventName, callback, maxCallbacks) {
+    return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
+}
+
+export function EventsOn(eventName, callback) {
+    return EventsOnMultiple(eventName, callback, -1);
+}
+
+export function EventsOff(eventName, ...additionalEventNames) {
+    return window.runtime.EventsOff(eventName, ...additionalEventNames);
+}
+
+export function EventsOffAll() {
+  return window.runtime.EventsOffAll();
+}
+
+export function EventsOnce(eventName, callback) {
+    return EventsOnMultiple(eventName, callback, 1);
+}
+
+export function EventsEmit(eventName) {
+    let args = [eventName].slice.call(arguments);
+    return window.runtime.EventsEmit.apply(null, args);
+}
+
+export function WindowReload() {
+    window.runtime.WindowReload();
+}
+
+export function WindowReloadApp() {
+    window.runtime.WindowReloadApp();
+}
+
+export function WindowSetAlwaysOnTop(b) {
+    window.runtime.WindowSetAlwaysOnTop(b);
+}
+
+export function WindowSetSystemDefaultTheme() {
+    window.runtime.WindowSetSystemDefaultTheme();
+}
+
+export function WindowSetLightTheme() {
+    window.runtime.WindowSetLightTheme();
+}
+
+export function WindowSetDarkTheme() {
+    window.runtime.WindowSetDarkTheme();
+}
+
+export function WindowCenter() {
+    window.runtime.WindowCenter();
+}
+
+export function WindowSetTitle(title) {
+    window.runtime.WindowSetTitle(title);
+}
+
+export function WindowFullscreen() {
+    window.runtime.WindowFullscreen();
+}
+
+export function WindowUnfullscreen() {
+    window.runtime.WindowUnfullscreen();
+}
+
+export function WindowIsFullscreen() {
+    return window.runtime.WindowIsFullscreen();
+}
+
+export function WindowGetSize() {
+    return window.runtime.WindowGetSize();
+}
+
+export function WindowSetSize(width, height) {
+    window.runtime.WindowSetSize(width, height);
+}
+
+export function WindowSetMaxSize(width, height) {
+    window.runtime.WindowSetMaxSize(width, height);
+}
+
+export function WindowSetMinSize(width, height) {
+    window.runtime.WindowSetMinSize(width, height);
+}
+
+export function WindowSetPosition(x, y) {
+    window.runtime.WindowSetPosition(x, y);
+}
+
+export function WindowGetPosition() {
+    return window.runtime.WindowGetPosition();
+}
+
+export function WindowHide() {
+    window.runtime.WindowHide();
+}
+
+export function WindowShow() {
+    window.runtime.WindowShow();
+}
+
+export function WindowMaximise() {
+    window.runtime.WindowMaximise();
+}
+
+export function WindowToggleMaximise() {
+    window.runtime.WindowToggleMaximise();
+}
+
+export function WindowUnmaximise() {
+    window.runtime.WindowUnmaximise();
+}
+
+export function WindowIsMaximised() {
+    return window.runtime.WindowIsMaximised();
+}
+
+export function WindowMinimise() {
+    window.runtime.WindowMinimise();
+}
+
+export function WindowUnminimise() {
+    window.runtime.WindowUnminimise();
+}
+
+export function WindowSetBackgroundColour(R, G, B, A) {
+    window.runtime.WindowSetBackgroundColour(R, G, B, A);
+}
+
+export function ScreenGetAll() {
+    return window.runtime.ScreenGetAll();
+}
+
+export function WindowIsMinimised() {
+    return window.runtime.WindowIsMinimised();
+}
+
+export function WindowIsNormal() {
+    return window.runtime.WindowIsNormal();
+}
+
+export function BrowserOpenURL(url) {
+    window.runtime.BrowserOpenURL(url);
+}
+
+export function Environment() {
+    return window.runtime.Environment();
+}
+
+export function Quit() {
+    window.runtime.Quit();
+}
+
+export function Hide() {
+    window.runtime.Hide();
+}
+
+export function Show() {
+    window.runtime.Show();
+}
+
+export function ClipboardGetText() {
+    return window.runtime.ClipboardGetText();
+}
+
+export function ClipboardSetText(text) {
+    return window.runtime.ClipboardSetText(text);
+}
+
+/**
+ * Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
+ *
+ * @export
+ * @callback OnFileDropCallback
+ * @param {number} x - x coordinate of the drop
+ * @param {number} y - y coordinate of the drop
+ * @param {string[]} paths - A list of file paths.
+ */
+
+/**
+ * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
+ *
+ * @export
+ * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
+ * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
+ */
+export function OnFileDrop(callback, useDropTarget) {
+    return window.runtime.OnFileDrop(callback, useDropTarget);
+}
+
+/**
+ * OnFileDropOff removes the drag and drop listeners and handlers.
+ */
+export function OnFileDropOff() {
+    return window.runtime.OnFileDropOff();
+}
+
+export function CanResolveFilePaths() {
+    return window.runtime.CanResolveFilePaths();
+}
+
+export function ResolveFilePaths(files) {
+    return window.runtime.ResolveFilePaths(files);
+}

+ 35 - 0
go.mod

@@ -0,0 +1,35 @@
+module SIMANC-WCS
+
+go 1.25.5
+
+require github.com/wailsapp/wails/v2 v2.11.0
+
+require (
+	github.com/bep/debounce v1.2.1 // indirect
+	github.com/go-ole/go-ole v1.3.0 // indirect
+	github.com/godbus/dbus/v5 v5.2.0 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/gorilla/websocket v1.5.3 // indirect
+	github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
+	github.com/labstack/echo/v4 v4.14.0 // indirect
+	github.com/labstack/gommon v0.4.2 // indirect
+	github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
+	github.com/leaanthony/gosod v1.0.4 // indirect
+	github.com/leaanthony/slicer v1.6.0 // indirect
+	github.com/leaanthony/u v1.1.1 // indirect
+	github.com/mattn/go-colorable v0.1.14 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/rivo/uniseg v0.4.7 // indirect
+	github.com/samber/lo v1.52.0 // indirect
+	github.com/tkrajina/go-reflector v0.5.8 // indirect
+	github.com/valyala/bytebufferpool v1.0.0 // indirect
+	github.com/valyala/fasttemplate v1.2.2 // indirect
+	github.com/wailsapp/go-webview2 v1.0.23 // indirect
+	github.com/wailsapp/mimetype v1.4.1 // indirect
+	golang.org/x/crypto v0.46.0 // indirect
+	golang.org/x/net v0.48.0 // indirect
+	golang.org/x/sys v0.39.0 // indirect
+	golang.org/x/text v0.32.0 // indirect
+)

+ 79 - 0
go.sum

@@ -0,0 +1,79 @@
+github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
+github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
+github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
+github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
+github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
+github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
+github.com/labstack/echo/v4 v4.14.0 h1:+tiMrDLxwv6u0oKtD03mv+V1vXXB3wCqPHJqPuIe+7M=
+github.com/labstack/echo/v4 v4.14.0/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
+github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
+github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
+github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
+github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
+github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
+github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
+github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
+github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
+github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
+github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
+github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
+github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
+github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
+github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
+github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
+github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0=
+github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
+github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
+github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
+github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ=
+github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k=
+golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
+golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
+golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
+golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
+golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 44 - 0
lib/server/utils.go

@@ -0,0 +1,44 @@
+package server
+
+import (
+	"context"
+	"errors"
+	"net"
+	"sync"
+	"time"
+)
+
+// 尝试连接到 address, 连接成功后不会发送任何数据
+func pingServer(ctx context.Context, address string) error {
+	dialer := net.Dialer{
+		Timeout:         2*time.Second,
+	}
+	conn, err := dialer.DialContext(ctx, "tcp", address)
+	if err != nil {
+		return err
+	}
+	_ = conn.Close()
+	return nil
+}
+
+// ConnectTest 尝试连接服务器是否连通
+func ConnectTest(ctx context.Context, servers []string) error {
+	errs := make(chan error, len(servers))
+	var wg sync.WaitGroup
+	wg.Add(len(servers))
+	for _, server := range servers {
+		wg.Go(func() {
+			if err := pingServer(ctx, server); err != nil {
+				errs <- err
+			}
+			wg.Done()
+		})
+	}
+	wg.Wait()
+	close(errs)
+	var retErr error
+	for err := range errs {
+		retErr = errors.Join(retErr, err)
+	}
+	return retErr
+}

+ 35 - 0
main.go

@@ -0,0 +1,35 @@
+package main
+
+import (
+	"embed"
+	
+	"github.com/wailsapp/wails/v2"
+	"github.com/wailsapp/wails/v2/pkg/options"
+	"github.com/wailsapp/wails/v2/pkg/options/assetserver"
+)
+
+//go:embed all:frontend/dist
+var assets embed.FS
+
+func main() {
+	app := NewApp()
+	err := wails.Run(&options.App{
+		Title:  "",
+		Width:  1200,
+		Height: 840,
+		MinWidth: 1200,
+		MinHeight: 840,
+		AssetServer: &assetserver.Options{
+			Assets: assets,
+		},
+		// BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
+		OnStartup:        app.startup,
+		Bind: []interface{}{
+			app,
+		},
+	})
+
+	if err != nil {
+		println("Error:", err.Error())
+	}
+}

+ 18 - 0
wails.json

@@ -0,0 +1,18 @@
+{
+  "$schema": "https://wails.io/schemas/config.v2.json",
+  "name": "SIMANC WCS",
+  "outputfilename": "simanc-wcs",
+  "frontend:install": "pnpm install",
+  "frontend:build": "pnpm run build",
+  "frontend:dev:watcher": "pnpm run dev",
+  "frontend:dev:serverUrl": "auto",
+  "author": {
+    "name": "SIMANC"
+  },
+  "info": {
+    "companyName": "山东西曼克技术有限公司",
+    "productName": "SIMANC WCS",
+    "copyright": "©SIMANC",
+    "comments": "精准搬运 · 高效流转"
+  }
+}

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio