Forráskód Böngészése

通用爬虫标注平台

mxs 10 hónapja
szülő
commit
3a3dc31e51
52 módosított fájl, 4916 hozzáadás és 0 törlés
  1. 6 0
      .gitignore
  2. 68 0
      ai.go
  3. 29 0
      ai_test.go
  4. 152 0
      app.go
  5. 116 0
      browser.go
  6. 35 0
      build/README.md
  7. BIN
      build/appicon.png
  8. 68 0
      build/darwin/Info.dev.plist
  9. 63 0
      build/darwin/Info.plist
  10. BIN
      build/windows/icon.ico
  11. 15 0
      build/windows/info.json
  12. 114 0
      build/windows/installer/project.nsi
  13. 249 0
      build/windows/installer/wails_tools.nsh
  14. 15 0
      build/windows/wails.exe.manifest
  15. 18 0
      cert.pem
  16. BIN
      data.db
  17. 213 0
      db.go
  18. 8 0
      frontend/README.md
  19. 13 0
      frontend/index.html
  20. 1145 0
      frontend/package-lock.json
  21. 20 0
      frontend/package.json
  22. 1 0
      frontend/package.json.md5
  23. 79 0
      frontend/src/App.vue
  24. 153 0
      frontend/src/components/EditSpider.vue
  25. 57 0
      frontend/src/components/InsertSpider.vue
  26. 35 0
      frontend/src/components/Login.vue
  27. 26 0
      frontend/src/components/Navigator.vue
  28. 41 0
      frontend/src/components/ViewArticle.vue
  29. 68 0
      frontend/src/components/jscodetpl.js
  30. 21 0
      frontend/src/main.js
  31. 33 0
      frontend/src/router/index.js
  32. 13 0
      frontend/src/style.css
  33. 340 0
      frontend/src/views/Home.vue
  34. 188 0
      frontend/src/views/Run.vue
  35. 43 0
      frontend/src/views/Setting.vue
  36. 7 0
      frontend/vite.config.js
  37. 35 0
      frontend/wailsjs/go/main/App.d.ts
  38. 67 0
      frontend/wailsjs/go/main/App.js
  39. 113 0
      frontend/wailsjs/go/models.ts
  40. 24 0
      frontend/wailsjs/runtime/package.json
  41. 249 0
      frontend/wailsjs/runtime/runtime.d.ts
  42. 238 0
      frontend/wailsjs/runtime/runtime.js
  43. 62 0
      go.mod
  44. 144 0
      go.sum
  45. 28 0
      key.pem
  46. 43 0
      main.go
  47. 117 0
      service.go
  48. 34 0
      tpl/load_content.js
  49. 23 0
      tpl/load_list_items.js
  50. 56 0
      types.go
  51. 218 0
      vm.go
  52. 13 0
      wails.json

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+pkg
+bin
+*.exe
+*.log
+*/bin
+.idea

+ 68 - 0
ai.go

@@ -0,0 +1,68 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+
+	//"log"
+	"strings"
+
+	zhipu "github.com/itcwc/go-zhipu/model_api"
+)
+
+const (
+	MODEL_NAME = "glm-4-flash" //"glm-4-air"
+)
+
+var (
+	expireAtTime = int64(1719803252) // token 过期时间
+	apiKey       = "5343038d9d934536456f281f8487866a.YUmO7HK9xNb990j9"
+)
+
+// UpdateResultDateStr
+func UpdateResultDateStr(rs ResultItems) (err error) {
+	tmp := make([]string, len(rs))
+	for i, v := range rs {
+		tmp[i] = v.ListPubTime
+	}
+
+	prompt := fmt.Sprintf(`根据我提供的内容,识别每行文本中的日期,按照YYYY-MM-DD形式输出,如:2024-01-01;找不到日期数据,输出NULL。不要联网,不要解释,不要说明,直接输出结果。
+				---------------------------
+				%s				
+				`, strings.Join(tmp, "\n"))
+	mssage := zhipu.PostParams{
+		Model: MODEL_NAME,
+		Messages: []zhipu.Message{
+			{
+				Role:    "user", // 消息的角色信息 详见文档
+				Content: prompt, // 消息内容
+			},
+		},
+	}
+	postResponse, err := zhipu.BeCommonModel(expireAtTime, mssage, apiKey)
+	if err != nil {
+		return err
+	}
+
+	//解析数据
+	choices, _ := postResponse["choices"].([]interface{})
+	obj, _ := choices[0].(map[string]interface{})
+	message, _ := obj["message"].(map[string]interface{})
+	value, _ := message["content"].(string)
+	// log.Println("提示语", prompt)
+	// log.Println("AI调用结果", value)
+	results := strings.Split(value, "\n")
+	if len(results) < len(rs) {
+		err = errors.New("调用大模型失败")
+		return
+	}
+	//更新
+	for i, v := range rs {
+		if results[i] != "NULL" {
+			v.ListPubTime = results[i]
+		} else {
+			v.ListPubTime = ""
+		}
+	}
+	return nil
+}

+ 29 - 0
ai_test.go

@@ -0,0 +1,29 @@
+package main
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestAI(t *testing.T) {
+	tmp := ResultItems{
+		{ListPubTime: "2024年5月1日"},
+		{ListPubTime: "2024/8/1"},
+		{ListPubTime: "中文干扰文本"},
+		{ListPubTime: "中文干扰文本 2024-7-12"},
+	}
+	callAIState := false
+	for i := 0; i < 5; i++ {
+		err := UpdateResultDateStr(tmp)
+		if err == nil {
+			callAIState = true
+			break
+		}
+		fmt.Println("ai调用失败,再次尝试", err.Error())
+	}
+	if callAIState {
+		for _, v := range tmp {
+			fmt.Println(v.ListPubTime)
+		}
+	}
+}

+ 152 - 0
app.go

@@ -0,0 +1,152 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"log"
+
+	"github.com/wailsapp/wails/v2/pkg/runtime"
+)
+
+var (
+	db     *SpiderDb
+	exitCh chan bool
+)
+
+// App struct
+type App struct {
+	ctx context.Context
+}
+
+// NewApp creates a new App application struct
+func NewApp() *App {
+	return &App{}
+}
+
+// startup is called when the app starts. The context is saved
+// so we can call the runtime methods
+func (a *App) startup(ctx context.Context) {
+	a.ctx = ctx
+	db = NewSpiderDb("./data.db")
+}
+
+// destory
+func (a *App) destory(ctx context.Context) {
+	db.Close()
+}
+
+// Greet returns a greeting for the given name
+func (a *App) Greet(name string) string {
+	return fmt.Sprintf("Hello %s, It's show time!", name)
+}
+
+// LoadSpiderConfigAll,带分页
+func (a *App) LoadSpiderConfigAll(pageSize, pageNo int) []*SpiderConfig {
+	return db.LoadAll()
+}
+
+// LoadSpiderConfigAll,带分页
+func (a *App) SaveOrUpdateSpiderConfig(sc *SpiderConfig) string {
+	db.SaveOrUpdate(sc)
+	return "ok"
+}
+
+// SwitchSpiderConfig
+func (a *App) SwitchSpiderConfig(code string) string {
+	log.Println("切换当前默认爬虫配置:", code)
+	db.Switch(code)
+	return "ok"
+}
+
+// SwitchSpiderConfig
+func (a *App) ViewCurrentSpiderConfig() *SpiderConfig {
+	return currentSpiderConfig
+}
+
+// SwitchSpiderConfig
+func (a *App) DeleteSpiderConfig(code string) string {
+	db.Delete(code)
+	return "ok"
+}
+
+// 推送消息
+func (a *App) pushMessage(event string, data interface{}) {
+	runtime.EventsEmit(a.ctx, event, data)
+}
+
+// 调试爬虫
+func (a *App) DebugSpider(url string, listDealy int64, contentDelay int64, headless bool, showImage bool, proxyServe string) {
+	exitCh = make(chan bool, 1)
+	RunSpider(url, listDealy, contentDelay, headless, showImage, proxyServe, exitCh)
+}
+
+// 停止调试
+func (a *App) StopDebugSpider() string {
+	defer func() {
+		if err := recover(); err != nil {
+			log.Println(err)
+		}
+	}()
+	exitCh <- true
+	return "ok"
+}
+
+// 查看所有结果
+func (a *App) ViewResultItemAll() ResultItems {
+	return currentResult
+}
+
+// ExportEpubFile
+func (a *App) ExportEpubFile(filepath string) string {
+	ExportEpubFile(filepath)
+	return "ok"
+}
+
+// SelectSaveFilePath
+func (a *App) SelectSaveFilePath() string {
+	path, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{Filters: []runtime.FileFilter{
+		{Pattern: "*.epub", DisplayName: "epub file *.epub"},
+		{Pattern: "*.xlsx", DisplayName: "excel file *.xlsx"},
+		{Pattern: "*.json", DisplayName: "json file *.json"},
+	}})
+	if err != nil {
+		log.Println(err.Error())
+		return ""
+	}
+	return path
+}
+
+// SelectOpenFilePath
+func (a *App) SelectOpenFilePath() string {
+	path, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{Filters: []runtime.FileFilter{
+		{Pattern: "*.xlsx", DisplayName: "excel file *.xlsx"},
+	}})
+	if err != nil {
+		log.Println(err.Error())
+		return ""
+	}
+	return path
+}
+
+// ImportSpiderConfigByExcelFile 通过excel文件导入爬虫配置
+func (a *App) ImportSpiderConfigByExcelFile(filepath string) string {
+	db.BatchImport(filepath)
+	return "ok"
+}
+
+// 获取login状态
+func (a *App) GetLoginState() bool {
+	return loginState
+}
+
+func (a *App) PutLoginState(state bool) string {
+	loginState = state
+	return "ok"
+}
+
+// CountYestodayArts
+func (a *App) CountYestodayArts(url string, listDealy int64,
+	trunPageDelay int64, headless bool, showImage bool) {
+	exitCh = make(chan bool, 1)
+	CountYestodayArts(url, listDealy, trunPageDelay, headless, showImage, exitCh)
+}

+ 116 - 0
browser.go

@@ -0,0 +1,116 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"math/rand"
+
+	"github.com/chromedp/cdproto/page"
+
+	"github.com/chromedp/chromedp"
+)
+
+var (
+	useragent = []string{
+		"Chrome: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
+		"Firefox: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0",
+		"Safari: Mozilla/5.0 (iPhone; CPU iPhone OS 11_2_5 like Mac OS X) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0 Mobile/15D60 Safari/604.1",
+		"MacOSX: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/602.2.14 (KHTML, like Gecko) Version/10.0.1 Safari/602.2.14",
+		"Mozilla/5.0(Macintosh;U;IntelMacOSX10_6_8;en-us)AppleWebKit/534.50(KHTML,likeGecko)Version/5.1Safari/534.50",
+		"Mozilla/5.0(Windows;U;WindowsNT6.1;en-us)AppleWebKit/534.50(KHTML,likeGecko)Version/5.1Safari/534.50",
+		"Mozilla/5.0(Macintosh;IntelMacOSX10.6;rv:2.0.1)Gecko/20100101Firefox/4.0.1",
+		"Mozilla/5.0(WindowsNT6.1;rv:2.0.1)Gecko/20100101Firefox/4.0.1",
+		"Mozilla/5.0(Macintosh;IntelMacOSX10_7_0)AppleWebKit/535.11(KHTML,likeGecko)Chrome/17.0.963.56Safari/535.11",
+		"Mozilla/4.0(compatible;MSIE7.0;WindowsNT5.1;Trident/4.0;SE2.XMetaSr1.0;SE2.XMetaSr1.0;.NETCLR2.0.50727;SE2.XMetaSr1.0)",
+		"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.70 Safari/537.36",
+		"Chrome 9 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36",
+		"Safari Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Safari/605.1.15",
+		"Safari Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Safari/605.1.15",
+		"Safari 11 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.2 Safari/605.1.15 QQBrowserLite/1.3.0",
+		"Chrome 9 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36",
+		"Chrome 59 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
+		"Chrome 9 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36",
+		"Safari 11 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/604.3.5 (KHTML, like Gecko) Version/11.0.1 Safari/604.3.5",
+		"Firefox 9 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:93.0) Gecko/20100101 Firefox/93.0",
+		"Safari Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15",
+		"Chrome 8 Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36",
+		"Chrome Mozilla/5.0 (X11; U; U; Linux x86_64; zh-my) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 Puffin/8.3.1.41624AP",
+		"Opera 28 Mozilla/5.0 (Linux; BRAVIA 4K 2015 Build/LMY48E.S265) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36 OPR/28.0.1754.0",
+		"Safari Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 HeyTapBrowser/40.7.29.1",
+		"Chrome 9 Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.58 Safari/537.36 Edg/93.0.961.33",
+		"Chrome 9 Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/15.0 Chrome/90.0.4430.210 Safari/537.36",
+		"Chrome 9 Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36",
+		"Chrome Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
+		"Microsoft Edge Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134",
+		"Chrome 8 Mozilla/5.0 (Windows NT 10.0; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36",
+		"Chrome 8 Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
+		"Chrome 9 Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36",
+		"Chrome 8 Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36",
+		"Chrome 9 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
+		"Chrome Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36",
+		"Firefox 7 Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101 Firefox/78.0",
+		"Chrome 9 Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36",
+		"Internet Explorer 11 Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; LCJB; rv:11.0) like Gecko",
+		"Chrome 9 Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36",
+		"Firefox 36  Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0",
+		"Chrome Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3870.400 QQBrowser/10.8.4405.400",
+		"Chrome 58 Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 SE 2.X MetaSr 1.0",
+		"Firefox 9 Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0",
+		"Chrome 8 Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
+		"Chrome 9 Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36 Edg/94.0.992.38",
+	}
+)
+
+func NewBrowser(headless bool, showImage bool, proxyServe string) (
+	context.Context, context.CancelFunc,
+	context.Context, context.CancelFunc,
+	context.Context, context.CancelFunc,
+) {
+	ctx, cancelFn := chromedp.NewContext(context.Background())
+
+	chromeOptions := append(chromedp.DefaultExecAllocatorOptions[:],
+		chromedp.NoDefaultBrowserCheck,                                  //不检查默认浏览器
+		chromedp.Flag("enable-automation", false),                       // 防止监测webdriver
+		chromedp.Flag("disable-blink-features", "AutomationControlled"), //禁用 blink 特征
+		chromedp.Flag("force-dev-mode-highlighting", true),
+		chromedp.Flag("disable-extensions", false), //是否禁用扩展
+		chromedp.Flag("headless", headless),
+		chromedp.Flag("user-agent", useragent[rand.Intn(20)]), //搞到底还是要在这里设置useragent
+		chromedp.Flag("disable-keep-alive", true),
+		chromedp.Flag("disable-gpu", true),
+		chromedp.Flag("no-sandbox", true),
+		chromedp.Flag("disable-dev-shm-usage", false),
+		chromedp.Flag("default-browser-check", false),
+		chromedp.Flag("ignore-certificate-errors", true), //忽略错误
+		chromedp.Flag("disable-web-security", true),      //禁用网络安全标志
+		chromedp.Flag("mute-audio", false),
+		chromedp.Flag("accept-language", `zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-TW;q=0.6`),
+	)
+	if proxyServe != "" {
+		chromeOptions = append(chromeOptions,
+			chromedp.ProxyServer(fmt.Sprintf("socks5://%s", proxyServe)),
+		)
+	}
+	if showImage {
+		chromeOptions = append(chromeOptions,
+			chromedp.Flag("blink-settings", "imagesEnabled=true"),
+		)
+	} else {
+		chromeOptions = append(chromeOptions,
+			chromedp.Flag("blink-settings", "imagesEnabled=false"),
+		)
+	}
+
+	allocCtx, allocCancelFn := chromedp.NewExecAllocator(ctx, chromeOptions...)
+	// 创建一个浏览器实例
+	incCtx, incCancelFn := chromedp.NewContext(allocCtx,
+		chromedp.WithLogf(nil))
+	//
+	chromedp.Run(ctx,
+		chromedp.ActionFunc(func(cxt context.Context) error {
+			_, err := page.AddScriptToEvaluateOnNewDocument("Object.defineProperty(navigator, 'webdriver', { get: () => false, });").Do(cxt)
+			return err
+		}),
+	)
+	return ctx, cancelFn, allocCtx, allocCancelFn, incCtx, incCancelFn
+}

