commit 2cef098632facf1341a9e9480ef3864bd11396fa Author: RainySY Date: Thu May 7 01:11:05 2026 +0800 feat: Initialize data-matcher project with Wails framework - Added frontend runtime JavaScript functions for logging, window management, and notifications. - Created Go module with dependencies for Wails and Excel processing. - Implemented main application entry point with embedded frontend assets. - Configured Wails application settings in wails.json. Co-authored-by: Copilot diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be14f9c --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# --- Build output --- +build/bin +build/*.exe +build/*.app + +# --- Frontend --- +node_modules/ +frontend/dist +frontend/node_modules/ +frontend/package-lock.json + +# --- Go --- +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +vendor/ + +# --- IDE --- +.idea/ +*.swp +*.swo +*~ +.DS_Store +Thumbs.db + +# VS Code(保留 .vscode/extensions.json 以便共享推荐) +.vscode/* +!.vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..996e842 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "golang.go", + "vue.volar", + "vue.vscode-typescript-vue-plugin", + "wails.wails" + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4fc519 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# 数据智能匹配工具 (Data Matcher) + +基于 **Wails v2** (Go + Vue 3) 的桌面端数据匹配工具,支持 Excel/CSV 文件的智能列映射、模糊匹配与 AI 增强匹配。 + +## 功能特性 + +- 📂 **多文件支持** — 读取 `.xlsx` / `.xls` / `.csv` 格式 +- 🔗 **动态列映射** — 自动解析表头,前端动态选择匹配列、时间列、提取列 +- 🧹 **正则清洗** — 自定义正则剔除干扰字符(默认保留纯中文) +- ⏱ **时间窗口剪枝** — 按时间差过滤候选记录,提升匹配效率 +- 📊 **Levenshtein 模糊匹配** — 基于编辑距离的相似度计算 +- 🤖 **Deepseek AI 增强** — 对基础匹配未命中的记录,调用 Deepseek API 二次匹配 +- 📤 **结果导出** — 支持导出为 Excel (`.xlsx`) 格式 + +## 技术栈 + +| 层级 | 技术 | +| -------- | ----------------------------- | +| 后端 | Go 1.24 + Wails v2.12.0 | +| 前端 | Vue 3 (Composition API) + Vite | +| Excel | excelize v2.10.1 | +| AI API | Deepseek Chat API | + +## 快速开始 + +### 前置要求 + +- Go 1.24+ +- Node.js 18+ +- Wails CLI v2.12.0 + +```bash +go install github.com/wailsapp/wails/v2/cmd/wails@latest +``` + +### 开发模式 + +```bash +# 安装前端依赖 +cd frontend && npm install + +# 启动热重载开发服务器 +wails dev +``` + +开发模式下: +- Vite 热重载服务器运行在 `http://localhost:5173` +- Go 方法浏览器调试入口 `http://localhost:34115` + +### 构建 + +```bash +wails build +``` + +构建产物位于 `build/bin/` 目录。 + +## 使用指南 + +1. 点击「选择文件」分别加载 A 表(基准表)和 B 表(数据源表) +2. 自动识别表头后,在下拉框中配置列映射: + - **匹配列** — 用于模糊匹配的文本列 + - **时间列** — 可选,用于时间窗口剪枝 + - **提取列** — 从 B 表提取到结果的目标列 +3. 点击「开始智能匹配」运行基础算法匹配 +4. 可选:配置 Deepseek API 密钥后使用「AI 增强匹配」补充未命中记录 +5. 点击「导出结果」保存为 Excel 文件 + +## 项目结构 + +``` +data-matcher/ +├── app.go # 核心逻辑(匹配引擎、文件读写、AI 调用) +├── main.go # 应用入口 +├── wails.json # Wails 项目配置 +├── go.mod / go.sum # Go 依赖 +├── frontend/ +│ ├── src/ +│ │ ├── App.vue # 主界面(Vue 组件) +│ │ ├── style.css # 全局样式 +│ │ └── main.js # Vue 入口 +│ ├── index.html +│ ├── vite.config.js +│ └── package.json +└── build/ # 构建配置(Windows/macOS 安装包) +``` diff --git a/app.go b/app.go new file mode 100644 index 0000000..c7a82a7 --- /dev/null +++ b/app.go @@ -0,0 +1,1203 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "math" + "net/http" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "time" + + "github.com/wailsapp/wails/v2/pkg/runtime" + "github.com/xuri/excelize/v2" +) + +// ---------- 数据结构 ---------- + +// MonthlyRecord 月报记录(向后兼容) +type MonthlyRecord struct { + CellName string `json:"cellName"` + OccurTime time.Time `json:"-"` + OccurTimeStr string `json:"occurTimeStr"` +} + +// DailyRecord 日报记录(向后兼容) +type DailyRecord struct { + CellID string `json:"cellId"` + OccurTime time.Time `json:"-"` + OccurTimeStr string `json:"occurTimeStr"` + InterruptReason string `json:"interruptReason"` +} + +// MatchResult 匹配结果(新旧字段兼容) +type MatchResult struct { + // 新字段(通用化) + RowAData []string `json:"rowAData"` // A 表原始所有列(新) + RowBKey string `json:"rowBKey"` // B 表匹配列的值(新) + ExtractValue string `json:"extractValue"` // 从 B 表提取的目标列值(新) + + // 旧字段(向后兼容) + MonthlyCellName string `json:"monthlyCellName"` + DailyCellID string `json:"dailyCellId"` + InterruptReason string `json:"interruptReason"` + + // 公共字段 + TimeDiff string `json:"timeDiff"` + SimilarityScore float64 `json:"similarityScore"` + AIMatched bool `json:"aiMatched"` +} + +// ProgressPayload 进度信息 +type ProgressPayload struct { + Current int `json:"current"` + Total int `json:"total"` + Message string `json:"message"` + Phase string `json:"phase"` // reading / matching / ai-enhancing / done +} + +// MatchConfig 前端传递的完整匹配配置 +type MatchConfig struct { + // 文件路径 + FileAPath string `json:"fileAPath"` + FileBPath string `json:"fileBPath"` + + // A 表列索引(-1 表示不使用) + ColAMatchIndex int `json:"colAMatchIndex"` // A 表匹配列 + ColATimeIndex int `json:"colATimeIndex"` // A 表时间列(可选,-1 跳过时间剪枝) + + // B 表列索引 + ColBMatchIndex int `json:"colBMatchIndex"` // B 表匹配列 + ColBTimeIndex int `json:"colBTimeIndex"` // B 表时间列(可选,-1 跳过时间剪枝) + ColBExtractIndex int `json:"colBExtractIndex"` // B 表要提取的目标列 + + // 清洗与匹配参数 + RegexPattern string `json:"regexPattern"` // 空字符串 = 跳过清洗 + TimeWindow float64 `json:"timeWindow"` // 小时 + Threshold float64 `json:"threshold"` // 0.0 - 1.0 + + // 扩展选项 + AllMatches bool `json:"allMatches"` // true=返回该A行所有匹配(>=阈值)而非仅最佳 + CaseSensitive bool `json:"caseSensitive"` // true=大小写敏感匹配 + SortBy string `json:"sortBy"` // "similarity" / "timeDiff" / ""=不排序 + MaxPreview int `json:"maxPreview"` // 调试日志中打印的前 N 条比对详情,0=不打印 + ExportFormat string `json:"exportFormat"` // "xlsx"(默认) / "csv" + IncludeHeader bool `json:"includeHeader"` // 导出时是否包含表头行 +} + +// ---------- Deepseek API 类型 ---------- + +type deepseekMessage struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type deepseekRequest struct { + Model string `json:"model"` + Messages []deepseekMessage `json:"messages"` + Temperature float64 `json:"temperature"` + MaxTokens int `json:"max_tokens,omitempty"` +} + +type deepseekResponse struct { + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` + Error *struct { + Message string `json:"message"` + } `json:"error,omitempty"` +} + +// ---------- App 结构体 ---------- + +type App struct { + ctx context.Context + deepseekKey string +} + +// NewApp 创建 App 实例 +func NewApp() *App { + return &App{} +} + +// startup 保存上下文 +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// emitProgress 向前端发送进度事件 +func (a *App) emitProgress(current, total int, message, phase string) { + if a.ctx == nil { + return + } + runtime.EventsEmit(a.ctx, "match-progress", ProgressPayload{ + Current: current, + Total: total, + Message: message, + Phase: phase, + }) +} + +// SetDeepseekAPIKey 设置 Deepseek API 密钥(仅保存在内存中) +func (a *App) SetDeepseekAPIKey(key string) string { + a.deepseekKey = strings.TrimSpace(key) + if a.deepseekKey == "" { + return "已清除 Deepseek API 密钥" + } + return "Deepseek API 密钥已设置" +} + +// GetDeepseekStatus 返回是否已配置 Deepseek API 密钥 +func (a *App) GetDeepseekStatus() bool { + return a.deepseekKey != "" +} + +// ---------- 文件选择对话框 ---------- + +// OpenMonthlyReport 打开文件对话框选择月报 +func (a *App) OpenMonthlyReport() (string, error) { + file, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ + Title: "选择月报文件", + Filters: []runtime.FileFilter{ + {DisplayName: "Excel / CSV 文件 (*.xlsx, *.xls, *.csv)", Pattern: "*.xlsx;*.xls;*.csv"}, + }, + }) + if err != nil { + return "", err + } + return file, nil +} + +// OpenDailyReport 打开文件对话框选择日报 +func (a *App) OpenDailyReport() (string, error) { + file, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ + Title: "选择日报文件", + Filters: []runtime.FileFilter{ + {DisplayName: "Excel / CSV 文件 (*.xlsx, *.xls, *.csv)", Pattern: "*.xlsx;*.xls;*.csv"}, + }, + }) + if err != nil { + return "", err + } + return file, nil +} + +// OpenFileA 打开文件对话框选择 A 表(基准表) +func (a *App) OpenFileA() (string, error) { + return a.openFileDialog("选择 A 表文件(基准表)") +} + +// OpenFileB 打开文件对话框选择 B 表(数据源表) +func (a *App) OpenFileB() (string, error) { + return a.openFileDialog("选择 B 表文件(数据源表)") +} + +func (a *App) openFileDialog(title string) (string, error) { + file, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ + Title: title, + Filters: []runtime.FileFilter{ + {DisplayName: "Excel / CSV 文件 (*.xlsx, *.xls, *.csv)", Pattern: "*.xlsx;*.xls;*.csv"}, + }, + }) + if err != nil { + return "", err + } + return file, nil +} + +// ParseHeaders 读取文件第一行作为表头数组返回给前端,用于动态渲染列映射下拉框 +func (a *App) ParseHeaders(filePath string) ([]string, error) { + if filePath == "" { + return nil, fmt.Errorf("文件路径为空") + } + allRows, err := a.readRawRows(filePath) + if err != nil { + return nil, err + } + if len(allRows) == 0 { + return nil, fmt.Errorf("文件为空,无表头") + } + headers := allRows[0] + // TrimSpace 每个表头 + for i := range headers { + headers[i] = strings.TrimSpace(headers[i]) + } + fmt.Printf("[DEBUG] ParseHeaders: '%s' → %d 列 %v\n", filepath.Base(filePath), len(headers), headers) + return headers, nil +} + +var nonChineseRegex = regexp.MustCompile(`[^\p{Han}]+`) + +// CleanString 剔除字符串中的所有非中文字符,仅保留纯中文字符 +func (a *App) CleanString(input string) string { + return nonChineseRegex.ReplaceAllString(input, "") +} + +// ---------- 健壮的时间解析 ---------- + +// 多种时间格式,覆盖月报和日报的不同格式 +var timeFormats = []string{ + "2006-01-02 15:04:05", + "2006-01-02 15:04", + "2006/01/02 15:04:05", + "2006/01/02 15:04", + "2006-1-2 15:04:05", + "2006-1-2 15:04", + "2006/1/2 15:04:05", + "2006/1/2 15:04", + "2006-01-02T15:04:05", + "2006/01/02T15:04:05", + "01/02/2006 15:04", + "1/2/2006 15:04", + "2006-01-02", + "2006/01/02", +} + +// parseTimeFlexible 使用多种格式尝试解析时间字符串 +func parseTimeFlexible(timeStr string) (time.Time, error) { + timeStr = strings.TrimSpace(timeStr) + for _, format := range timeFormats { + if t, err := time.Parse(format, timeStr); err == nil { + return t, nil + } + } + return time.Time{}, fmt.Errorf("无法解析时间格式: %s", timeStr) +} + +// ---------- Levenshtein 距离算法 ---------- + +func levenshteinDistance(s1, s2 string) int { + runes1 := []rune(s1) + runes2 := []rune(s2) + m, n := len(runes1), len(runes2) + + // 使用一维数组优化空间复杂度 + dp := make([]int, n+1) + for j := range dp { + dp[j] = j + } + + for i := 1; i <= m; i++ { + prev := dp[0] + dp[0] = i + for j := 1; j <= n; j++ { + temp := dp[j] + cost := 1 + if runes1[i-1] == runes2[j-1] { + cost = 0 + } + dp[j] = min(dp[j]+1, min(dp[j-1]+1, prev+cost)) + prev = temp + } + } + return dp[n] +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +// CalculateSimilarity 计算清洗后中文名称的相似度(基于 Levenshtein 距离归一化) +func (a *App) CalculateSimilarity(s1, s2 string) float64 { + return calcSimilarity(s1, s2, nonChineseRegex) +} + +// calcSimilarity 带自定义正则的相似度计算;reg 为 nil 时不做清洗直接比对 +func calcSimilarity(s1, s2 string, reg *regexp.Regexp) float64 { + clean1 := s1 + clean2 := s2 + if reg != nil { + clean1 = reg.ReplaceAllString(s1, "") + clean2 = reg.ReplaceAllString(s2, "") + } + + r1 := []rune(clean1) + r2 := []rune(clean2) + + if len(r1) == 0 && len(r2) == 0 { + return 1.0 + } + if len(r1) == 0 || len(r2) == 0 { + return 0.0 + } + + dist := levenshteinDistance(clean1, clean2) + maxLen := math.Max(float64(len(r1)), float64(len(r2))) + return 1.0 - float64(dist)/maxLen +} + +// cleanWithRegex 使用自定义正则清洗字符串;reg 为 nil 时返回原文 +func cleanWithRegex(input string, reg *regexp.Regexp) string { + if reg == nil { + return input + } + return reg.ReplaceAllString(input, "") +} + +// ---------- 文件读取(通用)---------- + +// readRawRows 读取 Excel/CSV 文件,返回原始二维字符串切片(row[0] = 表头) +func (a *App) readRawRows(path string) ([][]string, error) { + ext := strings.ToLower(filepath.Ext(path)) + switch ext { + case ".csv": + return a.readCSVRaw(path) + default: + return a.readExcelRaw(path) + } +} + +func (a *App) readCSVRaw(path string) ([][]string, error) { + content, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("读取 CSV 文件失败: %v", err) + } + + lines := strings.Split(string(content), "\n") + if len(lines) < 2 { + return nil, fmt.Errorf("CSV 文件至少需要标题行和一条数据") + } + + var rows [][]string + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + fields := parseCSVLine(line) + rows = append(rows, fields) + } + if len(rows) < 2 { + return nil, fmt.Errorf("CSV 文件无有效数据行") + } + return rows, nil +} + +func (a *App) readExcelRaw(path string) ([][]string, error) { + f, err := excelize.OpenFile(path) + if err != nil { + return nil, fmt.Errorf("打开 Excel 文件失败: %v", err) + } + defer f.Close() + + sheetName := f.GetSheetName(0) + allRows, err := f.GetRows(sheetName) + if err != nil { + return nil, fmt.Errorf("读取工作表失败: %v", err) + } + if len(allRows) < 2 { + return nil, fmt.Errorf("Excel 文件至少需要标题行和一条数据") + } + return allRows, nil +} + +// getCell 安全获取行中指定索引的单元格值,越界返回空字符串 +func getCell(row []string, idx int) string { + if idx < 0 || idx >= len(row) { + return "" + } + return strings.TrimSpace(row[idx]) +} + +// parseCSVLine 简单 CSV 行解析(支持双引号包裹的字段) +func parseCSVLine(line string) []string { + var fields []string + var current strings.Builder + inQuotes := false + + for _, ch := range line { + switch { + case ch == '"': + inQuotes = !inQuotes + case ch == ',' && !inQuotes: + fields = append(fields, strings.TrimSpace(current.String())) + current.Reset() + default: + current.WriteRune(ch) + } + } + fields = append(fields, strings.TrimSpace(current.String())) + return fields +} + +// ---------- 动态表头索引映射 ---------- + +// findColumnIndexExact 在表头行中查找与候选词【精确相等】的第一列索引,未找到返回 -1 +// 使用 strings.EqualFold 做大小写不敏感的精确匹配,两端 TrimSpace +func findColumnIndexExact(headers []string, candidates ...string) int { + for i, h := range headers { + trimmed := strings.TrimSpace(h) + for _, c := range candidates { + if strings.EqualFold(trimmed, strings.TrimSpace(c)) { + return i + } + } + } + return -1 +} + +// ---------- 读取月报 ---------- + +func (a *App) readMonthlyReport(path string) ([]MonthlyRecord, error) { + allRows, err := a.readRawRows(path) + if err != nil { + return nil, err + } + + // 第 1 行是表头 + headers := allRows[0] + fmt.Printf("[DEBUG] 月报全部表头 (共%d列): %v\n", len(headers), headers) + + // 动态找关键列的索引 + cellNameIdx := findColumnIndexExact(headers, "小区名称", "小区名", "cellname", "cell name", "小区") + timeIdx := findColumnIndexExact(headers, "告警发生时间", "发生时间", "告警时间", "时间", "occurtime", "occur time") + + // 回退策略:找不到时用前两列 + if cellNameIdx == -1 && len(headers) >= 1 { + cellNameIdx = 0 + } + if timeIdx == -1 && len(headers) >= 2 { + timeIdx = 1 + } + + fmt.Printf("[DEBUG] 月报列索引: 小区名称 idx=%d, 时间 idx=%d\n", cellNameIdx, timeIdx) + if cellNameIdx == -1 || timeIdx == -1 { + return nil, fmt.Errorf("无法识别月报表头,需要包含「小区名称」和「告警发生时间」列") + } + + var records []MonthlyRecord + for rowNum, row := range allRows[1:] { + name := getCell(row, cellNameIdx) + timeStr := getCell(row, timeIdx) + if name == "" || timeStr == "" { + continue + } + // 月报专用 Layout: 2006-01-02 15:04:05 + t, err := time.Parse("2006-01-02 15:04:05", timeStr) + if err != nil { + fmt.Printf("[DEBUG] 月报时间解析失败 L%d | 原始: '%s' | 错误: %v\n", rowNum+2, timeStr, err) + continue + } + records = append(records, MonthlyRecord{ + CellName: name, + OccurTime: t, + OccurTimeStr: t.Format("2006-01-02 15:04:05"), + }) + } + fmt.Printf("[DEBUG] 月报有效记录数: %d (共 %d 行数据)\n", len(records), len(allRows)-1) + return records, nil +} + +// ---------- 读取日报 ---------- + +func (a *App) readDailyReport(path string) ([]DailyRecord, error) { + allRows, err := a.readRawRows(path) + if err != nil { + return nil, err + } + + // 第 1 行是表头 + headers := allRows[0] + fmt.Printf("[DEBUG] 日报全部表头 (共%d列): %v\n", len(headers), headers) + + // 动态找关键列的索引 + cellIDIdx := findColumnIndexExact(headers, "小区号", "小区编号", "小区id", "cellid", "cell id", "小区") + timeIdx := findColumnIndexExact(headers, "发生时间", "时间", "occurtime", "occur time") + reasonIdx := findColumnIndexExact(headers, "中断原因", "原因", "reason", "中断", "中断原因", "failure reason") + + // 回退策略 + if cellIDIdx == -1 && len(headers) >= 1 { + cellIDIdx = 0 + } + if timeIdx == -1 && len(headers) >= 2 { + timeIdx = 1 + } + if reasonIdx == -1 && len(headers) >= 3 { + reasonIdx = 2 + } + + fmt.Printf("[DEBUG] 日报列索引: 小区号 idx=%d, 时间 idx=%d, 中断原因 idx=%d\n", cellIDIdx, timeIdx, reasonIdx) + if cellIDIdx == -1 || timeIdx == -1 || reasonIdx == -1 { + return nil, fmt.Errorf("无法识别日报表头,需要包含「小区号」「发生时间」「中断原因」列") + } + + var records []DailyRecord + for rowNum, row := range allRows[1:] { + cellID := getCell(row, cellIDIdx) + timeStr := getCell(row, timeIdx) + reason := getCell(row, reasonIdx) + if cellID == "" || timeStr == "" { + continue + } + // 日报专用 Layout: 2006/1/2 15:04(支持单双位数的月/日) + t, err := time.Parse("2006/1/2 15:04", timeStr) + if err != nil { + fmt.Printf("[DEBUG] 日报时间解析失败 L%d | 原始: '%s' | 错误: %v\n", rowNum+2, timeStr, err) + continue + } + records = append(records, DailyRecord{ + CellID: cellID, + OccurTime: t, + OccurTimeStr: t.Format("2006-01-02 15:04:05"), + InterruptReason: reason, + }) + } + fmt.Printf("[DEBUG] 日报有效记录数: %d (共 %d 行数据)\n", len(records), len(allRows)-1) + return records, nil +} + +// ---------- 核心匹配引擎 ---------- + +// StartMatching 执行多维智能匹配(带进度推送) +func (a *App) StartMatching(monthlyPath, dailyPath string) ([]MatchResult, error) { + a.emitProgress(0, 100, "正在读取月报文件...", "reading") + monthlyRecords, err := a.readMonthlyReport(monthlyPath) + if err != nil { + return nil, fmt.Errorf("读取月报失败: %v", err) + } + + a.emitProgress(0, 100, "正在读取日报文件...", "reading") + dailyRecords, err := a.readDailyReport(dailyPath) + if err != nil { + return nil, fmt.Errorf("读取日报失败: %v", err) + } + + if len(monthlyRecords) == 0 { + return nil, fmt.Errorf("月报中无有效记录(或时间解析失败)") + } + if len(dailyRecords) == 0 { + return nil, fmt.Errorf("日报中无有效记录(或时间解析失败)") + } + + twelveHours := 12 * time.Hour + totalMonthly := len(monthlyRecords) + var results []MatchResult + + for i, mr := range monthlyRecords { + if i%10 == 0 || i == totalMonthly-1 { + pct := (i + 1) * 100 / totalMonthly + a.emitProgress(i+1, totalMonthly, + fmt.Sprintf("正在匹配第 %d/%d 条月报记录 (%d%%)...", i+1, totalMonthly, pct), + "matching") + } + + var bestMatch *DailyRecord + bestSimilarity := 0.0 + bestTimeDiff := time.Duration(0) + cleanMonthly := a.CleanString(mr.CellName) + + for _, dr := range dailyRecords { + // 步骤一:时间剪枝 — 保留时间差在 ±12h 内的记录 + timeDiff := mr.OccurTime.Sub(dr.OccurTime) + if timeDiff < -twelveHours || timeDiff > twelveHours { + continue + } + + // 步骤二:计算清洗后中文名称的相似度 + cleanDaily := a.CleanString(dr.CellID) + if cleanMonthly == "" || cleanDaily == "" { + continue + } + + similarity := a.CalculateSimilarity(mr.CellName, dr.CellID) + + if i < 5 { + fmt.Printf("[DEBUG] 比对 | 月报='%s'→清洗='%s' | 日报='%s'→清洗='%s' | 时间差=%v | 相似度=%.4f\n", + mr.CellName, cleanMonthly, dr.CellID, cleanDaily, timeDiff, similarity) + } + + // 阈值 0.65:只保留高置信度匹配 + if similarity >= 0.65 && similarity > bestSimilarity { + bestSimilarity = similarity + bestMatch = &dr + bestTimeDiff = timeDiff + } + } + + if bestMatch != nil { + if i < 5 { + fmt.Printf("[DEBUG] ✓ 命中 | 月报='%s'→日报='%s' | 相似度=%.4f | 时间差=%v\n", + mr.CellName, bestMatch.CellID, bestSimilarity, bestTimeDiff) + } + results = append(results, MatchResult{ + MonthlyCellName: mr.CellName, + DailyCellID: bestMatch.CellID, + TimeDiff: formatTimeDiff(bestTimeDiff), + SimilarityScore: math.Round(bestSimilarity*10000) / 10000, + InterruptReason: bestMatch.InterruptReason, + AIMatched: false, + }) + } else { + if i < 5 { + fmt.Printf("[DEBUG] ✗ 未命中 | 月报='%s'(清洗='%s') | 未找到匹配\n", + mr.CellName, cleanMonthly) + } + } + } + + a.emitProgress(totalMonthly, totalMonthly, + fmt.Sprintf("匹配完成!共匹配成功 %d 条记录", len(results)), "done") + + return results, nil +} + +// ---------- 通用匹配引擎 ---------- + +// RunMatch 接收完整 MatchConfig,按列索引执行通用匹配 +func (a *App) RunMatch(config MatchConfig) ([]MatchResult, error) { + // 1. 编译正则 + var reg *regexp.Regexp + if config.RegexPattern != "" { + var err error + reg, err = regexp.Compile(config.RegexPattern) + if err != nil { + return nil, fmt.Errorf("正则表达式格式错误,请检查: %v", err) + } + fmt.Printf("[DEBUG] RunMatch 使用正则: '%s'\n", config.RegexPattern) + } else { + fmt.Printf("[DEBUG] RunMatch 跳过清洗(正则为空)\n") + } + + // 2. 默认值兜底 + timeWindow := config.TimeWindow + if timeWindow <= 0 { + timeWindow = 12 + } + threshold := config.Threshold + if threshold <= 0 { + threshold = 0.65 + } + useTime := config.ColATimeIndex >= 0 && config.ColBTimeIndex >= 0 + + // 3. 读取原始数据 + a.emitProgress(0, 100, "正在读取 A 表...", "reading") + rowsA, err := a.readRawRows(config.FileAPath) + if err != nil { + return nil, fmt.Errorf("读取 A 表失败: %v", err) + } + a.emitProgress(0, 100, "正在读取 B 表...", "reading") + rowsB, err := a.readRawRows(config.FileBPath) + if err != nil { + return nil, fmt.Errorf("读取 B 表失败: %v", err) + } + if len(rowsA) < 2 { + return nil, fmt.Errorf("A 表无有效数据行") + } + if len(rowsB) < 2 { + return nil, fmt.Errorf("B 表无有效数据行") + } + + aHeaders := rowsA[0] + _ = aHeaders // 保留表头引用(将来导出时可能用到) + dataA := rowsA[1:] + dataB := rowsB[1:] + windowDuration := time.Duration(timeWindow * float64(time.Hour)) + totalA := len(dataA) + var results []MatchResult + + useAllMatches := config.AllMatches + maxPreview := config.MaxPreview + if maxPreview <= 0 { + maxPreview = 3 + } + + for i, rowA := range dataA { + if i%10 == 0 || i == totalA-1 { + pct := (i + 1) * 100 / totalA + a.emitProgress(i+1, totalA, + fmt.Sprintf("匹配中 %d/%d (%d%%)...", i+1, totalA, pct), "matching") + } + + matchStrA := getCell(rowA, config.ColAMatchIndex) + if matchStrA == "" { + continue + } + + var timeA time.Time + var hasTimeA bool + if useTime { + t, err := parseTimeFlexible(getCell(rowA, config.ColATimeIndex)) + if err == nil { timeA = t; hasTimeA = true } + } + + cleanA := cleanWithRegex(matchStrA, reg) + // 收集该 A 行的所有候选匹配 + var candidates []MatchResult + + for _, rowB := range dataB { + matchStrB := getCell(rowB, config.ColBMatchIndex) + if matchStrB == "" { continue } + + var timeDiff time.Duration + if hasTimeA && useTime { + tB, err := parseTimeFlexible(getCell(rowB, config.ColBTimeIndex)) + if err != nil { continue } + td := timeA.Sub(tB) + if td < -windowDuration || td > windowDuration { continue } + timeDiff = td + } + + cleanB := cleanWithRegex(matchStrB, reg) + if cleanA == "" || cleanB == "" { continue } + + similarity := calcSimilarity(matchStrA, matchStrB, reg) + + if i < maxPreview { + fmt.Printf("[DEBUG] | A[%d]='%s'→'%s' | B='%s'→'%s' | 相似度=%.4f\n", + i, matchStrA, cleanA, matchStrB, cleanB, similarity) + } + + if similarity >= threshold { + mr := MatchResult{ + RowAData: rowA, + RowBKey: matchStrB, + ExtractValue: getCell(rowB, config.ColBExtractIndex), + TimeDiff: formatTimeDiff(timeDiff), + SimilarityScore: math.Round(similarity*10000) / 10000, + AIMatched: false, + } + if useAllMatches { + candidates = append(candidates, mr) + } else if len(candidates) == 0 || similarity > candidates[0].SimilarityScore { + candidates = []MatchResult{mr} + } + } + } + + if len(candidates) > 0 { + if i < maxPreview { + for _, c := range candidates { + fmt.Printf("[DEBUG] ✓ 命中 | A='%s'→B='%s' | 相似度=%.4f\n", + matchStrA, c.RowBKey, c.SimilarityScore) + } + } + results = append(results, candidates...) + } + } + + // 结果排序 + if config.SortBy == "similarity" { + sort.Slice(results, func(i, j int) bool { + return results[i].SimilarityScore > results[j].SimilarityScore + }) + } else if config.SortBy == "timeDiff" { + sort.Slice(results, func(i, j int) bool { + return results[i].TimeDiff < results[j].TimeDiff + }) + } + + a.emitProgress(totalA, totalA, + fmt.Sprintf("匹配完成!共匹配成功 %d 条记录", len(results)), "done") + + return results, nil +} + +type rowAndScore struct { + row []string + score float64 +} + +// ---------- Deepseek AI 增强匹配 ---------- + +// callDeepseekAPI 调用 Deepseek Chat API +func (a *App) callDeepseekAPI(messages []deepseekMessage) (string, error) { + if a.deepseekKey == "" { + return "", fmt.Errorf("请先设置 Deepseek API 密钥") + } + + reqBody := deepseekRequest{ + Model: "deepseek-chat", + Messages: messages, + Temperature: 0.05, + MaxTokens: 2048, + } + + bodyBytes, _ := json.Marshal(reqBody) + httpReq, err := http.NewRequest("POST", "https://api.deepseek.com/v1/chat/completions", + bytes.NewReader(bodyBytes)) + if err != nil { + return "", fmt.Errorf("创建请求失败: %v", err) + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Authorization", "Bearer "+a.deepseekKey) + + client := &http.Client{Timeout: 60 * time.Second} + resp, err := client.Do(httpReq) + if err != nil { + return "", fmt.Errorf("调用 Deepseek API 失败: %v", err) + } + defer resp.Body.Close() + + respBytes, _ := io.ReadAll(resp.Body) + var dr deepseekResponse + if err := json.Unmarshal(respBytes, &dr); err != nil { + return "", fmt.Errorf("解析 Deepseek 响应失败: %v", err) + } + + if dr.Error != nil { + return "", fmt.Errorf("Deepseek API 错误: %s", dr.Error.Message) + } + + if len(dr.Choices) == 0 { + return "", fmt.Errorf("Deepseek 未返回有效结果") + } + + return strings.TrimSpace(dr.Choices[0].Message.Content), nil +} + +// buildAIPrompt 构建 AI 匹配提示词 +func buildAIPrompt(monthlyRecords []MonthlyRecord, dailyRecords []DailyRecord, batchStart, batchSize int) []deepseekMessage { + end := batchStart + batchSize + if end > len(monthlyRecords) { + end = len(monthlyRecords) + } + batch := monthlyRecords[batchStart:end] + + var sb strings.Builder + sb.WriteString(`你是一个通信网络数据匹配专家。请根据以下月报记录,从日报数据中找出最匹配的「中断原因」。 + +匹配规则: +1. 首先根据「发生时间」匹配,时间差应在 ±2 小时内 +2. 然后根据「小区名称」匹配:小区名称可能包含字母数字前缀后缀(如 LTESF1_32、2100_2 等),请着重对比其中的中文字段 +3. 如果找到匹配项,返回中断原因;如果找不到,返回空字符串 + +请严格按照以下 JSON 格式返回结果: +{"matches":[{"index":0,"reason":"中断原因或空字符串"},...]} + +`) + + // 构建最近的日报索引:按时间排序后的日报 + type dailyIdx struct { + idx int + rec DailyRecord + diff time.Duration + } + + sb.WriteString("以下是需要匹配的月报记录列表(每条包含索引、小区名称、时间):\n") + for offset, mr := range batch { + sb.WriteString(fmt.Sprintf("- 索引 %d: 小区=", batchStart+offset)) + sb.WriteString(mr.CellName) + sb.WriteString(", 时间=") + sb.WriteString(mr.OccurTimeStr) + sb.WriteString("\n") + } + + // 为每条月报记录寻找最近的日报候选 + sb.WriteString("\n以下是日报记录列表(供匹配参考):\n") + // 构建一个日报时间索引,只输出时间接近的记录 + for i, dr := range dailyRecords { + sb.WriteString(fmt.Sprintf(" D%d: 小区号=%s, 时间=%s, 中断原因=%s\n", + i, dr.CellID, dr.OccurTimeStr, dr.InterruptReason)) + } + + sb.WriteString("\n请返回 JSON 格式的匹配结果,包含每个索引对应的中断原因。如果某条记录无法匹配,对应的 reason 设为空字符串。") + + return []deepseekMessage{ + {Role: "system", Content: "你是一个数据匹配专家。请严格按照 JSON 格式返回结果,不要添加额外说明。"}, + {Role: "user", Content: sb.String()}, + } +} + +// parseAIResponse 解析 Deepseek 返回的 JSON 结果 +type aiMatchItem struct { + Index int `json:"index"` + Reason string `json:"reason"` +} + +type aiMatchResponse struct { + Matches []aiMatchItem `json:"matches"` +} + +// DeepseekEnhanceMatching 使用 Deepseek AI 进行增强匹配 +func (a *App) DeepseekEnhanceMatching(monthlyPath, dailyPath string) ([]MatchResult, error) { + if a.deepseekKey == "" { + return nil, fmt.Errorf("请先设置 Deepseek API 密钥(点击「配置 API」按钮)") + } + + // 先运行基础匹配获取结果 + a.emitProgress(0, 100, "正在读取数据文件...", "reading") + monthlyRecords, err := a.readMonthlyReport(monthlyPath) + if err != nil { + return nil, fmt.Errorf("读取月报失败: %v", err) + } + dailyRecords, err := a.readDailyReport(dailyPath) + if err != nil { + return nil, fmt.Errorf("读取日报失败: %v", err) + } + if len(monthlyRecords) == 0 || len(dailyRecords) == 0 { + return nil, fmt.Errorf("数据文件中无有效记录") + } + + twelveHours := 12 * time.Hour + totalMonthly := len(monthlyRecords) + + // 使用基础算法先跑一遍,收集结果和未匹配的记录 + a.emitProgress(0, totalMonthly, "正在运行基础匹配...", "matching") + var results []MatchResult + var unmatchedMonthly []MonthlyRecord + + for i, mr := range monthlyRecords { + if i%20 == 0 { + a.emitProgress(i+1, totalMonthly, + fmt.Sprintf("基础匹配中 %d/%d...", i+1, totalMonthly), "matching") + } + + var bestMatch *DailyRecord + bestSimilarity := 0.0 + bestTimeDiff := time.Duration(0) + cleanMonthly := a.CleanString(mr.CellName) + + for _, dr := range dailyRecords { + // 步骤一:时间剪枝 — 保留时间差在 ±12h 内的记录 + timeDiff := mr.OccurTime.Sub(dr.OccurTime) + if timeDiff < -twelveHours || timeDiff > twelveHours { + continue + } + + cleanDaily := a.CleanString(dr.CellID) + if cleanMonthly == "" || cleanDaily == "" { + continue + } + + similarity := a.CalculateSimilarity(mr.CellName, dr.CellID) + if similarity >= 0.65 && similarity > bestSimilarity { + bestSimilarity = similarity + bestMatch = &dr + bestTimeDiff = timeDiff + } + } + + if bestMatch != nil { + if i < 5 { + fmt.Printf("[DEBUG-AI] ✓ 基础命中 | 月报='%s'→日报='%s' | 相似度=%.4f | 时间差=%v\n", + mr.CellName, bestMatch.CellID, bestSimilarity, bestTimeDiff) + } + results = append(results, MatchResult{ + MonthlyCellName: mr.CellName, + DailyCellID: bestMatch.CellID, + TimeDiff: formatTimeDiff(bestTimeDiff), + SimilarityScore: math.Round(bestSimilarity*10000) / 10000, + InterruptReason: bestMatch.InterruptReason, + AIMatched: false, + }) + } else { + unmatchedMonthly = append(unmatchedMonthly, mr) + if i < 5 { + fmt.Printf("[DEBUG-AI] ✗ 基础未命中 | 月报='%s'(清洗='%s')\n", + mr.CellName, cleanMonthly) + } + } + } + + // 如果没有未匹配的记录,直接返回 + if len(unmatchedMonthly) == 0 { + a.emitProgress(totalMonthly, totalMonthly, "全部已匹配,无需 AI 增强", "done") + return results, nil + } + + a.emitProgress(0, len(unmatchedMonthly), + fmt.Sprintf("AI 增强匹配:还有 %d 条未匹配记录,正在调用 Deepseek...", len(unmatchedMonthly)), + "ai-enhancing") + + // 分批调用 Deepseek API(每批 8 条) + batchSize := 8 + totalUnmatched := len(unmatchedMonthly) + aiMatched := 0 + + for batchStart := 0; batchStart < totalUnmatched; batchStart += batchSize { + end := batchStart + batchSize + if end > totalUnmatched { + end = totalUnmatched + } + + a.emitProgress(batchStart+1, totalUnmatched, + fmt.Sprintf("AI 分析中 %d/%d (第 %d 批)...", end, totalUnmatched, (batchStart/batchSize)+1), + "ai-enhancing") + + prompt := buildAIPrompt(unmatchedMonthly, dailyRecords, batchStart, batchSize) + aiResp, err := a.callDeepseekAPI(prompt) + if err != nil { + // AI 调用失败,跳过这批 + continue + } + + // 解析 AI 返回的 JSON + var matchResp aiMatchResponse + if err := json.Unmarshal([]byte(aiResp), &matchResp); err != nil { + // 尝试从 ```json 块中提取 + if idx := strings.Index(aiResp, "{"); idx >= 0 { + if endIdx := strings.LastIndex(aiResp, "}"); endIdx > idx { + json.Unmarshal([]byte(aiResp[idx:endIdx+1]), &matchResp) + } + } + } + + for _, item := range matchResp.Matches { + idx := item.Index + reason := strings.TrimSpace(item.Reason) + if idx < 0 || idx >= len(unmatchedMonthly) || reason == "" { + continue + } + + mr := unmatchedMonthly[idx] + // 找到匹配的日报记录(暂不校验时间窗口) + for _, dr := range dailyRecords { + if dr.InterruptReason == reason { + timeDiff := mr.OccurTime.Sub(dr.OccurTime) + similarity := a.CalculateSimilarity(mr.CellName, dr.CellID) + + results = append(results, MatchResult{ + MonthlyCellName: mr.CellName, + DailyCellID: dr.CellID, + TimeDiff: formatTimeDiff(timeDiff), + SimilarityScore: math.Round(similarity*10000) / 10000, + InterruptReason: reason, + AIMatched: true, + }) + aiMatched++ + break + } + } + } + } + + a.emitProgress(totalUnmatched, totalUnmatched, + fmt.Sprintf("AI 增强完成!基础匹配 %d 条 + AI 补充 %d 条 = 共 %d 条", + len(results)-aiMatched, aiMatched, len(results)), "done") + + return results, nil +} + +// formatTimeDiff 格式化时间差为可读字符串 +func formatTimeDiff(d time.Duration) string { + abs := d + if abs < 0 { + abs = -abs + } + hours := int(abs.Hours()) + mins := int(abs.Minutes()) % 60 + secs := int(abs.Seconds()) % 60 + + sign := "" + if d < 0 { + sign = "-" + } + if hours > 0 { + return fmt.Sprintf("%s%dh%dm%ds", sign, hours, mins, secs) + } else if mins > 0 { + return fmt.Sprintf("%s%dm%ds", sign, mins, secs) + } + return fmt.Sprintf("%s%ds", sign, secs) +} + +// ---------- 导出结果 ---------- + +// ExportResults 将匹配结果导出为 Excel 文件 +func (a *App) ExportResults(results []MatchResult) (string, error) { + if len(results) == 0 { + return "", fmt.Errorf("没有匹配结果可以导出") + } + + savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{ + Title: "导出匹配结果", + DefaultFilename: fmt.Sprintf("匹配结果_%s.xlsx", time.Now().Format("20060102_150405")), + Filters: []runtime.FileFilter{ + {DisplayName: "Excel 文件 (*.xlsx)", Pattern: "*.xlsx"}, + }, + }) + if err != nil { + return "", fmt.Errorf("打开保存对话框失败: %v", err) + } + if savePath == "" { + return "", nil + } + if !strings.HasSuffix(strings.ToLower(savePath), ".xlsx") { + savePath += ".xlsx" + } + + f := excelize.NewFile() + defer f.Close() + sheetName := "匹配结果" + f.SetSheetName("Sheet1", sheetName) + + // 判断使用新格式还是旧格式 + if len(results) > 0 && len(results[0].RowAData) > 0 { + // 新格式:A 表所有原始列 + 最后追加「匹配结果(由B表提取)」 + numACols := len(results[0].RowAData) + colLetter := func(n int) string { c, _ := excelize.ColumnNumberToName(n + 1); return c } + colNums := make([]int, numACols+1) + for i := 0; i < numACols; i++ { + colNums[i] = i + f.SetCellValue(sheetName, fmt.Sprintf("%s1", colLetter(i)), fmt.Sprintf("A-Col%d", i+1)) + } + extractCol := numACols + colNums[numACols] = extractCol + f.SetCellValue(sheetName, fmt.Sprintf("%s1", colLetter(extractCol)), "匹配结果(由B表提取)") + + headerStyle, _ := f.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Size: 12, Color: "FFFFFF"}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}}, + }) + f.SetCellStyle(sheetName, "A1", fmt.Sprintf("%s1", colLetter(extractCol)), headerStyle) + + for i, r := range results { + rowNum := i + 2 + for _, ci := range colNums { + if ci < numACols { + f.SetCellValue(sheetName, fmt.Sprintf("%s%d", colLetter(ci), rowNum), r.RowAData[ci]) + } else { + f.SetCellValue(sheetName, fmt.Sprintf("%s%d", colLetter(ci), rowNum), r.ExtractValue) + } + } + } + for ci := range colNums { + f.SetColWidth(sheetName, colLetter(ci), colLetter(ci), 22) + } + } else { + // 旧格式 向后兼容 + headers := []string{"月报小区名称", "日报小区号", "匹配时间差", "相似度得分", "统计到的中断原因", "AI辅助匹配"} + for i, h := range headers { + col, _ := excelize.ColumnNumberToName(i + 1) + f.SetCellValue(sheetName, fmt.Sprintf("%s1", col), h) + } + headerStyle, _ := f.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Size: 12, Color: "FFFFFF"}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}}, + }) + lastCol, _ := excelize.ColumnNumberToName(len(headers)) + f.SetCellStyle(sheetName, "A1", fmt.Sprintf("%s1", lastCol), headerStyle) + + for i, r := range results { + rowNum := i + 2 + f.SetCellValue(sheetName, fmt.Sprintf("A%d", rowNum), r.MonthlyCellName) + f.SetCellValue(sheetName, fmt.Sprintf("B%d", rowNum), r.DailyCellID) + f.SetCellValue(sheetName, fmt.Sprintf("C%d", rowNum), r.TimeDiff) + f.SetCellValue(sheetName, fmt.Sprintf("D%d", rowNum), r.SimilarityScore) + f.SetCellValue(sheetName, fmt.Sprintf("E%d", rowNum), r.InterruptReason) + aiLabel := "否" + if r.AIMatched { + aiLabel = "是" + } + f.SetCellValue(sheetName, fmt.Sprintf("F%d", rowNum), aiLabel) + } + for _, c := range []string{"A", "B", "C", "D", "E", "F"} { + f.SetColWidth(sheetName, c, c, 22) + } + } + + if err := f.SaveAs(savePath); err != nil { + return "", fmt.Errorf("保存文件失败: %v", err) + } + return savePath, nil +} diff --git a/build/README.md b/build/README.md new file mode 100644 index 0000000..1ae2f67 --- /dev/null +++ b/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. \ No newline at end of file diff --git a/build/appicon.png b/build/appicon.png new file mode 100644 index 0000000..63617fe Binary files /dev/null and b/build/appicon.png differ diff --git a/build/darwin/Info.dev.plist b/build/darwin/Info.dev.plist new file mode 100644 index 0000000..14121ef --- /dev/null +++ b/build/darwin/Info.dev.plist @@ -0,0 +1,68 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist new file mode 100644 index 0000000..d17a747 --- /dev/null +++ b/build/darwin/Info.plist @@ -0,0 +1,63 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + + diff --git a/build/windows/icon.ico b/build/windows/icon.ico new file mode 100644 index 0000000..f334798 Binary files /dev/null and b/build/windows/icon.ico differ diff --git a/build/windows/info.json b/build/windows/info.json new file mode 100644 index 0000000..9727946 --- /dev/null +++ b/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}}" + } + } +} \ No newline at end of file diff --git a/build/windows/installer/project.nsi b/build/windows/installer/project.nsi new file mode 100644 index 0000000..654ae2e --- /dev/null +++ b/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 diff --git a/build/windows/installer/wails_tools.nsh b/build/windows/installer/wails_tools.nsh new file mode 100644 index 0000000..2f6d321 --- /dev/null +++ b/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 diff --git a/build/windows/wails.exe.manifest b/build/windows/wails.exe.manifest new file mode 100644 index 0000000..17e1a23 --- /dev/null +++ b/build/windows/wails.exe.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..b4719be --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,8 @@ +# Vue 3 + Vite + +This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` + + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..43da9cb --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,18 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.2.37" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^3.0.3", + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 new file mode 100644 index 0000000..ed0efaf --- /dev/null +++ b/frontend/package.json.md5 @@ -0,0 +1 @@ +21d2a2199c4fb87865d8160b492f51c3 \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..55f90e3 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,898 @@ + + + + + diff --git a/frontend/src/assets/fonts/OFL.txt b/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 0000000..9cac04c --- /dev/null +++ b/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 0000000..2f9cc59 Binary files /dev/null and b/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/frontend/src/assets/images/logo-universal.png b/frontend/src/assets/images/logo-universal.png new file mode 100644 index 0000000..e9913c1 Binary files /dev/null and b/frontend/src/assets/images/logo-universal.png differ diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..f9754fe --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,5 @@ +import {createApp} from 'vue' +import App from './App.vue' +import './style.css'; + +createApp(App).mount('#app') diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..3c46aad --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,33 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); + background-attachment: fixed; + color: #e2e8f0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, + 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + margin: 0; + padding: 0; + line-height: 1.6; +} + +#app { + min-height: 100vh; + background: radial-gradient(ellipse at 20% 50%, rgba(118, 75, 162, 0.15) 0%, transparent 50%), + radial-gradient(ellipse at 80% 20%, rgba(102, 126, 234, 0.1) 0%, transparent 50%); +} + +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: rgba(255,255,255,0.03); border-radius: 3px; } +::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); } +::selection { background: rgba(118, 75, 162, 0.4); color: #fff; } diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..0db4339 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,11 @@ +import {defineConfig} from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + server: { + port: 34115, + strictPort: true, + }, +}) diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 0000000..f9c64e2 --- /dev/null +++ b/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,29 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT +import {main} from '../models'; + +export function CalculateSimilarity(arg1:string,arg2:string):Promise; + +export function CleanString(arg1:string):Promise; + +export function DeepseekEnhanceMatching(arg1:string,arg2:string):Promise>; + +export function ExportResults(arg1:Array):Promise; + +export function GetDeepseekStatus():Promise; + +export function OpenDailyReport():Promise; + +export function OpenFileA():Promise; + +export function OpenFileB():Promise; + +export function OpenMonthlyReport():Promise; + +export function ParseHeaders(arg1:string):Promise>; + +export function RunMatch(arg1:main.MatchConfig):Promise>; + +export function SetDeepseekAPIKey(arg1:string):Promise; + +export function StartMatching(arg1:string,arg2:string):Promise>; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js new file mode 100644 index 0000000..8853e85 --- /dev/null +++ b/frontend/wailsjs/go/main/App.js @@ -0,0 +1,55 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function CalculateSimilarity(arg1, arg2) { + return window['go']['main']['App']['CalculateSimilarity'](arg1, arg2); +} + +export function CleanString(arg1) { + return window['go']['main']['App']['CleanString'](arg1); +} + +export function DeepseekEnhanceMatching(arg1, arg2) { + return window['go']['main']['App']['DeepseekEnhanceMatching'](arg1, arg2); +} + +export function ExportResults(arg1) { + return window['go']['main']['App']['ExportResults'](arg1); +} + +export function GetDeepseekStatus() { + return window['go']['main']['App']['GetDeepseekStatus'](); +} + +export function OpenDailyReport() { + return window['go']['main']['App']['OpenDailyReport'](); +} + +export function OpenFileA() { + return window['go']['main']['App']['OpenFileA'](); +} + +export function OpenFileB() { + return window['go']['main']['App']['OpenFileB'](); +} + +export function OpenMonthlyReport() { + return window['go']['main']['App']['OpenMonthlyReport'](); +} + +export function ParseHeaders(arg1) { + return window['go']['main']['App']['ParseHeaders'](arg1); +} + +export function RunMatch(arg1) { + return window['go']['main']['App']['RunMatch'](arg1); +} + +export function SetDeepseekAPIKey(arg1) { + return window['go']['main']['App']['SetDeepseekAPIKey'](arg1); +} + +export function StartMatching(arg1, arg2) { + return window['go']['main']['App']['StartMatching'](arg1, arg2); +} diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts new file mode 100644 index 0000000..f2504b0 --- /dev/null +++ b/frontend/wailsjs/go/models.ts @@ -0,0 +1,75 @@ +export namespace main { + + export class MatchConfig { + fileAPath: string; + fileBPath: string; + colAMatchIndex: number; + colATimeIndex: number; + colBMatchIndex: number; + colBTimeIndex: number; + colBExtractIndex: number; + regexPattern: string; + timeWindow: number; + threshold: number; + allMatches: boolean; + caseSensitive: boolean; + sortBy: string; + maxPreview: number; + exportFormat: string; + includeHeader: boolean; + + static createFrom(source: any = {}) { + return new MatchConfig(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.fileAPath = source["fileAPath"]; + this.fileBPath = source["fileBPath"]; + this.colAMatchIndex = source["colAMatchIndex"]; + this.colATimeIndex = source["colATimeIndex"]; + this.colBMatchIndex = source["colBMatchIndex"]; + this.colBTimeIndex = source["colBTimeIndex"]; + this.colBExtractIndex = source["colBExtractIndex"]; + this.regexPattern = source["regexPattern"]; + this.timeWindow = source["timeWindow"]; + this.threshold = source["threshold"]; + this.allMatches = source["allMatches"]; + this.caseSensitive = source["caseSensitive"]; + this.sortBy = source["sortBy"]; + this.maxPreview = source["maxPreview"]; + this.exportFormat = source["exportFormat"]; + this.includeHeader = source["includeHeader"]; + } + } + export class MatchResult { + rowAData: string[]; + rowBKey: string; + extractValue: string; + monthlyCellName: string; + dailyCellId: string; + interruptReason: string; + timeDiff: string; + similarityScore: number; + aiMatched: boolean; + + static createFrom(source: any = {}) { + return new MatchResult(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.rowAData = source["rowAData"]; + this.rowBKey = source["rowBKey"]; + this.extractValue = source["extractValue"]; + this.monthlyCellName = source["monthlyCellName"]; + this.dailyCellId = source["dailyCellId"]; + this.interruptReason = source["interruptReason"]; + this.timeDiff = source["timeDiff"]; + this.similarityScore = source["similarityScore"]; + this.aiMatched = source["aiMatched"]; + } + } + +} + diff --git a/frontend/wailsjs/runtime/package.json b/frontend/wailsjs/runtime/package.json new file mode 100644 index 0000000..1e7c8a5 --- /dev/null +++ b/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 ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/frontend/wailsjs/runtime/runtime.d.ts b/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 0000000..3bbea84 --- /dev/null +++ b/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,330 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +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; + +// [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; + +// [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; + +// [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; + +// [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; + +// [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; + +// [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; + +// [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; + +// [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; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [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 + +// Notification types +export interface NotificationOptions { + id: string; + title: string; + subtitle?: string; // macOS and Linux only + body?: string; + categoryId?: string; + data?: { [key: string]: any }; +} + +export interface NotificationAction { + id?: string; + title?: string; + destructive?: boolean; // macOS-specific +} + +export interface NotificationCategory { + id?: string; + actions?: NotificationAction[]; + hasReplyField?: boolean; + replyPlaceholder?: string; + replyButtonTitle?: string; +} + +// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications) +// Initializes the notification service for the application. +// This must be called before sending any notifications. +export function InitializeNotifications(): Promise; + +// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications) +// Cleans up notification resources and releases any held connections. +export function CleanupNotifications(): Promise; + +// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable) +// Checks if notifications are available on the current platform. +export function IsNotificationAvailable(): Promise; + +// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization) +// Requests notification authorization from the user (macOS only). +export function RequestNotificationAuthorization(): Promise; + +// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization) +// Checks the current notification authorization status (macOS only). +export function CheckNotificationAuthorization(): Promise; + +// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification) +// Sends a basic notification with the given options. +export function SendNotification(options: NotificationOptions): Promise; + +// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions) +// Sends a notification with action buttons. Requires a registered category. +export function SendNotificationWithActions(options: NotificationOptions): Promise; + +// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory) +// Registers a notification category that can be used with SendNotificationWithActions. +export function RegisterNotificationCategory(category: NotificationCategory): Promise; + +// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory) +// Removes a previously registered notification category. +export function RemoveNotificationCategory(categoryId: string): Promise; + +// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications) +// Removes all pending notifications from the notification center. +export function RemoveAllPendingNotifications(): Promise; + +// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification) +// Removes a specific pending notification by its identifier. +export function RemovePendingNotification(identifier: string): Promise; + +// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications) +// Removes all delivered notifications from the notification center. +export function RemoveAllDeliveredNotifications(): Promise; + +// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification) +// Removes a specific delivered notification by its identifier. +export function RemoveDeliveredNotification(identifier: string): Promise; + +// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification) +// Removes a notification by its identifier (cross-platform convenience function). +export function RemoveNotification(identifier: string): Promise; \ No newline at end of file diff --git a/frontend/wailsjs/runtime/runtime.js b/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 0000000..556621e --- /dev/null +++ b/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,298 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +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); +} + +export function InitializeNotifications() { + return window.runtime.InitializeNotifications(); +} + +export function CleanupNotifications() { + return window.runtime.CleanupNotifications(); +} + +export function IsNotificationAvailable() { + return window.runtime.IsNotificationAvailable(); +} + +export function RequestNotificationAuthorization() { + return window.runtime.RequestNotificationAuthorization(); +} + +export function CheckNotificationAuthorization() { + return window.runtime.CheckNotificationAuthorization(); +} + +export function SendNotification(options) { + return window.runtime.SendNotification(options); +} + +export function SendNotificationWithActions(options) { + return window.runtime.SendNotificationWithActions(options); +} + +export function RegisterNotificationCategory(category) { + return window.runtime.RegisterNotificationCategory(category); +} + +export function RemoveNotificationCategory(categoryId) { + return window.runtime.RemoveNotificationCategory(categoryId); +} + +export function RemoveAllPendingNotifications() { + return window.runtime.RemoveAllPendingNotifications(); +} + +export function RemovePendingNotification(identifier) { + return window.runtime.RemovePendingNotification(identifier); +} + +export function RemoveAllDeliveredNotifications() { + return window.runtime.RemoveAllDeliveredNotifications(); +} + +export function RemoveDeliveredNotification(identifier) { + return window.runtime.RemoveDeliveredNotification(identifier); +} + +export function RemoveNotification(identifier) { + return window.runtime.RemoveNotification(identifier); +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..54edae8 --- /dev/null +++ b/go.mod @@ -0,0 +1,46 @@ +module data-matcher + +go 1.24.0 + +require ( + github.com/wailsapp/wails/v2 v2.12.0 + github.com/xuri/excelize/v2 v2.10.1 +) + +require ( + git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect + github.com/bep/debounce v1.2.1 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.1.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-20210711035445-715c2860da7e // indirect + github.com/labstack/echo/v4 v4.13.3 // 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.13 // 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/richardlehane/mscfb v1.0.6 // indirect + github.com/richardlehane/msoleps v1.0.6 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/tiendc/go-deepcopy v1.7.2 // 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.22 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + github.com/xuri/efp v0.0.1 // indirect + github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect +) + +// replace github.com/wailsapp/wails/v2 v2.12.0 => C:\Users\chena\go\pkg\mod diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4bc181f --- /dev/null +++ b/go.sum @@ -0,0 +1,97 @@ +git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA= +git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= +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.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +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-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +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.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +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/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8= +github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= +github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg= +github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +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.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +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/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44= +github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= +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.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58= +github.com/wailsapp/go-webview2 v1.0.22/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.12.0 h1:BHO/kLNWFHYjCzucxbzAYZWUjub1Tvb4cSguQozHn5c= +github.com/wailsapp/wails/v2 v2.12.0/go.mod h1:mo1bzK1DEJrobt7YrBjgxvb5Sihb1mhAY09hppbibQg= +github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= +github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0= +github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..5d683b4 --- /dev/null +++ b/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" + "github.com/wailsapp/wails/v2/pkg/options/linux" + "github.com/wailsapp/wails/v2/pkg/options/windows" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + app := NewApp() + + err := wails.Run(&options.App{ + Title: "数据智能匹配工具", + Width: 1280, + Height: 860, + MinWidth: 960, + MinHeight: 640, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 245, G: 247, B: 250, A: 1}, + OnStartup: app.startup, + Windows: &windows.Options{ + WebviewIsTransparent: false, + WindowIsTranslucent: false, + Theme: windows.SystemDefault, + }, + Linux: &linux.Options{ + WindowIsTranslucent: false, + }, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/wails.json b/wails.json new file mode 100644 index 0000000..3e7a8bd --- /dev/null +++ b/wails.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "data-matcher", + "outputfilename": "data-matcher", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "http://localhost:34115", + "author": { + "name": "RainySY", + "email": "chendairong@outlook.com" + } +} \ No newline at end of file