+ 35 - 0
build/README.md

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

BIN
build/appicon.png


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

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

+ 63 - 0
build/darwin/Info.plist

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

BIN
build/windows/icon.ico


+ 15 - 0
build/windows/info.json

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

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

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

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

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

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

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

+ 18 - 0
cert.pem

@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC+TCCAeGgAwIBAgIQWG6URMK1Ue9VcGjsy8zypjANBgkqhkiG9w0BAQsFADAS
+MRAwDgYDVQQKEwdBY21lIENvMB4XDTI0MDkwNTIxNTM1MloXDTI1MDkwNTIxNTM1
+MlowEjEQMA4GA1UEChMHQWNtZSBDbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
+AQoCggEBAK6HcRaSQiUXu1poHgToSSyrJZoC9ry4R6m+4DRMMHjh9tV0ZD0Nikdo
+HVcgneeaIgs+3ZQJTbG7NP2IHVTX26nAIpM4TlkDEXtx+uJnNH5h0V/vtwVk0lE7
+Hv3cwOxzDbVYuIJO23EAII3Dh/BEhT1tL50xtaS6hOUDeYcVv7BRqdcpNMaNVpC3
+156N9lCzYVlEL8/W/km8M3QQa6bpOo6iyHj68VLVjsd8hQG19XctpXnz/RPfZONS
+YqM04fGddRdNtcy+TUBC/qdYxtSJwI9nPNx+K9DynyBVwQ1ppRaPOl9mPAwyxzFp
+CAkr+m3adFWNaDVsWIrQaZNWzh8DWh8CAwEAAaNLMEkwDgYDVR0PAQH/BAQDAgWg
+MBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFAYDVR0RBA0wC4IJ
+bG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQCLAfeh30waA5o5za1CUNJheAr0
+m/mwmVxTdWdxLdwOZTcGdNHnpdXKfwoPVt1ZaSbBKxj9+1haw2FFXjl01pczOuMK
+HSmDOqeJFCtovJVoHazXw+5o35fA5iJukENUmGhE3w3YI7gBaxwIRD/3dO17IkmN
+JJtqmYpFp6WsTYV50v083a8hEqX2FGF4b74l6MbVV/Q1XHBc60HL2UWIwop/V/3s
+KP0zqnSzC/eh7qfFFGFEZ0Xlumt2Pc7+96+0QflZOFrOQIIIskFo4jMqdblB3EV+
+4tx1shZdMnHT6fX6kmKaG6663qgRaMaMIW1DiWdsP+5mh7wqDyPiTmkbvaHh
+-----END CERTIFICATE-----

BIN
data.db


+ 213 - 0
db.go

@@ -0,0 +1,213 @@
+package main
+
+import (
+	"encoding/json"
+	"sort"
+
+	"log"
+
+	"github.com/boltdb/bolt"
+	"github.com/xuri/excelize/v2"
+)
+
+type (
+	//SpiderDB 爬虫库,这里模拟真实数据库
+	SpiderDb struct {
+		db *bolt.DB
+	}
+)
+
+var (
+	loginState          bool = false
+	currentSpiderConfig *SpiderConfig
+)
+
+// NewSpiderDb
+func NewSpiderDb(dbfile string) *SpiderDb {
+	db, err := bolt.Open(dbfile, 0600, nil)
+	if err != nil {
+		log.Fatal(err)
+	}
+	err = db.Update(func(tx *bolt.Tx) error {
+		_, err := tx.CreateBucketIfNotExists([]byte("myBucket"))
+		return err
+	})
+	if err != nil {
+		log.Fatal(err)
+	}
+	return &SpiderDb{
+		db,
+	}
+}
+
+// Close
+func (s *SpiderDb) Close() {
+	s.db.Close()
+}
+
+// CopyAttribute
+func CopyAttribute(dst *string, value1, value2 string) {
+	if value1 != "" {
+		*dst = value1
+	} else if value2 != "" {
+		*dst = value2
+	}
+}
+
+// MergeSpiderConfig 合并
+func MergeSpiderConfig(src1, src2 *SpiderConfig) *SpiderConfig {
+	nsc := new(SpiderConfig)
+	CopyAttribute(&nsc.Code, src2.Code, src1.Code)
+	CopyAttribute(&nsc.Site, src2.Site, src1.Site)
+	CopyAttribute(&nsc.Channel, src2.Channel, src1.Channel)
+	CopyAttribute(&nsc.Url, src2.Url, src1.Url)
+	CopyAttribute(&nsc.Author, src2.Author, src1.Author)
+	CopyAttribute(&nsc.ListItemCss, src2.ListItemCss, src1.ListItemCss)
+	CopyAttribute(&nsc.ListLinkCss, src2.ListLinkCss, src1.ListLinkCss)
+	CopyAttribute(&nsc.ListPubtimeCss, src2.ListPubtimeCss, src1.ListPubtimeCss)
+	CopyAttribute(&nsc.ListNextPageCss, src2.ListNextPageCss, src1.ListNextPageCss)
+	CopyAttribute(&nsc.TitleCss, src2.TitleCss, src1.TitleCss)
+	CopyAttribute(&nsc.PublishTimeCss, src2.PublishTimeCss, src1.PublishTimeCss)
+	CopyAttribute(&nsc.PublishUnitCss, src2.PublishUnitCss, src1.PublishUnitCss)
+	CopyAttribute(&nsc.ContentCss, src2.ContentCss, src1.ContentCss)
+	CopyAttribute(&nsc.AttachCss, src2.AttachCss, src1.AttachCss)
+	CopyAttribute(&nsc.ListJSCode, src2.ListJSCode, src1.ListJSCode)
+	CopyAttribute(&nsc.ContentJSCode, src2.ContentJSCode, src1.ContentJSCode)
+	CopyAttribute(&nsc.AttachJSCode, src2.AttachJSCode, src1.AttachJSCode)
+	return nsc
+}
+
+// Load
+func (s *SpiderDb) Load(code string) *SpiderConfig {
+	var req *SpiderConfig = new(SpiderConfig)
+	err := s.db.View(func(tx *bolt.Tx) error {
+		bucket := tx.Bucket([]byte("myBucket"))
+		value := bucket.Get([]byte(code))
+		if value != nil && len(value) > 0 {
+			_ = json.Unmarshal(value, req)
+		}
+		return nil
+	})
+	if err != nil {
+		log.Fatal(err)
+	}
+	return req
+}
+
+// SaveOrUpdate
+func (s *SpiderDb) SaveOrUpdate(sc *SpiderConfig) {
+	//加载原始数据
+	var sc1 *SpiderConfig = new(SpiderConfig)
+	var sc2 *SpiderConfig
+	err := s.db.View(func(tx *bolt.Tx) error {
+		bucket := tx.Bucket([]byte("myBucket"))
+		value := bucket.Get([]byte(sc.Code))
+		if value != nil && len(value) > 0 {
+			_ = json.Unmarshal(value, sc1)
+		}
+		return nil
+	})
+	if err != nil {
+		log.Println(err.Error())
+		return
+	}
+	//更新
+	if sc1 != nil {
+		sc2 = MergeSpiderConfig(sc1, sc)
+		value, _ := json.Marshal(sc2)
+		err = s.db.Update(func(tx *bolt.Tx) error {
+			bucket := tx.Bucket([]byte("myBucket"))
+			err := bucket.Put([]byte(sc.Code), value)
+			return err
+		})
+		if err != nil {
+			log.Println(err.Error())
+			return
+		}
+	}
+}
+
+// LoadAll,默认按照代码排序
+func (s *SpiderDb) LoadAll() SpiderConfiges {
+	ret := make(SpiderConfiges, 0)
+	// 开始读取事务
+	err := s.db.View(func(tx *bolt.Tx) error {
+		// 遍历数据库中的所有桶
+		bucket := tx.Bucket([]byte("myBucket"))
+		// 遍历桶中的所有键/值对
+		return bucket.ForEach(func(k, v []byte) error {
+			var sf *SpiderConfig = new(SpiderConfig)
+			json.Unmarshal(v, sf)
+			if sf != nil {
+				ret = append(ret, sf)
+			}
+			return nil
+		})
+	})
+	sort.Sort(ret)
+	if err != nil {
+		log.Println(err.Error())
+	}
+	return ret
+}
+
+// 切换当前默认爬虫配置
+func (s *SpiderDb) Switch(code string) {
+	if sc := s.Load(code); sc != nil {
+		currentSpiderConfig = sc
+	}
+}
+
+// Delete
+func (s *SpiderDb) Delete(code string) {
+	err := s.db.Update(func(tx *bolt.Tx) error {
+		bucket := tx.Bucket([]byte("myBucket"))
+		err := bucket.Delete([]byte(code))
+		return err
+	})
+	if err != nil {
+		log.Println(err.Error())
+		return
+	}
+}
+
+// 批量导入
+func (s *SpiderDb) BatchImport(filepath string) error {
+	f, err := excelize.OpenFile(filepath)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	for _, sheetName := range f.GetSheetList() {
+		// 获取工作表的所有行
+		rows, err := f.GetRows(sheetName)
+		if err != nil {
+			continue
+		}
+		//
+		for index, row := range rows {
+			if index == 0 || len(row) < 5 || row[0] == "" || row[3] == "" {
+				continue
+			}
+			sc := &SpiderConfig{
+				Code:    row[0],
+				Site:    row[1],
+				Channel: row[2],
+				Url:     row[3],
+				Author:  row[4],
+			}
+			value, _ := json.Marshal(sc)
+			err = s.db.Update(func(tx *bolt.Tx) error {
+				bucket := tx.Bucket([]byte("myBucket"))
+				err := bucket.Put([]byte(sc.Code), value)
+				return err
+			})
+			if err != nil {
+				continue
+			}
+		}
+
+	}
+
+	return nil
+}

+ 8 - 0
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 `<script setup>` SFCs,
+check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+
+## Recommended IDE Setup
+
+- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)

+ 13 - 0
frontend/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8"/>
+    <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
+    <title>spidercreator</title>
+</head>
+<body>
+<div id="app"></div>
+<script src="./src/main.js" type="module"></script>
+</body>
+</html>
+

+ 1145 - 0
frontend/package-lock.json

@@ -0,0 +1,1145 @@
+{
+  "name": "frontend",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "frontend",
+      "version": "0.0.0",
+      "dependencies": {
+        "element-plus": "^2.8.2",
+        "vue": "^3.2.37",
+        "vue-router": "^4.4.3"
+      },
+      "devDependencies": {
+        "@vitejs/plugin-vue": "^3.0.3",
+        "vite": "^3.0.7"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.24.8",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
+      "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.24.7",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
+      "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.25.6",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@babel/parser/-/parser-7.25.6.tgz",
+      "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.25.6"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.25.6",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@babel/types/-/types-7.25.6.tgz",
+      "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.24.8",
+        "@babel/helper-validator-identifier": "^7.24.7",
+        "to-fast-properties": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@ctrl/tinycolor": {
+      "version": "3.6.1",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
+      "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@element-plus/icons-vue": {
+      "version": "2.3.1",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz",
+      "integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==",
+      "license": "MIT",
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@esbuild/android-arm/-/android-arm-0.15.18.tgz",
+      "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz",
+      "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@floating-ui/core": {
+      "version": "1.6.7",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@floating-ui/core/-/core-1.6.7.tgz",
+      "integrity": "sha512-yDzVT/Lm101nQ5TCVeK65LtdN7Tj4Qpr9RTXJ2vPFLqtLxwOrpoxAHAJI8J3yYWUc40J0BDBheaitK5SJmno2g==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/utils": "^0.2.7"
+      }
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "1.6.10",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@floating-ui/dom/-/dom-1.6.10.tgz",
+      "integrity": "sha512-fskgCFv8J8OamCmyun8MfjB1Olfn+uZKjOKZ0vhYF3gRmEUXcGOjxWL8bBr7i4kIuPZ2KD2S3EUIOxnjC8kl2A==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/core": "^1.6.0",
+        "@floating-ui/utils": "^0.2.7"
+      }
+    },
+    "node_modules/@floating-ui/utils": {
+      "version": "0.2.7",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@floating-ui/utils/-/utils-0.2.7.tgz",
+      "integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.0",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+      "license": "MIT"
+    },
+    "node_modules/@popperjs/core": {
+      "name": "@sxzz/popperjs-es",
+      "version": "2.11.7",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
+      "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs"
+      }
+    },
+    "node_modules/@types/lodash": {
+      "version": "4.17.7",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@types/lodash/-/lodash-4.17.7.tgz",
+      "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/lodash-es": {
+      "version": "4.17.12",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+      "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/lodash": "*"
+      }
+    },
+    "node_modules/@types/web-bluetooth": {
+      "version": "0.0.16",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
+      "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==",
+      "license": "MIT"
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "3.2.0",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@vitejs/plugin-vue/-/plugin-vue-3.2.0.tgz",
+      "integrity": "sha512-E0tnaL4fr+qkdCNxJ+Xd0yM31UwMkQje76fsDVBBUCoGOUPexu2VDUYHL8P4CwV+zMvWw6nlRw19OnRKmYAJpw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^3.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.3",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@vue/compiler-core/-/compiler-core-3.5.3.tgz",
+      "integrity": "sha512-adAfy9boPkP233NTyvLbGEqVuIfK/R0ZsBsIOW4BZNfb4BRpRW41Do1u+ozJpsb+mdoy80O20IzAsHaihRb5qA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.25.3",
+        "@vue/shared": "3.5.3",
+        "entities": "^4.5.0",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.0"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.3",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@vue/compiler-dom/-/compiler-dom-3.5.3.tgz",
+      "integrity": "sha512-wnzFArg9zpvk/811CDOZOadJRugf1Bgl/TQ3RfV4nKfSPok4hi0w10ziYUQR6LnnBAUlEXYLUfZ71Oj9ds/+QA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.3",
+        "@vue/shared": "3.5.3"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.3",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@vue/compiler-sfc/-/compiler-sfc-3.5.3.tgz",
+      "integrity": "sha512-P3uATLny2tfyvMB04OQFe7Sczteno7SLFxwrOA/dw01pBWQHB5HL15a8PosoNX2aG/EAMGqnXTu+1LnmzFhpTQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.25.3",
+        "@vue/compiler-core": "3.5.3",
+        "@vue/compiler-dom": "3.5.3",
+        "@vue/compiler-ssr": "3.5.3",
+        "@vue/shared": "3.5.3",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.11",
+        "postcss": "^8.4.44",
+        "source-map-js": "^1.2.0"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.3",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@vue/compiler-ssr/-/compiler-ssr-3.5.3.tgz",
+      "integrity": "sha512-F/5f+r2WzL/2YAPl7UlKcJWHrvoZN8XwEBLnT7S4BXwncH25iDOabhO2M2DWioyTguJAGavDOawejkFXj8EM1w==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.3",
+        "@vue/shared": "3.5.3"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "6.6.3",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@vue/devtools-api/-/devtools-api-6.6.3.tgz",
+      "integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.3",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@vue/reactivity/-/reactivity-3.5.3.tgz",
+      "integrity": "sha512-2w61UnRWTP7+rj1H/j6FH706gRBHdFVpIqEkSDAyIpafBXYH8xt4gttstbbCWdU3OlcSWO8/3mbKl/93/HSMpw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.3"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.3",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@vue/runtime-core/-/runtime-core-3.5.3.tgz",
+      "integrity": "sha512-5b2AQw5OZlmCzSsSBWYoZOsy75N4UdMWenTfDdI5bAzXnuVR7iR8Q4AOzQm2OGoA41xjk53VQKrqQhOz2ktWaw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.3",
+        "@vue/shared": "3.5.3"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.3",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@vue/runtime-dom/-/runtime-dom-3.5.3.tgz",
+      "integrity": "sha512-wPR1DEGc3XnQ7yHbmkTt3GoY0cEnVGQnARRdAkDzZ8MbUKEs26gogCQo6AOvvgahfjIcnvWJzkZArQ1fmWjcSg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.3",
+        "@vue/runtime-core": "3.5.3",
+        "@vue/shared": "3.5.3",
+        "csstype": "^3.1.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.3",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@vue/server-renderer/-/server-renderer-3.5.3.tgz",
+      "integrity": "sha512-28volmaZVG2PGO3V3+gBPKoSHvLlE8FGfG/GKXKkjjfxLuj/50B/0OQGakM/g6ehQeqCrZYM4eHC4Ks48eig1Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.3",
+        "@vue/shared": "3.5.3"
+      },
+      "peerDependencies": {
+        "vue": "3.5.3"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.3",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@vue/shared/-/shared-3.5.3.tgz",
+      "integrity": "sha512-Jp2v8nylKBT+PlOUjun2Wp/f++TfJVFjshLzNtJDdmFJabJa7noGMncqXRM1vXGX+Yo2V7WykQFNxusSim8SCA==",
+      "license": "MIT"
+    },
+    "node_modules/@vueuse/core": {
+      "version": "9.13.0",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@vueuse/core/-/core-9.13.0.tgz",
+      "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/web-bluetooth": "^0.0.16",
+        "@vueuse/metadata": "9.13.0",
+        "@vueuse/shared": "9.13.0",
+        "vue-demi": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/core/node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vueuse/metadata": {
+      "version": "9.13.0",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@vueuse/metadata/-/metadata-9.13.0.tgz",
+      "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/shared": {
+      "version": "9.13.0",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/@vueuse/shared/-/shared-9.13.0.tgz",
+      "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
+      "license": "MIT",
+      "dependencies": {
+        "vue-demi": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/shared/node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/async-validator": {
+      "version": "4.2.5",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/async-validator/-/async-validator-4.2.5.tgz",
+      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+      "license": "MIT"
+    },
+    "node_modules/csstype": {
+      "version": "3.1.3",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/csstype/-/csstype-3.1.3.tgz",
+      "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+      "license": "MIT"
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.13",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/dayjs/-/dayjs-1.11.13.tgz",
+      "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
+      "license": "MIT"
+    },
+    "node_modules/element-plus": {
+      "version": "2.8.2",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/element-plus/-/element-plus-2.8.2.tgz",
+      "integrity": "sha512-pqoQlaUmzUFCjjTHyxGO7Cd0CizsQpIaad1ozV9PCaYjh2T4MIMnjfifqiYs2lWoZ/8GVwrRG1WTCfnZEjwfcg==",
+      "license": "MIT",
+      "dependencies": {
+        "@ctrl/tinycolor": "^3.4.1",
+        "@element-plus/icons-vue": "^2.3.1",
+        "@floating-ui/dom": "^1.0.1",
+        "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
+        "@types/lodash": "^4.14.182",
+        "@types/lodash-es": "^4.17.6",
+        "@vueuse/core": "^9.1.0",
+        "async-validator": "^4.2.5",
+        "dayjs": "^1.11.3",
+        "escape-html": "^1.0.3",
+        "lodash": "^4.17.21",
+        "lodash-es": "^4.17.21",
+        "lodash-unified": "^1.0.2",
+        "memoize-one": "^6.0.0",
+        "normalize-wheel-es": "^1.2.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    },
+    "node_modules/entities": {
+      "version": "4.5.0",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/entities/-/entities-4.5.0.tgz",
+      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild/-/esbuild-0.15.18.tgz",
+      "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/android-arm": "0.15.18",
+        "@esbuild/linux-loong64": "0.15.18",
+        "esbuild-android-64": "0.15.18",
+        "esbuild-android-arm64": "0.15.18",
+        "esbuild-darwin-64": "0.15.18",
+        "esbuild-darwin-arm64": "0.15.18",
+        "esbuild-freebsd-64": "0.15.18",
+        "esbuild-freebsd-arm64": "0.15.18",
+        "esbuild-linux-32": "0.15.18",
+        "esbuild-linux-64": "0.15.18",
+        "esbuild-linux-arm": "0.15.18",
+        "esbuild-linux-arm64": "0.15.18",
+        "esbuild-linux-mips64le": "0.15.18",
+        "esbuild-linux-ppc64le": "0.15.18",
+        "esbuild-linux-riscv64": "0.15.18",
+        "esbuild-linux-s390x": "0.15.18",
+        "esbuild-netbsd-64": "0.15.18",
+        "esbuild-openbsd-64": "0.15.18",
+        "esbuild-sunos-64": "0.15.18",
+        "esbuild-windows-32": "0.15.18",
+        "esbuild-windows-64": "0.15.18",
+        "esbuild-windows-arm64": "0.15.18"
+      }
+    },
+    "node_modules/esbuild-android-64": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz",
+      "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-android-arm64": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz",
+      "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-darwin-64": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz",
+      "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-darwin-arm64": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz",
+      "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-freebsd-64": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz",
+      "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-freebsd-arm64": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz",
+      "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-32": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz",
+      "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-64": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz",
+      "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-arm": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz",
+      "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-arm64": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz",
+      "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-mips64le": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz",
+      "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-ppc64le": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz",
+      "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-riscv64": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz",
+      "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-s390x": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz",
+      "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-netbsd-64": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz",
+      "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-openbsd-64": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz",
+      "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-sunos-64": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz",
+      "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-windows-32": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz",
+      "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-windows-64": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz",
+      "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-windows-arm64": {
+      "version": "0.15.18",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz",
+      "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT"
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/is-core-module": {
+      "version": "2.15.1",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/is-core-module/-/is-core-module-2.15.1.tgz",
+      "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-es": {
+      "version": "4.17.21",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/lodash-es/-/lodash-es-4.17.21.tgz",
+      "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-unified": {
+      "version": "1.0.3",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/lodash-unified/-/lodash-unified-1.0.3.tgz",
+      "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/lodash-es": "*",
+        "lodash": "*",
+        "lodash-es": "*"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.11",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/magic-string/-/magic-string-0.30.11.tgz",
+      "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0"
+      }
+    },
+    "node_modules/memoize-one": {
+      "version": "6.0.0",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/memoize-one/-/memoize-one-6.0.0.tgz",
+      "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.7",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/nanoid/-/nanoid-3.3.7.tgz",
+      "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/normalize-wheel-es": {
+      "version": "1.2.0",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
+      "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.0",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/picocolors/-/picocolors-1.1.0.tgz",
+      "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
+      "license": "ISC"
+    },
+    "node_modules/postcss": {
+      "version": "8.4.45",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/postcss/-/postcss-8.4.45.tgz",
+      "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.7",
+        "picocolors": "^1.0.1",
+        "source-map-js": "^1.2.0"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/resolve": {
+      "version": "1.22.8",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/resolve/-/resolve-1.22.8.tgz",
+      "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-core-module": "^2.13.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "2.79.1",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/rollup/-/rollup-2.79.1.tgz",
+      "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.0",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/source-map-js/-/source-map-js-1.2.0.tgz",
+      "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/to-fast-properties": {
+      "version": "2.0.0",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+      "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/vite": {
+      "version": "3.2.10",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/vite/-/vite-3.2.10.tgz",
+      "integrity": "sha512-Dx3olBo/ODNiMVk/cA5Yft9Ws+snLOXrhLtrI3F4XLt4syz2Yg8fayZMWScPKoz12v5BUv7VEmQHnsfpY80fYw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.15.9",
+        "postcss": "^8.4.18",
+        "resolve": "^1.22.1",
+        "rollup": "^2.79.1"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      },
+      "peerDependencies": {
+        "@types/node": ">= 14",
+        "less": "*",
+        "sass": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue": {
+      "version": "3.5.3",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/vue/-/vue-3.5.3.tgz",
+      "integrity": "sha512-xvRbd0HpuLovYbOHXRHlSBsSvmUJbo0pzbkKTApWnQGf3/cu5Z39mQeA5cZdLRVIoNf3zI6MSoOgHUT5i2jO+Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.3",
+        "@vue/compiler-sfc": "3.5.3",
+        "@vue/runtime-dom": "3.5.3",
+        "@vue/server-renderer": "3.5.3",
+        "@vue/shared": "3.5.3"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-router": {
+      "version": "4.4.3",
+      "resolved": "https://mirrors.cloud.tencent.com/npm/vue-router/-/vue-router-4.4.3.tgz",
+      "integrity": "sha512-sv6wmNKx2j3aqJQDMxLFzs/u/mjA9Z5LCgy6BE0f7yFWMjrPLnS/sPNn8ARY/FXw6byV18EFutn5lTO6+UsV5A==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.3"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    }
+  }
+}

+ 20 - 0
frontend/package.json

@@ -0,0 +1,20 @@
+{
+  "name": "frontend",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "element-plus": "^2.8.2",
+    "vue": "^3.2.37",
+    "vue-router": "^4.4.3"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^3.0.3",
+    "vite": "^3.0.7"
+  }
+}

+ 1 - 0
frontend/package.json.md5

@@ -0,0 +1 @@
+891fae62404d5758340e325f88e4e87e

+ 79 - 0
frontend/src/App.vue

@@ -0,0 +1,79 @@
+<template>
+  <el-container style="height: 100vh;">
+    <el-aside :width="sidebarWidth" style="background-color: #333;overflow: hidden;  ">
+      <div class="sidebar-header" @click="toggleSidebar">
+        <el-icon :size="20" color="#fff">
+          <Fold v-if="!isCollapse" />
+          <Expand v-else />
+        </el-icon>
+      </div>
+      <el-menu
+        :default-active="activeIndex"
+        class="el-menu-vertical-demo"
+        :collapse="isCollapse"
+        background-color="#333"
+        text-color="#fff"
+        router
+        active-text-color="#ffd04b">
+        <el-menu-item index="/">
+          <el-icon><Guide /></el-icon>
+          <span slot="title">爬虫开发</span>
+        </el-menu-item>
+        <el-menu-item index="/setting">
+          <el-icon><Setting /></el-icon>
+          <span slot="title">系统设置</span> 
+        </el-menu-item>
+      </el-menu>
+    </el-aside>
+    
+    <el-container>
+      <el-header style="background-color: #F7F7F7; padding: 0;">
+        <h3><el-icon><Guide /></el-icon>剑鱼可视化爬虫开发平台 v1.0</h3>
+      </el-header>
+      <el-main>
+        <router-view></router-view>
+      </el-main>
+    </el-container>
+  </el-container>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+
+const isCollapse = ref(false);
+const activeIndex = ref('/');
+const sidebarWidth = ref('140px');
+
+function toggleSidebar() {
+  isCollapse.value = !isCollapse.value;
+  sidebarWidth.value = isCollapse.value ? '70px' : '160px';
+}
+toggleSidebar()
+</script>
+
+<style>
+body {
+  margin: 0;
+}
+.el-header {
+  align-items: left;
+  height: 10vh;
+ }
+ 
+.sidebar-header {
+  height: 60px;
+  background-color: #333;
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+}
+.el-menu-vertical-demo {
+  overflow: hidden; 
+  border:none;
+}
+
+ 
+ 
+</style>

+ 153 - 0
frontend/src/components/EditSpider.vue

@@ -0,0 +1,153 @@
+<!-- 新增爬虫 -->
+<template>
+    <el-dialog title="仅编辑 CSS选择器部分" v-model="dialogVisible" :close-on-click-modal="false" width="80%">
+        <div class="flex gap-2">
+            <el-space>
+                <el-tag type="primary">{{ formData.site }} </el-tag>
+                <el-tag type="success">{{ formData.channel }} </el-tag>
+                <el-tag type="info">{{ formData.code }} </el-tag>
+                <el-tag type="warning">{{ formData.url }} </el-tag>
+                <el-tag type="danger">{{ formData.author }} </el-tag>
+            </el-space>
+        </div>
+        <div class="space" />
+        <el-tabs v-model="activeName" class="demo-tabs">
+            <el-tab-pane label="列表页CSS选择器" name="first">
+                <el-form ref="form1" :model="formData" label-width="160px">
+                    <el-row>
+                        <el-col :span="12">
+                            <el-form-item label="列表条目">
+                                <el-input v-model="formData.listItemCss" placeholder="ul.list li"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="12">
+                            <el-form-item label="列表条目链接">
+                                <el-input v-model="formData.listLinkCss" placeholder="a"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                    <el-row>
+                        <el-col :span="12">
+                            <el-form-item label="列表条目发布时间">
+                                <el-input v-model="formData.listPublishTimeCss" placeholder="span"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="12">
+                            <el-form-item label="列表翻页下一页">
+                                <el-input v-model="formData.listNextPageCss" placeholder="li.nextpage a"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-form>
+                <el-divider>手写列表页筛选JS代码<el-button type="primary"  @click='editorHandle.ImportListCode'>导入样例</el-button></el-divider>
+                <el-input v-model="formData.listJs" style="width: 100%" :rows="6" type="textarea"
+                    placeholder="Please input" />
+                    <div class="space"/>
+            </el-tab-pane>
+            <el-tab-pane label="详情页CSS选择器" name="second">
+                <el-form ref="form2" :model="formData" label-width="160px">
+                    <el-row>
+                        <el-col :span="12">
+                            <el-form-item label="详情页标题">
+                                <el-input v-model="formData.titleCss" placeholder="ul.list li"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="12">
+                            <el-form-item label="详情页发布时间">
+                                <el-input v-model="formData.publishTimeCss" placeholder="a"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                    <el-row>
+                        <el-col :span="12">
+                            <el-form-item label="详情页发布单位">
+                                <el-input v-model="formData.publishUnitCss" placeholder="span"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="12">
+                            <el-form-item label="详情页正文">
+                                <el-input v-model="formData.contentCss" placeholder="li.nextpage a"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                    <el-row>
+                        <el-col :span="24">
+                            <el-form-item label="详情页附件">
+                                <el-input v-model="formData.attachCss" placeholder="span"></el-input>
+                            </el-form-item>
+                        </el-col>
+
+                    </el-row>
+                </el-form>
+                <el-divider>手写详情页筛选JS代码<el-button type="primary"  @click='editorHandle.ImportContentCode'>导入样例</el-button></el-divider>
+                <el-input v-model="formData.contentJs" style="width: 100%" :rows="6" type="textarea"
+                    placeholder="Please input" />
+                    <div class="space"/>
+            </el-tab-pane>
+            <el-tab-pane label="详情页附件下载" name="third">
+                <el-divider>手写附件下载/上传JS代码 <el-button type="primary" @click='editorHandle.ImportAttachCode'>导入样例</el-button></el-divider>
+                <el-input v-model="formData.attachJs" class="codeEditor"   :rows="6" type="textarea"
+                    placeholder="Please input" />
+            </el-tab-pane>
+            <div class="space"/>
+        </el-tabs>
+
+
+        <span slot="footer" class="dialog-footer">
+            <el-button @click="dialogVisible = false">取 消</el-button>
+            <el-button type="primary" @click="handleSave">保 存</el-button>
+        </span>
+    </el-dialog>
+</template>
+<script setup>
+import { ref, defineEmits } from 'vue';
+import {TemplateJsCode} from './jscodetpl.js'
+const emit = defineEmits(['custom-event']);
+//表单数据
+const formData = ref({
+    site: '',
+    channel: '',
+    url: '',
+    code: '',
+    author: '',
+});
+
+const activeName = ref("first")
+const dialogVisible = ref(false)
+
+//编辑器事件管理
+const editorHandle={
+    ImportListCode:()=>{
+        formData.value.listJs=TemplateJsCode.ListJsCode
+    },
+    ImportContentCode:()=>{
+        formData.value.contentJs=TemplateJsCode.ContentJsCode
+    },
+    ImportAttachCode:()=>{
+        formData.value.attachJs=TemplateJsCode.AttachJsCode
+    },
+}
+
+const handleSave = () => {
+    dialogVisible.value = false;
+    emit("custom-event", { ...formData.value })
+    formData.value = {}
+}
+
+
+//这里是重点
+defineExpose({
+    dialogVisible,
+    formData,
+    emit
+})
+</script>
+<style>
+.codeEditor{
+    width:100%;
+    font-size:12pt;
+    font-weight: bold;
+    line-height: 1.5em;
+    padding: 10px;
+}
+</style>

+ 57 - 0
frontend/src/components/InsertSpider.vue

@@ -0,0 +1,57 @@
+<!-- 新增爬虫 -->
+<template>
+    <el-dialog title="信息录入" v-model="dialogVisible" :close-on-click-modal="false" width="50%">
+        <el-form ref="form" :model="formData" label-width="80px">
+            <el-form-item label="站点">
+                <el-input v-model="formData.site" placeholder="剑鱼"></el-input>
+            </el-form-item>
+            <el-form-item label="栏目">
+                <el-input v-model="formData.channel" placeholder="中标结果"></el-input>
+            </el-form-item>
+            <el-form-item label="地址">
+                <el-input v-model="formData.url" placeholder="https://www.jianyu360.cn"></el-input>
+            </el-form-item>
+            <el-form-item label="代码">
+                <el-input v-model="formData.code" placeholder="0015"></el-input>
+            </el-form-item>
+            <el-form-item label="开发者">
+                <el-input v-model="formData.author" placeholder="外包1"></el-input>
+            </el-form-item>
+        </el-form>
+        <span slot="footer" class="dialog-footer">
+            <el-button @click="dialogVisible = false">取 消</el-button>
+            <el-button type="primary" @click="handleSave">保 存</el-button>
+        </span>
+    </el-dialog>
+</template>
+<script setup>
+import { ref, defineComponent } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus'
+const formData = ref({
+    site: '',
+    channel: '',
+    url: '',
+    code: '',
+    author: '',
+});
+
+const dialogVisible = ref(false);
+
+const props = defineProps({
+    onSubmit: Function,
+});
+
+const handleSave = () => {
+    dialogVisible.value = false;
+    props.onSubmit({ ...formData.value });
+    formData.value = {}
+    ElMessageBox.alert('已经成功添加爬虫任务', '提示信息', {
+        callback: (action) => {
+        },
+    })
+}
+//这里是重点
+defineExpose({
+    dialogVisible
+})
+</script>

+ 35 - 0
frontend/src/components/Login.vue

@@ -0,0 +1,35 @@
+<template>
+    <el-dialog width="40%" title="用户登录" v-model="dialogVisible" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
+        <el-form :model="formData" label-width="80px">
+            <el-form-item label="用户名">
+                <el-input v-model="formData.username" placeholder="usercode"></el-input>
+            </el-form-item>
+            <el-form-item label="密码">
+                <el-input type="password" v-model="formData.password"></el-input>
+            </el-form-item>
+            <el-form-item>
+                <el-button type="primary" @click="submitForm">登录</el-button>
+            </el-form-item>
+        </el-form>
+    </el-dialog>
+</template>
+<script setup>
+import { ref, defineEmits, defineExpose } from 'vue';
+const dialogVisible = ref(false)
+const emit = defineEmits(['login-event']);
+
+//实现用户身份登录
+const submitForm=()=>{
+    //TODO
+    dialogVisible.value=false
+    emit('login-event',{...formData.value})
+}
+
+//
+const formData = ref({})
+//这里是重点
+defineExpose({
+    dialogVisible,
+    emit
+})
+</script>

+ 26 - 0
frontend/src/components/Navigator.vue

@@ -0,0 +1,26 @@
+<template>
+  <el-page-header @back="goBack" title="返回">
+    <template #content>
+      <span class="text-large font-600 mr-3"> {{ pageTitle }} </span>
+    </template>
+  </el-page-header>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { ElPageHeader } from 'element-plus';
+import { useRouter } from 'vue-router';
+const router = useRouter();
+const props = defineProps({
+  pageTitle: {
+    type: String,
+    required: true,
+  },
+});
+
+const emit = defineEmits();
+
+function goBack() {
+  router.go(-1);
+}
+</script>

+ 41 - 0
frontend/src/components/ViewArticle.vue

@@ -0,0 +1,41 @@
+<!-- 文章查看 -->
+<template>
+    <el-dialog width="80%" title="文章查看" v-model="dialogVisible">
+        <el-row>
+            <el-col :span="24">
+                <h3>{{ formData.title }}</h3>
+            </el-col>
+        </el-row>
+        <el-row>
+            <el-col :span="24">
+                <el-space>
+                    <el-tag type="primary">{{ formData.publishTime }} </el-tag>
+                    <el-tag type="success">{{ formData.publishUnit }} </el-tag>
+                </el-space>
+            </el-col>
+        </el-row>
+        <el-row>
+            <el-col :span=24>
+                <el-scrollbar ref="scrollbarRef" height="400px">
+                    <div v-html="formData.content"></div>
+                </el-scrollbar>
+            </el-col>
+        </el-row>
+    </el-dialog>
+</template>
+<script setup>
+import { ref, defineExpose } from 'vue';
+const dialogVisible = ref(false)
+
+const scrollTop=()=>{
+    scrollbarRef.wrap.scrollTop = 0
+}
+//
+const formData = ref({})
+//这里是重点
+defineExpose({
+    formData,
+    dialogVisible,
+    scrollTop
+})
+</script>

+ 68 - 0
frontend/src/components/jscodetpl.js

@@ -0,0 +1,68 @@
+//模板
+export const TemplateJsCode={
+    ListJsCode:`
+var ret = []
+document.querySelectorAll("{{.ListItemCss}}").forEach((v, i) => {
+    let item = {}
+    if ("{{.ListLinkCss}}" != "") {
+        let link = v.querySelector("{{.ListLinkCss}}")
+        if (link) {
+            var href = link.href
+            if (!href.startsWith("http")) href = window.location.origin + "/" + href
+            let title = link.getAttribute("title") || link.innerText
+            item = { "title": title, "href": href, "no": i }
+        } else {
+            item = { "no": i }
+        }
+    }
+    if ("{{.ListPubtimeCss}}" != "") {
+        let pubtime = v.querySelector("{{.ListPubtimeCss}}")
+        if (pubtime) {
+            item["pubtime"] = pubtime.innerText
+        }
+    }
+    ret.push(item)
+})
+ret
+    `,
+    ContentJsCode:`
+//执行JS代码
+var ret = {}
+var tmp = null
+
+if ("{{.TitleCss}}" != "") {//标题
+	tmp = document.querySelector("{{.TitleCss}}")
+	if (tmp) ret["title"] = tmp.getAttribute("title") || tmp.innerText
+}
+if ("{{.PublishUnitCss}}" != "") {//采购单位
+	tmp = document.querySelector("{{.PublishUnitCss}}")
+	if (tmp) ret["publishUnit"] = tmp.getAttribute("title") || tmp.innerText
+}
+if ("{{.PublishTimeCss}}" != "") {//发布时间
+	tmp = document.querySelector("{{.PublishTimeCss}}")
+	if (tmp) ret["publishTime"] = tmp.getAttribute("title") || tmp.innerText
+}
+if ("{{.ContentCss}}" != "") {//正文内容
+	tmp = document.querySelector("{{.ContentCss}}")
+	if (tmp) {
+		ret["content"] = tmp.innerText
+		ret["contentHtml"] = tmp.innerHTML
+	}
+}
+if("{{.AttachCss}}"!=""){//附件
+	tmp = document.querySelectorAll("{{.AttachCss}} a")
+	let attach=[]
+	if(tmp){
+		tmp.forEach((v,i)=>{
+			attach.push([v.getAttribute("title")||v.innerText,v.href])
+		})
+	}
+	ret["attachLinks"]=attach
+}
+ret
+    `,
+    AttachJsCode:`
+//附件下载以及提交
+    
+`
+}

+ 21 - 0
frontend/src/main.js

@@ -0,0 +1,21 @@
+import { createApp } from 'vue'
+import ElementPlus from 'element-plus'
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+import zhCn from 'element-plus/es/locale/lang/zh-cn'
+
+import 'element-plus/dist/index.css'
+import './style.css';
+import router from './router';
+import App from './App.vue'
+
+const app = createApp(App)
+
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+  app.component(key, component)
+}
+app.use(ElementPlus, {
+  locale: zhCn,
+})
+app.use(router);
+
+app.mount('#app')

+ 33 - 0
frontend/src/router/index.js

@@ -0,0 +1,33 @@
+// router/index.js
+import { createRouter, createWebHashHistory } from 'vue-router';
+import Home from "../views/Home.vue"
+import Run from "../views/Run.vue"
+import Setting from "../views/Setting.vue"
+
+
+const routes = [
+  {
+    path: '/',
+    name: 'Home',
+    component: Home
+  },
+  {
+    path: '/run',
+    name: 'run',
+    component: Run
+  },
+  {
+    path: '/setting',
+    name: 'setting',
+    component: Setting
+  },
+  // 更多路由...
+];
+
+const router = createRouter({
+  //history: createWebHistory(process.env.BASE_URL),
+  history: createWebHashHistory(),
+  routes
+});
+
+export default router;

+ 13 - 0
frontend/src/style.css

@@ -0,0 +1,13 @@
+body {
+    margin: 0;
+    background-color:#fff;
+}
+
+#app {
+    height: 100vh;
+    overflow: hidden;
+}
+
+.space{
+    height:8px;
+}

+ 340 - 0
frontend/src/views/Home.vue

@@ -0,0 +1,340 @@
+<template>
+
+    <Navigator pageTitle="首页"></Navigator>
+
+    <div class="space"></div>
+    <el-card>
+        <el-header style="text-align: center;">
+            <el-space>
+                <el-button-group>
+                    <el-button type="primary" @click="handleImport">
+                        <el-icon>
+                            <DocumentCopy />
+                        </el-icon>
+                        导入
+                    </el-button>
+
+                    <el-button type="primary" @click="handleAdd"><el-icon>
+                            <DocumentAdd />
+                        </el-icon>新增(^1)</el-button>
+
+                    <el-button type="primary" @click="handleDelete"><el-icon>
+                            <Delete />
+                        </el-icon>删除(^2)</el-button>
+                </el-button-group>
+                <el-button-group>
+                    <el-button type="primary" @click="handelDevelop">
+                        <el-icon>
+                            <SetUp />
+                        </el-icon>
+                        CSS嗅探(^3)
+                    </el-button>
+                    <el-button type="primary" @click="handleEdit"><el-icon>
+                            <Edit />
+                        </el-icon>开发(^4)</el-button>
+                    <el-button type="primary" @click="handleDebug"><el-icon>
+                            <Promotion />
+                        </el-icon>调试(^5)</el-button>
+                    <el-button type="primary" @click="handleSave"><el-icon>
+                            <UploadFilled />
+                        </el-icon>保存(^6)</el-button>
+                </el-button-group>
+            </el-space>
+        </el-header>
+        <el-main>
+            <el-table ref="spiderTable" :data="tableData" @selection-change="handleSelectionChange"
+                :row-style="getRowStyle">
+                <el-table-column type="selection" width="55"></el-table-column>
+                <el-table-column prop="code" label="代码" width="120" show-overflow-tooltip></el-table-column>
+                <el-table-column prop="site" label="网站" width="120" show-overflow-tooltip></el-table-column>
+                <el-table-column prop="channel" label="栏目" width="90" show-overflow-tooltip></el-table-column>
+                <el-table-column prop="url" label="栏目地址" width="90" show-overflow-tooltip></el-table-column>
+                <el-table-column prop="author" label="开发者" width="90" show-overflow-tooltip></el-table-column>
+                <el-table-column prop="listItemCss" label="列表CSS选择器" show-overflow-tooltip></el-table-column>
+                <el-table-column prop="titleCss" label="详情CSS选择器" show-overflow-tooltip></el-table-column>
+                <el-table-column align="right">
+                    <template #header>
+                        <el-input v-model="search" size="small" placeholder="按照代码过滤" />
+                    </template>
+                    <template #default="scope">
+                        <el-button size="small" @click="tableEvents.handleEdit(scope.$index, scope.row)">
+                            编辑
+                        </el-button>
+                        <el-button size="small" type="danger">
+                            删除
+                        </el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+            <div class="space"></div>
+            <el-pagination align="right" @size-change="handleSizeChange" @current-change="handleCurrentChange"
+                :current-page="currentPage" :page-sizes="[10, 20, 30, 40]" :page-size="pageSize"
+                layout="total, prev, pager, next" :total="tableData.length">
+            </el-pagination>
+        </el-main>
+    </el-card>
+    <InsertSpider ref="insertSpiderDialog" :onSubmit="onInsertSpiderOk"></InsertSpider>
+    <EditSpider ref="editSpiderDialog" @custom-event="dialogEvents.editSpiderConfigSaveEvent" />
+    <Login ref="loginDialog" @login-event="dialogEvents.loginEvent" />
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted } from 'vue'
+import { useRouter } from 'vue-router';
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { BrowserOpenURL, EventsOn } from "../../wailsjs/runtime"
+import { LoadSpiderConfigAll, SaveOrUpdateSpiderConfig } from "../../wailsjs/go/main/App"
+import { GetLoginState, PutLoginState } from "../../wailsjs/go/main/App"
+import { ImportSpiderConfigByExcelFile, SwitchSpiderConfig, DeleteSpiderConfig, SelectOpenFilePath } from "../../wailsjs/go/main/App"
+import Navigator from "../components/Navigator.vue"
+import InsertSpider from "../components/InsertSpider.vue"
+import EditSpider from "../components/EditSpider.vue"
+import Login from "../components/Login.vue"
+
+
+
+const router = useRouter();
+const spiderTable = ref(null)
+
+const tableData = ref([
+    {
+        "code": "001",
+        "site": "中国政府采购网",
+        "channel": "政务公开栏目",
+        'url': 'https://www.baidu.com',
+        'author': 'a7',
+        'listcss': 'list_item_css:""\nlist_link_css:""',
+        'contentcss': ''
+    },
+    // ...更多数据
+]);
+const insertSpiderDialog = ref(null)
+const editSpiderDialog = ref(null)
+const loginDialog = ref(null)
+
+const currentPage = ref(1);
+const pageSize = ref(10);
+const multipleSelection = ref([]);
+
+const dialogEvents = {
+    editSpiderConfigSaveEvent: function (data) {
+        console.log("change data:", data)
+        SaveOrUpdateSpiderConfig(data).then(result => {
+            ElMessage({
+                message: `成功更新爬虫 ${data.site} /${data.channel}/${data.code}`,
+                showClose: true,
+                duration: 3000,
+            });
+            //表格数据更新
+            tableData.value.forEach((v, i) => {
+                if (v.code == data.code) v = data
+            })
+            //更新当前选择
+            SwitchSpiderConfig(data.code).then(result => { })
+        })
+    },
+    loginEvent: function (data) {
+        PutLoginState(true).then(r => { })
+    }
+}
+
+const tableEvents = {
+    handleEdit: (index, v) => {
+        ElMessage({
+            message: `${v.site} ${v.channel} ${v.url}`,
+            showClose: true,
+            duration: 3000,
+        });
+        editSpiderDialog.value.dialogVisible = true
+        editSpiderDialog.value.formData = v
+    }
+}
+
+const getRowStyle = ({ row }) => {
+    return row.selected ? { backgroundColor: '#F7F7F7' } : {};
+};
+
+const handleSizeChange = (val) => {
+    pageSize.value = val;
+    currentPage.value = 1;
+};
+
+const handleCurrentChange = (val) => {
+    currentPage.value = val;
+};
+
+const handleSelectionChange = (val) => {
+    multipleSelection.value = val;
+    tableData.value.forEach((row) => {
+        row.selected = val.includes(row);
+    });
+    if (multipleSelection.value.length > 0) {
+        let v = multipleSelection.value[0]
+        SwitchSpiderConfig(v.code).then(result => { })
+    }
+};
+
+//删除爬虫
+const handleDelete = () => {
+    if (multipleSelection.value.length > 0) {
+        let v = multipleSelection.value[0]
+        DeleteSpiderConfig(v.code).then(result => {
+            loadTabelData()
+        })
+    } else {
+        ElMessage({
+            message: "先选择一个任务",
+            showClose: true,
+            duration: 3000,
+            type: 'error',
+        });
+    }
+};
+
+//
+const handelDevelop = () => {
+    if (multipleSelection.value.length > 0) {
+        let v = multipleSelection.value[0]
+        // 自定义关闭时间
+        ElMessage({
+            message: `${v.site} ${v.channel} ${v.url}`,
+            showClose: true,
+            duration: 3000,
+        });
+        BrowserOpenURL(v.url)
+    } else {
+        ElMessage({
+            message: "先选择一个任务",
+            showClose: true,
+            duration: 3000,
+            type: 'error',
+        });
+    }
+}
+
+//弹出新增加任务对话框
+const handleAdd = () => {
+    insertSpiderDialog.value.dialogVisible = true
+};
+//新增任务成功
+const onInsertSpiderOk = (data) => {
+    SaveOrUpdateSpiderConfig(data).then(result => {
+        loadTabelData()
+    })
+}
+
+//编辑任务
+const handleEdit = () => {
+    if (multipleSelection.value.length > 0) {
+        let v = multipleSelection.value[0]
+        // 自定义关闭时间
+        ElMessage({
+            message: `${v.site} ${v.channel} ${v.url}`,
+            showClose: true,
+            duration: 3000,
+        });
+        editSpiderDialog.value.dialogVisible = true
+        editSpiderDialog.value.formData = v
+    } else {
+        ElMessage({
+            message: "先选择一个任务",
+            showClose: true,
+            duration: 3000,
+            type: 'error',
+        });
+    }
+};
+
+const handleDebug = () => {
+    if (multipleSelection.value.length > 0) {
+        let v = multipleSelection.value[0]
+        router.push({
+            path: '/run'
+        });
+    } else {
+        ElMessage({
+            message: "先选择一个任务",
+            showClose: true,
+            duration: 3000,
+            type: 'error',
+        });
+    }
+}
+
+//异步调用
+const loadTabelData = () => {
+    LoadSpiderConfigAll(10, 10).then(result => {
+        tableData.value = result
+    })
+}
+loadTabelData()
+
+//Wails事件绑定
+EventsOn("spiderConfigChange", data => {
+    tableData.value.forEach((v, i) => {
+        if (v.code == data.code) {
+            let rowData = { ...data }
+            tableData.value[i] = rowData
+            spiderTable.value.toggleRowSelection(tableData.value[i], true);
+        }
+    })
+})
+
+//快捷键管理
+const handleShortcut = () => {
+    if (event.key === '1' && event.ctrlKey) {
+        handleAdd()
+    } else if (event.key === '2' && event.ctrlKey) {
+        handleDelete()
+    } else if (event.key === '3' && event.ctrlKey) {
+        handelDevelop()
+    }
+    else if (event.key === '4' && event.ctrlKey) {
+        handleEdit()
+    } else if (event.key === '5' && event.ctrlKey) {
+        handleDebug()
+    }
+}
+//批量导入爬虫设置
+const handleImport = () => {
+    SelectOpenFilePath().then(r => {
+        if (r === "") return
+        ImportSpiderConfigByExcelFile(r).then(d => {
+            ElMessage({
+                message: 'Excel爬虫任务集导入完成',
+                showClose: true,
+                duration: 3000,
+            });
+            loadTabelData()
+        })
+    })
+}
+const handleSave = () => {
+    ElMessage({
+        message: '该功能尚未开发完成',
+        showClose: true,
+        duration: 3000,
+    });
+}
+//快捷键绑定
+onMounted(() => {
+    window.addEventListener('keydown', handleShortcut);
+    GetLoginState().then(r => {
+        console.log("login state ", r)
+        if (loginDialog.value.dialogVisible) {
+            loginDialog.value.dialogVisible = !r
+        } else {
+            console.log("login dialog is not visible")
+        }
+    })
+
+})
+//快捷键解绑
+onUnmounted(() => {
+    window.removeEventListener('keydown', handleShortcut);
+})
+
+
+</script>
+
+<style scoped></style>

+ 188 - 0
frontend/src/views/Run.vue

@@ -0,0 +1,188 @@
+<template>
+    <Navigator pageTitle="运行/测试爬虫"></Navigator>
+    <div class="space"></div>
+    <div class="flex gap-2">
+        <el-space>
+            <el-tag type="primary"> {{ formData.code }}</el-tag>
+            <el-tag type="success"> {{ formData.site }}</el-tag>
+            <el-tag type="info"> {{ formData.channel }}</el-tag>
+            <el-tag type="warning">{{ formData.url }}</el-tag>
+            <el-tag type="primary"> {{ formData.author }}</el-tag>
+        </el-space>
+
+    </div>
+    <div class="space"></div>
+    <el-form ref="form" :model="formData" label-width="120px">
+
+        <el-form-item label="URL">
+            <el-input v-model="formData.url"></el-input>
+        </el-form-item>
+        <el-row>
+            <el-col :span="8"><el-form-item label="列表延时(MS)">
+                    <el-input v-model="formData.listDelay"></el-input>
+                </el-form-item></el-col>
+            <el-col :span="8"><el-form-item label="详情延时(MS)">
+                    <el-input v-model="formData.contentDelay"></el-input>
+                </el-form-item></el-col>
+            <el-col :span="8"><el-form-item label="代理地址">
+                    <el-input v-model="formData.proxyServe"></el-input>
+                </el-form-item></el-col>
+        </el-row>
+        <el-row>
+            <el-col :span="12"><el-form-item label="浏览器">
+                    <el-radio-group v-model="formData.headless">
+                        <el-radio value="true">无头</el-radio>
+                        <el-radio value="false">显式</el-radio>
+                    </el-radio-group> </el-form-item>
+            </el-col>
+            <el-col :span="12"><el-form-item label="显示图像">
+                    <el-radio-group v-model="formData.showImage">
+                        <el-radio value="true">显示</el-radio>
+                        <el-radio value="false">不显示</el-radio>
+                    </el-radio-group> </el-form-item>
+            </el-col>
+        </el-row>
+
+    </el-form>
+    <div style="text-align: center;">
+        <el-space>
+            <el-button type="primary" @click="handleDebug"><el-icon>
+                    <VideoPlay />
+                </el-icon>执行</el-button>
+            <el-button type="primary" @click="handleStop"><el-icon>
+                    <VideoPause />
+                </el-icon>终止</el-button>
+            <el-button type="primary" @click="handleRefersh"><el-icon>
+                    <Refresh />
+                </el-icon>刷新结果</el-button>
+            <el-button type="primary" @click="handleCountYestday"><el-icon>
+                    <Refresh />
+                </el-icon>统计昨日信息发布量</el-button>
+            <el-dropdown>
+                <el-button type="primary">
+                    结果导出<el-icon class="el-icon--right"><arrow-down /></el-icon>
+                </el-button>
+                <template #dropdown>
+                    <el-dropdown-menu>
+                        <el-dropdown-item @click="handleExportEpub">导出EPUB格式文件</el-dropdown-item>
+                        <el-dropdown-item>导出JSON格式文件</el-dropdown-item>
+                        <el-dropdown-item>导出Excel格式文件</el-dropdown-item>
+                        <el-dropdown-item>补录/上推至平台</el-dropdown-item>
+                    </el-dropdown-menu>
+                </template>
+            </el-dropdown></el-space>
+    </div>
+    <el-divider />
+    <div id="debugEventContian">执行日志:&nbsp;{{ debugLogLine }}</div>
+    <el-divider />
+    <el-table :data="tableData" style="width: 100%" height="240" @row-click="handleRowClick">
+        <el-table-column prop="no" label="序号" width="90" />
+        <el-table-column prop="title" label="标题" width="240" show-overflow-tooltip />
+        <el-table-column prop="href" label="链接" show-overflow-tooltip />
+        <el-table-column prop="contentShort" label="正文" show-overflow-tooltip />
+    </el-table>
+    <ViewArticle ref="articleDialog" />
+</template>
+<script setup>
+import { ref } from 'vue';
+import { ElMessage } from 'element-plus'
+import Navigator from "../components/Navigator.vue"
+import ViewArticle from "../components/ViewArticle.vue"
+
+import { ViewCurrentSpiderConfig, DebugSpider, StopDebugSpider } from "../../wailsjs/go/main/App"
+import { ViewResultItemAll, SelectSaveFilePath, ExportEpubFile, CountYestodayArts } from "../../wailsjs/go/main/App"
+import { EventsOn } from "../../wailsjs/runtime"
+const formData = ref({})
+const articleDialog = ref(null)
+
+const debugLogLine = ref("")
+const tableData = ref([])
+
+//开始调试
+const handleDebug = () => {
+    ElMessage({
+        message: `${[formData.value.url, formData.value.listDelay, formData.value.contentDelay,
+        formData.value.headless, formData.value.showImage, formData.value.proxyServe].join("//")}!`,
+        showClose: true,
+        duration: 3000,
+    });
+    DebugSpider(formData.value.url, parseInt(formData.value.listDelay), parseInt(formData.value.contentDelay),
+        formData.value.headless == 'true', formData.value.showImage == 'true', formData.value.proxyServe)
+}
+//停止调试
+const handleStop = () => {
+    StopDebugSpider()
+}
+//
+const truncateString = (str, maxLength) => {
+    return str.substring(0, maxLength) + "..";
+}
+//刷新加载数据
+const handleRefersh = () => {
+    ViewResultItemAll().then(result => {
+        result = result.slice(-20);
+        result.forEach((v, i) => {
+            v.contentShort = truncateString(v.content, 50)
+        })
+        tableData.value = result
+    })
+}
+//handleExportEpub导出文件
+const handleExportEpub = () => {
+    SelectSaveFilePath().then(r => {
+        if (r == "") return
+        ExportEpubFile(r).then(d => {
+            ElMessage({
+                message: `导出epub文件${r}完成!`,
+                showClose: true,
+                duration: 3000,
+            });
+        })
+    })
+}
+
+const replaceAll = function (src, search, replacement) {
+    return src.split(search).join(replacement);
+};
+//行点击事件
+const handleRowClick = (row, column, event) => {
+    articleDialog.value.dialogVisible = true
+    row.content = replaceAll(row.content, '\n', '<br/>')
+    articleDialog.value.formData = row
+    articleDialog.value.scrollTop()
+}
+//
+const handleCountYestday = () => {
+    if (formData.value.listNextPageCss != "" && formData.value.listPublishTimeCss != "") {
+        ElMessage({
+            message: `${[formData.value.url, formData.value.listDelay, formData.value.contentDelay,
+            formData.value.headless, formData.value.showImage, formData.value.proxyServe].join("//")}!`,
+            showClose: true,
+            duration: 3000,
+        });
+        CountYestodayArts(formData.value.url, parseInt(formData.value.listDelay), parseInt(formData.value.contentDelay),
+            formData.value.headless == 'true', formData.value.showImage == 'true')
+    } else {
+        ElMessage({
+            message: "当前爬虫设置,CSS选择器,不具备列表页发布时间+列表页翻页。",
+            type: 'error',
+            showClose: true,
+            duration: 3000,
+        });
+    }
+}
+//Wails事件绑定
+EventsOn("debug_event", data => {
+    debugLogLine.value = data
+})
+//加载当前爬虫配置
+ViewCurrentSpiderConfig().then(result => {
+    result['listDelay'] = 500
+    result['contentDelay'] = 500
+    result['proxyServe'] = ''
+    result['showImage'] = 'false'
+    result['headless'] = 'false'
+    formData.value = { ...result }
+})
+</script>
+<style></style>

+ 43 - 0
frontend/src/views/Setting.vue

@@ -0,0 +1,43 @@
+<template>
+    <Navigator pageTitle="系统设置"></Navigator>
+    <div class="space"></div>
+    <el-card>
+
+        <el-form :model="formData" label-width="auto">
+            <h3>大模型参数选择</h3>
+            <el-row>
+                <el-col :span="24">
+                    <el-form-item label="是否使用大模型">
+                        <el-switch v-model="formData.useBigModel" />
+                    </el-form-item>
+                </el-col>
+            </el-row>
+            <el-row>
+                <el-col :span="12">
+                    <el-form-item label="Api key">
+                        <el-input v-model="formData.mykey" />
+                    </el-form-item>
+                </el-col>
+                <el-col :span="12">
+                    <el-form-item label="模型选择">
+                        <el-select v-model="formData.modelName" placeholder="请选择要使用的模型">
+                            <el-option label="GLM-4-Flash(免费)" value="GLM-4-Flash" />
+                            <el-option label="GLM-4-Air" value="GLM-4-Air" />
+                            <el-option label="GLM-4-Long" value="GLM-4-Long" />
+                        </el-select>
+                    </el-form-item>
+                </el-col>
+            </el-row>
+        </el-form>
+
+
+    </el-card>
+</template>
+<script setup>
+import { ref } from 'vue';
+import Navigator from "../components/Navigator.vue"
+
+const formData = ref({
+    modelName: "",
+})
+</script>

+ 7 - 0
frontend/vite.config.js

@@ -0,0 +1,7 @@
+import {defineConfig} from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [vue()]
+})

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

@@ -0,0 +1,35 @@
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+import {main} from '../models';
+
+export function CountYestodayArts(arg1:string,arg2:number,arg3:number,arg4:boolean,arg5:boolean):Promise<void>;
+
+export function DebugSpider(arg1:string,arg2:number,arg3:number,arg4:boolean,arg5:boolean,arg6:string):Promise<void>;
+
+export function DeleteSpiderConfig(arg1:string):Promise<string>;
+
+export function ExportEpubFile(arg1:string):Promise<string>;
+
+export function GetLoginState():Promise<boolean>;
+
+export function Greet(arg1:string):Promise<string>;
+
+export function ImportSpiderConfigByExcelFile(arg1:string):Promise<string>;
+
+export function LoadSpiderConfigAll(arg1:number,arg2:number):Promise<Array<main.SpiderConfig>>;
+
+export function PutLoginState(arg1:boolean):Promise<string>;
+
+export function SaveOrUpdateSpiderConfig(arg1:main.SpiderConfig):Promise<string>;
+
+export function SelectOpenFilePath():Promise<string>;
+
+export function SelectSaveFilePath():Promise<string>;
+
+export function StopDebugSpider():Promise<string>;
+
+export function SwitchSpiderConfig(arg1:string):Promise<string>;
+
+export function ViewCurrentSpiderConfig():Promise<main.SpiderConfig>;
+
+export function ViewResultItemAll():Promise<main.ResultItems>;

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

@@ -0,0 +1,67 @@
+// @ts-check
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+export function CountYestodayArts(arg1, arg2, arg3, arg4, arg5) {
+  return window['go']['main']['App']['CountYestodayArts'](arg1, arg2, arg3, arg4, arg5);
+}
+
+export function DebugSpider(arg1, arg2, arg3, arg4, arg5, arg6) {
+  return window['go']['main']['App']['DebugSpider'](arg1, arg2, arg3, arg4, arg5, arg6);
+}
+
+export function DeleteSpiderConfig(arg1) {
+  return window['go']['main']['App']['DeleteSpiderConfig'](arg1);
+}
+
+export function ExportEpubFile(arg1) {
+  return window['go']['main']['App']['ExportEpubFile'](arg1);
+}
+
+export function GetLoginState() {
+  return window['go']['main']['App']['GetLoginState']();
+}
+
+export function Greet(arg1) {
+  return window['go']['main']['App']['Greet'](arg1);
+}
+
+export function ImportSpiderConfigByExcelFile(arg1) {
+  return window['go']['main']['App']['ImportSpiderConfigByExcelFile'](arg1);
+}
+
+export function LoadSpiderConfigAll(arg1, arg2) {
+  return window['go']['main']['App']['LoadSpiderConfigAll'](arg1, arg2);
+}
+
+export function PutLoginState(arg1) {
+  return window['go']['main']['App']['PutLoginState'](arg1);
+}
+
+export function SaveOrUpdateSpiderConfig(arg1) {
+  return window['go']['main']['App']['SaveOrUpdateSpiderConfig'](arg1);
+}
+
+export function SelectOpenFilePath() {
+  return window['go']['main']['App']['SelectOpenFilePath']();
+}
+
+export function SelectSaveFilePath() {
+  return window['go']['main']['App']['SelectSaveFilePath']();
+}
+
+export function StopDebugSpider() {
+  return window['go']['main']['App']['StopDebugSpider']();
+}
+
+export function SwitchSpiderConfig(arg1) {
+  return window['go']['main']['App']['SwitchSpiderConfig'](arg1);
+}
+
+export function ViewCurrentSpiderConfig() {
+  return window['go']['main']['App']['ViewCurrentSpiderConfig']();
+}
+
+export function ViewResultItemAll() {
+  return window['go']['main']['App']['ViewResultItemAll']();
+}

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

@@ -0,0 +1,113 @@
+export namespace main {
+	
+	export class AttachLink {
+	    title: string;
+	    href: string;
+	
+	    static createFrom(source: any = {}) {
+	        return new AttachLink(source);
+	    }
+	
+	    constructor(source: any = {}) {
+	        if ('string' === typeof source) source = JSON.parse(source);
+	        this.title = source["title"];
+	        this.href = source["href"];
+	    }
+	}
+	export class ResultItem {
+	    no: number;
+	    href: string;
+	    listTitle: string;
+	    listPubishTime: string;
+	    title: string;
+	    publishUnit: string;
+	    publishTime: string;
+	    content: string;
+	    contentHtml: string;
+	    attachLinks: AttachLink[];
+	    attachJson: string;
+	
+	    static createFrom(source: any = {}) {
+	        return new ResultItem(source);
+	    }
+	
+	    constructor(source: any = {}) {
+	        if ('string' === typeof source) source = JSON.parse(source);
+	        this.no = source["no"];
+	        this.href = source["href"];
+	        this.listTitle = source["listTitle"];
+	        this.listPubishTime = source["listPubishTime"];
+	        this.title = source["title"];
+	        this.publishUnit = source["publishUnit"];
+	        this.publishTime = source["publishTime"];
+	        this.content = source["content"];
+	        this.contentHtml = source["contentHtml"];
+	        this.attachLinks = this.convertValues(source["attachLinks"], AttachLink);
+	        this.attachJson = source["attachJson"];
+	    }
+	
+		convertValues(a: any, classs: any, asMap: boolean = false): any {
+		    if (!a) {
+		        return a;
+		    }
+		    if (a.slice && a.map) {
+		        return (a as any[]).map(elem => this.convertValues(elem, classs));
+		    } else if ("object" === typeof a) {
+		        if (asMap) {
+		            for (const key of Object.keys(a)) {
+		                a[key] = new classs(a[key]);
+		            }
+		            return a;
+		        }
+		        return new classs(a);
+		    }
+		    return a;
+		}
+	}
+	export class SpiderConfig {
+	    site: string;
+	    channel: string;
+	    author: string;
+	    url: string;
+	    code: string;
+	    listItemCss: string;
+	    listLinkCss: string;
+	    listPublishTimeCss: string;
+	    listNextPageCss: string;
+	    titleCss: string;
+	    publishUnitCss: string;
+	    publishTimeCss: string;
+	    contentCss: string;
+	    attachCss: string;
+	    listJs: string;
+	    contentJs: string;
+	    attachJs: string;
+	
+	    static createFrom(source: any = {}) {
+	        return new SpiderConfig(source);
+	    }
+	
+	    constructor(source: any = {}) {
+	        if ('string' === typeof source) source = JSON.parse(source);
+	        this.site = source["site"];
+	        this.channel = source["channel"];
+	        this.author = source["author"];
+	        this.url = source["url"];
+	        this.code = source["code"];
+	        this.listItemCss = source["listItemCss"];
+	        this.listLinkCss = source["listLinkCss"];
+	        this.listPublishTimeCss = source["listPublishTimeCss"];
+	        this.listNextPageCss = source["listNextPageCss"];
+	        this.titleCss = source["titleCss"];
+	        this.publishUnitCss = source["publishUnitCss"];
+	        this.publishTimeCss = source["publishTimeCss"];
+	        this.contentCss = source["contentCss"];
+	        this.attachCss = source["attachCss"];
+	        this.listJs = source["listJs"];
+	        this.contentJs = source["contentJs"];
+	        this.attachJs = source["attachJs"];
+	    }
+	}
+
+}
+

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

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

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

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

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

@@ -0,0 +1,238 @@
+/*
+ _       __      _ __
+| |     / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__  )
+|__/|__/\__,_/_/_/____/
+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 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);
+}

+ 62 - 0
go.mod

@@ -0,0 +1,62 @@
+module spidercreator
+
+go 1.21.5
+
+toolchain go1.22.4
+
+require (
+	github.com/bmaupin/go-epub v1.1.0
+	github.com/boltdb/bolt v1.3.1
+	github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476
+	github.com/chromedp/chromedp v0.10.0
+	github.com/itcwc/go-zhipu v0.0.0-20240626065325-ffc8bf1cfaaa
+	github.com/wailsapp/wails/v2 v2.9.1
+	github.com/xuri/excelize/v2 v2.8.1
+)
+
+require (
+	github.com/bep/debounce v1.2.1 // indirect
+	github.com/chromedp/sysutil v1.0.0 // indirect
+	github.com/gabriel-vasile/mimetype v1.3.1 // indirect
+	github.com/go-ole/go-ole v1.2.6 // indirect
+	github.com/gobwas/httphead v0.1.0 // indirect
+	github.com/gobwas/pool v0.2.1 // indirect
+	github.com/gobwas/ws v1.4.0 // indirect
+	github.com/godbus/dbus/v5 v5.1.0 // indirect
+	github.com/gofrs/uuid v3.1.0+incompatible // indirect
+	github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
+	github.com/google/uuid v1.3.0 // indirect
+	github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
+	github.com/josharian/intern v1.0.0 // indirect
+	github.com/labstack/echo/v4 v4.10.2 // indirect
+	github.com/labstack/gommon v0.4.0 // indirect
+	github.com/leaanthony/go-ansi-parser v1.6.0 // indirect
+	github.com/leaanthony/gosod v1.0.3 // indirect
+	github.com/leaanthony/slicer v1.6.0 // indirect
+	github.com/leaanthony/u v1.1.0 // indirect
+	github.com/mailru/easyjson v0.7.7 // indirect
+	github.com/mattn/go-colorable v0.1.13 // indirect
+	github.com/mattn/go-isatty v0.0.19 // indirect
+	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
+	github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/richardlehane/mscfb v1.0.4 // indirect
+	github.com/richardlehane/msoleps v1.0.3 // indirect
+	github.com/rivo/uniseg v0.4.4 // indirect
+	github.com/samber/lo v1.38.1 // indirect
+	github.com/tkrajina/go-reflector v0.5.6 // indirect
+	github.com/valyala/bytebufferpool v1.0.0 // indirect
+	github.com/valyala/fasttemplate v1.2.2 // indirect
+	github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50 // indirect
+	github.com/wailsapp/go-webview2 v1.0.10 // indirect
+	github.com/wailsapp/mimetype v1.4.1 // indirect
+	github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect
+	github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect
+	golang.org/x/crypto v0.23.0 // indirect
+	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
+	golang.org/x/net v0.25.0 // indirect
+	golang.org/x/sys v0.22.0 // indirect
+	golang.org/x/text v0.15.0 // indirect
+)
+
+// replace github.com/wailsapp/wails/v2 v2.9.1 => /Users/taozhang/go/pkg/mod

+ 144 - 0
go.sum

@@ -0,0 +1,144 @@
+github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
+github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
+github.com/bmaupin/go-epub v1.1.0 h1:XJyvvjchtUlbZ2P7eaEeB8EFw2NgVY5ycREFpmd6MKM=
+github.com/bmaupin/go-epub v1.1.0/go.mod h1:mBan+0WgVv5JbPNw1xfnfQoTRN9iPMKBshZwPOL0SY0=
+github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
+github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
+github.com/chromedp/cdproto v0.0.0-20240801214329-3f85d328b335/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
+github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 h1:VnjHsRXCRti7Av7E+j4DCha3kf68echfDzQ+wD11SBU=
+github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
+github.com/chromedp/chromedp v0.10.0 h1:bRclRYVpMm/UVD76+1HcRW9eV3l58rFfy7AdBvKab1E=
+github.com/chromedp/chromedp v0.10.0/go.mod h1:ei/1ncZIqXX1YnAYDkxhD4gzBgavMEUu7JCKvztdomE=
+github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
+github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+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/gabriel-vasile/mimetype v1.3.1 h1:qevA6c2MtE1RorlScnixeG0VA1H4xrXyhyX3oWBynNQ=
+github.com/gabriel-vasile/mimetype v1.3.1/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
+github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
+github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
+github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
+github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
+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/gofrs/uuid v3.1.0+incompatible h1:q2rtkjaKT4YEr6E1kamy0Ha4RtepWlQBedyHx0uzKwA=
+github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
+github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/itcwc/go-zhipu v0.0.0-20240626065325-ffc8bf1cfaaa h1:iOly0dSYv9AdoWfWt3uk4IF4O/nW+fyXV9rnC87UC7s=
+github.com/itcwc/go-zhipu v0.0.0-20240626065325-ffc8bf1cfaaa/go.mod h1:z7QZm7ol2nikFFGHwArJr1NTtBSE0M0g9MvHKxm1Sw0=
+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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
+github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
+github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
+github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
+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.0 h1:T8TuMhFB6TUMIUm0oRrSbgJudTFw9csT3ZK09w0t4Pg=
+github.com/leaanthony/go-ansi-parser v1.6.0/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
+github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ=
+github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4=
+github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
+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.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
+github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
+github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
+github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+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.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
+github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
+github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+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.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
+github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
+github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
+github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM=
+github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
+github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
+github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
+github.com/tkrajina/go-reflector v0.5.6/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.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+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/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50 h1:uxE3GYdXIOfhMv3unJKETJEhw78gvzuQqRX/rVirc2A=
+github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
+github.com/wailsapp/go-webview2 v1.0.10 h1:PP5Hug6pnQEAhfRzLCoOh2jJaPdrqeRgJKZhyYyDV/w=
+github.com/wailsapp/go-webview2 v1.0.10/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
+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.9.1 h1:irsXnoQrCpeKzKTYZ2SUVlRRyeMR6I0vCO9Q1cvlEdc=
+github.com/wailsapp/wails/v2 v2.9.1/go.mod h1:7maJV2h+Egl11Ak8QZN/jlGLj2wg05bsQS+ywJPT0gI=
+github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 h1:Chd9DkqERQQuHpXjR/HSV1jLZA6uaoiwwH3vSuF3IW0=
+github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
+github.com/xuri/excelize/v2 v2.8.1 h1:pZLMEwK8ep+CLIUWpWmvW8IWE/yxqG0I1xcN6cVMGuQ=
+github.com/xuri/excelize/v2 v2.8.1/go.mod h1:oli1E4C3Pa5RXg1TBXn4ENCXDV5JUMlBluUhG7c+CEE=
+github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4=
+github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
+golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
+golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
+golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
+golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+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-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+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.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 28 - 0
key.pem

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCuh3EWkkIlF7ta
+aB4E6EksqyWaAva8uEepvuA0TDB44fbVdGQ9DYpHaB1XIJ3nmiILPt2UCU2xuzT9
+iB1U19upwCKTOE5ZAxF7cfriZzR+YdFf77cFZNJROx793MDscw21WLiCTttxACCN
+w4fwRIU9bS+dMbWkuoTlA3mHFb+wUanXKTTGjVaQt9eejfZQs2FZRC/P1v5JvDN0
+EGum6TqOosh4+vFS1Y7HfIUBtfV3LaV58/0T32TjUmKjNOHxnXUXTbXMvk1AQv6n
+WMbUicCPZzzcfivQ8p8gVcENaaUWjzpfZjwMMscxaQgJK/pt2nRVjWg1bFiK0GmT
+Vs4fA1ofAgMBAAECggEACguJmr74R6JCCkYL1ER6UbPYCjE5eksw9Lgjt17bO1nm
+FwsH6euplcqMRcN+0yGv6+3GWwreCei4eA8pgQSbg/2m/8ox2DWw/+Xjhrxh7RQ8
+NMVbR1gyMrKwafQWtoU4uMNOe1GGl85mEUK7xDxtXse2AdomlkCV/YhhqkC6M6+n
+OpfYe9SEIeOwJjeiXlLg8tsqWDO9jdgjS7ZwnHqHeYyMorNCErUfpkgxpDEP8Dgt
+IpPnssiENr0bIpR43YO7KNE0WqhwUWIAVLXt9lMTfDayB80mezgeCLKQPP64eWeh
+evcfpzFsma90VTkHojQBbWpN/75QRrDEYzu/zwg4IQKBgQDMnqAUDS/BCXpalj/R
+vcE+WeoUmKEK+TqAbW9d3HahVusH8/eSwrDWUk7zafV3bHNFVW6Fer47vs0BIS1D
+orUygIc6GsDg/77crU5dio4N2F/Pmg+tLJHq31qWQAEZZFVGoaIvRYD2qCabUZPx
+dVIPK3ghaAzSE05v8v6JVYfUbQKBgQDaWohgRWDYJcsPOkVqez4y5Y3gWUZkI968
+zyFwaMZ8tSMbg6m7AJaUZFtz+dePDsrX8n/iGuEpMXZSmSG3sSPwS41k5poiBBdt
+Qy9r/Irp31SlA4FXy9Yq1SpXFSXvUZMSPdxOC03tGfOPa30Z2rH0rX6l81u5KhEa
+vwMXQxnZOwKBgEa5XSMRG7xhBkVhQVXBfJWMhnfv+VnNowbYzHFozigd3sa08JFt
+canicR95NDq+5WjFipngPvhvjnQhf3+tMWvvOM5AiQI740BrNnbmeQsYCqW63khA
+635/DNR58udP4pmzLFeiclzO6ektXTFMF7zejXsed6/0tFvFZW0afwRRAoGBAIU3
+ltyld2BoLmsr8g31Aw2qX9TworGV8N7gwFYElpSfLrwqp/MfeL8wO1uWopz1OWxm
+1v7rx1OKidX690dLG9IPRkS5LHB0bpaK1vPbMCVfzBSg/tjB0/ht9VcL4AkSi9gl
+RbOX0gNGQgLOYZTUiJ3u+8Xjo6Jkt+rJfulCVxLhAoGAZ1M20dYPI0edvYvdUOyL
+xDwmdlkOO2DU4+od7hhV9vdvvshbRC88m3YgjcGx59oFie++ynwf8c+LHi4Qx2Tw
+J/NzmWoOlC4Q2FljTGrNmbxAtVzkXOfpKoL9+YETZzHJEBLYdmETLfXLQK4OaXQz
+AxverbaEA3UNOAQPte5r7pI=
+-----END PRIVATE KEY-----

+ 43 - 0
main.go

@@ -0,0 +1,43 @@
+package main
+
+import (
+	"embed"
+
+	"github.com/wailsapp/wails/v2"
+	"github.com/wailsapp/wails/v2/pkg/options"
+	"github.com/wailsapp/wails/v2/pkg/options/assetserver"
+)
+
+var (
+	//go:embed all:frontend/dist
+	assets embed.FS
+	app    *App
+)
+
+func main() {
+	//
+	go runHttpServe()
+
+	// Create an instance of the app structure
+	app = NewApp()
+
+	// Create application with options
+	err := wails.Run(&options.App{
+		Title:  "剑鱼-爬虫开发平台 v1.0",
+		Width:  1224,
+		Height: 668,
+		AssetServer: &assetserver.Options{
+			Assets: assets,
+		},
+		BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 1},
+		OnStartup:        app.startup,
+		OnShutdown:       app.destory,
+		Bind: []interface{}{
+			app,
+		},
+	})
+
+	if err != nil {
+		println("Error:", err.Error())
+	}
+}

+ 117 - 0
service.go

@@ -0,0 +1,117 @@
+// 对外服务
+package main
+
+import (
+	"crypto/tls"
+	_ "embed"
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+
+	"github.com/wailsapp/wails/v2/pkg/runtime"
+)
+
+const (
+	LISTEN_ADDR = ":8080"
+)
+
+type (
+	SpiderConfigItem struct {
+		Key string `json:"key"`
+		Css string `json:"css"`
+	}
+)
+
+var (
+	//go:embed cert.pem
+	certBytes []byte
+	//go:embed key.pem
+	keyBytes []byte
+)
+
+func runHttpServe() {
+	// 设置HTTP服务器
+	mux := http.NewServeMux()
+	// 解析证书
+	cert, err := tls.X509KeyPair(certBytes, keyBytes)
+	if err != nil {
+		log.Println(err.Error())
+		return
+	}
+	// 创建一个TLS配置
+	tlsConfig := &tls.Config{
+		// 可以在这里添加其他TLS配置
+		Certificates:       []tls.Certificate{cert},
+		ServerName:         "localhost",
+		InsecureSkipVerify: true,
+	}
+	server := &http.Server{
+		Addr:      LISTEN_ADDR,
+		Handler:   mux,
+		TLSConfig: tlsConfig,
+	}
+	//这里注册HTTP服务
+	mux.HandleFunc("/save", SaveSpiderConfig)
+	mux.HandleFunc("/load", LoadSpiderConfig)
+	//
+	log.Println("Starting HTTPS server on ", LISTEN_ADDR)
+	err = server.ListenAndServeTLS("", "")
+	if err != nil {
+		log.Println("Failed to start server:  ", err.Error())
+		return
+	}
+}
+
+// LoadCurrentSpiderConfig,json处理
+func SaveSpiderConfig(w http.ResponseWriter, r *http.Request) {
+	log.Println("保存设置")
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+	w.Header().Set("Content-Type", "application/json")
+	var req = new(SpiderConfigItem)
+	err := json.NewDecoder(r.Body).Decode(req)
+	if err != nil {
+		log.Println("序列化失败")
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	log.Println("CSS", req.Key, req.Css)
+	//TODO 业务操作
+	switch req.Key {
+	case "listItemCss":
+		currentSpiderConfig.ListItemCss = req.Css
+	case "listLinkCss":
+		currentSpiderConfig.ListLinkCss = req.Css
+	case "listPublishTimeCss":
+		currentSpiderConfig.ListPubtimeCss = req.Css
+	case "listNextPageCss":
+		currentSpiderConfig.ListNextPageCss = req.Css
+	case "titleCss":
+		currentSpiderConfig.TitleCss = req.Css
+	case "publishUnitCss":
+		currentSpiderConfig.PublishUnitCss = req.Css
+	case "publishTimeCss":
+		currentSpiderConfig.PublishTimeCss = req.Css
+	case "contentCss":
+		currentSpiderConfig.ContentCss = req.Css
+	case "attachCss":
+		currentSpiderConfig.AttachCss = req.Css
+	}
+	fmt.Fprint(w, "{'code':200}")
+	db.SaveOrUpdate(currentSpiderConfig)
+	//TODO 通知开发工具端,CSS选择器有变动
+	runtime.EventsEmit(app.ctx, "spiderConfigChange", currentSpiderConfig)
+}
+
+// LoadCurrentSpiderConfig,加载,返回当前配置项
+func LoadSpiderConfig(w http.ResponseWriter, r *http.Request) {
+	log.Println("加载当前配置项")
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+	w.Header().Set("Content-Type", "application/json")
+	err := json.NewEncoder(w).Encode(currentSpiderConfig)
+	if err != nil {
+		log.Println("反向序列化失败")
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+}

+ 34 - 0
tpl/load_content.js

@@ -0,0 +1,34 @@
+//执行JS代码
+var ret = {}
+var tmp = null
+
+if ("{{.TitleCss}}" != "") {//标题
+	tmp = document.querySelector("{{.TitleCss}}")
+	if (tmp) ret["title"] = tmp.getAttribute("title") || tmp.innerText
+}
+if ("{{.PublishUnitCss}}" != "") {//采购单位
+	tmp = document.querySelector("{{.PublishUnitCss}}")
+	if (tmp) ret["publishUnit"] = tmp.getAttribute("title") || tmp.innerText
+}
+if ("{{.PublishTimeCss}}" != "") {//发布时间
+	tmp = document.querySelector("{{.PublishTimeCss}}")
+	if (tmp) ret["publishTime"] = tmp.getAttribute("title") || tmp.innerText
+}
+if ("{{.ContentCss}}" != "") {//正文内容
+	tmp = document.querySelector("{{.ContentCss}}")
+	if (tmp) {
+		ret["content"] = tmp.innerText
+		ret["contentHtml"] = tmp.innerHTML
+	}
+}
+if("{{.AttachCss}}"!=""){//附件
+	tmp = document.querySelectorAll("{{.AttachCss}} a")
+	let attach=[]
+	if(tmp){
+		tmp.forEach((v,i)=>{
+			attach.push([v.getAttribute("title")||v.innerText,v.href])
+		})
+	}
+	ret["attachLinks"]=attach
+}
+ret

+ 23 - 0
tpl/load_list_items.js

@@ -0,0 +1,23 @@
+var ret = []
+document.querySelectorAll("{{.ListItemCss}}").forEach((v, i) => {
+    let item = {}
+    if ("{{.ListLinkCss}}" != "") {
+        let link = v.querySelector("{{.ListLinkCss}}")
+        if (link) {
+            var href = link.href
+            if (!href.startsWith("http")) href = window.location.origin + "/" + href
+            let title = link.getAttribute("title") || link.innerText
+            item = { "listTitle": title, "href": href, "no": i }
+        } else {
+            item = { "no": i }
+        }
+    }
+    if ("{{.ListPubtimeCss}}" != "") {
+        let pubtime = v.querySelector("{{.ListPubtimeCss}}")
+        if (pubtime) {
+            item["listPubishTime"] = pubtime.innerText
+        }
+    }
+    ret.push(item)
+})
+ret

+ 56 - 0
types.go

@@ -0,0 +1,56 @@
+package main
+
+type (
+	//爬虫配置信息
+	SpiderConfig struct {
+		Site            string `json:"site"`
+		Channel         string `json:"channel"`
+		Author          string `json:"author"`
+		Url             string `json:"url"`
+		Code            string `json:"code"`
+		ListItemCss     string `json:"listItemCss"`
+		ListLinkCss     string `json:"listLinkCss"`
+		ListPubtimeCss  string `json:"listPublishTimeCss"`
+		ListNextPageCss string `json:"listNextPageCss"`
+		TitleCss        string `json:"titleCss"`
+		PublishUnitCss  string `json:"publishUnitCss"`
+		PublishTimeCss  string `json:"publishTimeCss"`
+		ContentCss      string `json:"contentCss"`
+		AttachCss       string `json:"attachCss"`
+		ListJSCode      string `json:"listJs"`
+		ContentJSCode   string `json:"contentJs"`
+		AttachJSCode    string `json:"attachJs"`
+	}
+	//附件链接
+	AttachLink struct {
+		Title string `json:"title"`
+		Href  string `json:"href"`
+	}
+	//爬取结果信息
+	ResultItem struct {
+		No          int           `json:"no"`
+		Href        string        `json:"href"`
+		ListTitle   string        `json:"listTitle"`
+		ListPubTime string        `json:"listPubishTime"`
+		Title       string        `json:"title"`
+		PublishUnit string        `json:"publishUnit"`
+		PublishTime string        `json:"publishTime"`
+		Content     string        `json:"content"`
+		ContentHtml string        `json:"contentHtml"`
+		AttachLinks []*AttachLink `json:"attachLinks"` //存放附件的标题,链接
+		AttachJson  string        `json:"attachJson"`  //存放附件的OSS元信息
+	}
+
+	ResultItems    []*ResultItem
+	SpiderConfiges []*SpiderConfig
+)
+
+func (sc SpiderConfiges) Len() int {
+	return len(sc)
+}
+func (sc SpiderConfiges) Swap(i, j int) {
+	sc[i], sc[j] = sc[j], sc[i]
+}
+func (sc SpiderConfiges) Less(i, j int) bool {
+	return sc[i].Code > sc[j].Code
+}

+ 218 - 0
vm.go

@@ -0,0 +1,218 @@
+package main
+
+import (
+	"bytes"
+	_ "embed"
+	"fmt"
+	"log"
+	"os"
+	"strconv"
+	"strings"
+	"text/template"
+	"time"
+
+	"github.com/bmaupin/go-epub"
+	"github.com/chromedp/chromedp"
+)
+
+const (
+	MAX_TRUN_PAGE = 1000
+)
+
+var (
+	//go:embed tpl/load_list_items.js
+	loadListItemsJS string
+	//go:embed tpl/load_content.js
+	loadContentJS string
+
+	currentResult = make(ResultItems, 0)
+)
+
+// renderJavascriptCoder
+func renderJavascriptCoder(tpl string, sc *SpiderConfig) string {
+	t, err := template.New("").Parse(tpl)
+	if err != nil {
+		log.Println("创建JS代码模板失败", err.Error())
+		return ""
+	}
+	buf := new(bytes.Buffer)
+	err = t.Execute(buf, sc)
+	if err != nil {
+		log.Println("执行JS代码模板失败", err.Error())
+		return ""
+	}
+	return buf.String()
+}
+
+// RunSpider
+func RunSpider(url string, listDealy int64, contentDelay int64, headless bool, showImage bool, proxyServe string, exit chan bool) {
+	sc := MergeSpiderConfig(currentSpiderConfig, &SpiderConfig{Url: url})
+	_, baseCancel, _, _, ctx, cancel := NewBrowser(headless, showImage, proxyServe)
+	log.Println("1浏览器打开")
+	app.pushMessage("debug_event", "1 浏览器打开")
+	defer func() {
+		cancel()
+		baseCancel()
+		log.Println("0浏览器已经销毁")
+		app.pushMessage("debug_event", "0 浏览器已经销毁")
+		close(exit)
+	}()
+	currentResult = make(ResultItems, 0, 0)
+	chromedp.Run(ctx, chromedp.Tasks{
+		chromedp.Navigate(sc.Url),
+		chromedp.WaitReady("document.body", chromedp.ByJSPath),
+		chromedp.Sleep(time.Duration(listDealy) * time.Millisecond),
+	})
+	app.pushMessage("debug_event", "2 页面已经打开")
+	log.Println("2页面打开")
+	listResult := make(ResultItems, 0)
+	//TODO 2. 执行JS代码,获取列表页信息
+	runJs := renderJavascriptCoder(loadListItemsJS, sc)
+	err := chromedp.Run(ctx, chromedp.Tasks{
+		chromedp.Evaluate(runJs, &listResult),
+	})
+	if err != nil {
+		log.Println("执行JS代码失败", err.Error())
+		app.pushMessage("debug_event", "2 执行JS代码失败")
+		return
+	}
+	app.pushMessage("debug_event", "3 获取列表完成")
+	log.Println("3获取列表完成")
+
+	//TODO 3. 打开详情页 ,最多打开10条
+	runJs = renderJavascriptCoder(loadContentJS, sc)
+	for _, v := range listResult {
+		select {
+		case <-exit:
+			return
+		default:
+			app.pushMessage("debug_event", fmt.Sprintf("4. %d- 待 下载详情页 %s ", v.No, v.Title))
+			var result string = ""
+			err = chromedp.Run(ctx, chromedp.Tasks{
+				chromedp.Navigate(v.Href),
+				chromedp.WaitReady(`document.body`, chromedp.ByJSPath),
+				chromedp.Sleep(time.Duration(contentDelay) * time.Millisecond),
+				chromedp.Evaluate(runJs, v),
+			})
+			if err != nil {
+				log.Println("执行JS代码失败", err.Error())
+			}
+			//关闭当前TAB页
+			chromedp.Run(ctx, chromedp.Tasks{
+				chromedp.Evaluate(`var ret="";window.close();ret`, &result),
+			})
+			app.pushMessage("debug_event", fmt.Sprintf("4. %d- 下载详情页 %s 完成", v.No, v.Title))
+			currentResult = append(currentResult, v)
+		}
+	}
+	app.pushMessage("debug_event", "5 采集测试完成")
+	log.Println("5采集测试完成")
+}
+
+// ExportEpubFile 导出epub文件
+func ExportEpubFile(filepath string) {
+	output := epub.NewEpub("")
+	output.SetTitle(currentSpiderConfig.Site)
+	output.SetAuthor("unknow")
+	for i, art := range currentResult {
+		body := "<h2>" + art.Title + "</h2><p>" + strings.Join(strings.Split(art.Content, "\n"), "</p><p>") + "</p>"
+		output.AddSection(body, art.Title, fmt.Sprintf("%06d.xhtml", i+1), "")
+	}
+	fo, err := os.Create(filepath)
+	if err != nil {
+		app.pushMessage("debug_event", err.Error())
+	}
+	output.WriteTo(fo)
+	fo.Close()
+}
+
+// CountYestodayArts 统计昨日信息发布量
+func CountYestodayArts(url string, listDealy int64, trunPageDelay int64,
+	headless bool, showImage bool, exit chan bool) (count int) {
+	sc := MergeSpiderConfig(currentSpiderConfig, &SpiderConfig{Url: url})
+	_, baseCancel, _, _, ctx, cancel := NewBrowser(headless, showImage, "")
+	log.Println("1浏览器打开")
+	app.pushMessage("debug_event", "1 浏览器打开")
+	defer func() {
+		cancel()
+		baseCancel()
+		log.Println("0浏览器已经销毁")
+		app.pushMessage("debug_event", "0 浏览器已经销毁")
+		app.pushMessage("debug_event", fmt.Sprintf("99 昨日信息发布量:%d ", count))
+		close(exit)
+	}()
+
+	//时间比较
+	now := time.Now()
+	yesterday := now.AddDate(0, 0, -1) // 获取昨天的日期
+	startOfYesterday := time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 0, 0, 0, 0, now.Location())
+	endOfYesterday := startOfYesterday.AddDate(0, 0, 1).Add(-time.Nanosecond)
+
+	//TODO 1.
+	chromedp.Run(ctx, chromedp.Tasks{
+		chromedp.Navigate(sc.Url),
+		chromedp.WaitReady("document.body", chromedp.ByJSPath),
+		chromedp.Sleep(time.Duration(listDealy) * time.Millisecond),
+	})
+	app.pushMessage("debug_event", "2 页面已经打开")
+	log.Println("2页面打开")
+	//TODO 2. 执行JS代码,获取列表页信息
+	runJs := renderJavascriptCoder(loadListItemsJS, sc)
+	tmp := map[string]bool{}
+	//最多翻页1000页
+	for i := 0; i < MAX_TRUN_PAGE; i++ {
+		select {
+		case <-exit:
+			return
+		default:
+			app.pushMessage("debug_event", "3 执行列表页JS")
+			listResult := make(ResultItems, 0)
+			err := chromedp.Run(ctx, chromedp.Tasks{
+				chromedp.Evaluate(runJs, &listResult),
+			})
+			if err != nil {
+				log.Println("执行JS代码失败", err.Error())
+				app.pushMessage("debug_event", "3 执行JS代码失败")
+				return
+			}
+			//TODO 人工智能转换采集到的日期
+			callAIState := false
+			for j := 0; j < 5; j++ {
+				app.pushMessage("debug_event", "3 执行AI提取列表发布时间"+strconv.Itoa(j+1))
+				err := UpdateResultDateStr(listResult)
+				if err == nil {
+					callAIState = true
+					break
+				}
+			}
+			if !callAIState {
+				app.pushMessage("debug_event", "3 多轮次调用AI均未得到合理结果")
+				return
+			}
+			//TODO 日期统计
+			for _, r := range listResult {
+				day, err := time.Parse("2006-01-02", r.ListPubTime)
+				if err != nil {
+					continue
+				}
+				if _, ok := tmp[r.Href]; ok { //去重
+					continue
+				}
+				if day.After(startOfYesterday) && day.Before(endOfYesterday) {
+					count += 1
+				} else if day.Before(startOfYesterday) {
+					return
+				}
+			}
+			app.pushMessage("debug_event", fmt.Sprintf("4 当前观测昨日信息发布量:%d ", count))
+			//TODO 翻页
+			//fmt.Println("下一页CSS选择器", currentSpiderConfig.ListNextPageCss)
+			chromedp.Run(ctx, chromedp.Tasks{
+				chromedp.Click(fmt.Sprintf(`document.querySelector("%s")`, currentSpiderConfig.ListNextPageCss),
+					chromedp.ByJSPath),
+				chromedp.Sleep(time.Duration(trunPageDelay) * time.Millisecond),
+			})
+		}
+	}
+	return
+}

+ 13 - 0
wails.json

@@ -0,0 +1,13 @@
+{
+  "$schema": "https://wails.io/schemas/config.v2.json",
+  "name": "爬虫开发平台",
+  "outputfilename": "爬虫开发平台",
+  "frontend:install": "npm install",
+  "frontend:build": "npm run build",
+  "frontend:dev:watcher": "npm run dev",
+  "frontend:dev:serverUrl": "auto",
+  "author": {
+    "name": "xiaoa7",
+    "email": "394922814@qq.com"
+  }
+}