WailsStudy

Wails

Wails中文文档

安装

需求:Go1.18+、npm

1
go install github.com/wailsapp/wails/v2/cmd/wails@latest

创建项目

创建项目 | WailsVue之外前端框架

1
wails init -n testDemo -t vue	#使用 JavaScript 生成一个名为 todolist的 Vue项目

此时得到了一个这样的结构

此时它已经可以跑了 第一次跑会慢一点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
todoList
      .gitignore
      app.go					# 把方法传递前端被js转译
      go.mod
      go.sum
      main.go					# 程序主入口 配置一些窗口配置 以及app注册
      README.md
      wails.json
    
    ├─build {...}				# 编译后文件
    
    └─frontend					# 前端项目 这里是vue项目 除了下面的wailsjs外基本和vue项目没有区别
          index.html
          package.json
          README.md
          vite.config.js
        
        ├─dist
              gitkeep
        
        ├─src
            App.vue
            main.js
            style.css
          
          ├─assets
            ├─fonts
                  nunito-v16-latin-regular.woff2
                  OFL.txt
            
            └─images
                    logo-universal.png
          
          └─components
                  HelloWorld.vue
        
        └─wailsjs				# 被转译的go方法以及runtime包
            ├─go
              └─main
                      App.d.ts
                      App.js
            
            └─runtime
                    package.json
                    runtime.d.ts
                    runtime.js

其中比较重要的文件为 ./main.go ./app.go以及 ./frontend/src/目录下的前端项目

wails的具体的方法Wails中文文档已经讲的十分清楚且明了 故接下来只解释我所写的那些东西

两个示例demo

demo1-todoList

demo2-chatGPT

demo1-todoList

最终成果

项目地址:mhqdz/todolist-WailsStudy (github.com)

1419a8a6e659b3373e2cf3aa4b40a97

文件解释

./main.go

程序入口 配置一些窗口设置 以及 App结构体(能被前端看到的结构体)的注册

实际上可以把type App struct直接写在main里 都是同一个包 不过这种写法比较清晰

init出来的main是这样的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
       "embed"

       "github.com/wailsapp/wails/v2"
       "github.com/wailsapp/wails/v2/pkg/options"
       "github.com/wailsapp/wails/v2/pkg/options/assetserver"
)

//go:embed all:frontend/dist
var assets embed.FS

func main() {
       // Create an instance of the app structure
       app := NewApp()

       // Create application with options
       err := wails.Run(&options.App{
               Title:  "todoList",
               Width:  1024,
               Height: 768,
               AssetServer: &assetserver.Options{
                       Assets: assets,
               },
               BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
               OnStartup:        app.startup,
               Bind: []interface{}{
                       app,
               },
       })

       if err != nil {
               println("Error:", err.Error())
       }
}

最终完成的main也没有太大的差距 仅仅是增加了一点配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import (
   "embed"

   "github.com/wailsapp/wails/v2"
   "github.com/wailsapp/wails/v2/pkg/options"
   "github.com/wailsapp/wails/v2/pkg/options/assetserver"
   "github.com/wailsapp/wails/v2/pkg/options/windows"
)

// go注解 读取 frontend/dist 的所有内容
//
//go:embed all:frontend/dist
var assets embed.FS

// main 程序入口 调用了wails.Run() 定义了一些窗口选项 并指定了那些App可以暴露
func main() {
   // Create an instance of the app structure
   app := NewApp()

   // wails.Run()
   // Create application with options
   err := wails.Run(&options.App{
   	Title:         "todoList",
   	Width:         400,
   	Height:        600,
   	MaxHeight:     3000,
   	DisableResize: true,
   	// 无窗口 关闭
   	Frameless: true,
   	Windows: &windows.Options{
   		WebviewIsTransparent: true,
   		WindowIsTranslucent:  true,
   		// BackdropType: windows.Acrylic,
   	},
   	AssetServer: &assetserver.Options{
   		Assets: assets,
   	},
   	BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 0},
   	OnStartup:        app.startup,

   	// Bind 方法绑定 它会检查 Bind 字段中列出的结构体实例
   	// 确定哪些函数是公开的(公开以大写字母开头的函数)
   	// 并生成前端可以调用的这些方法的 JavaScript 版本
   	Bind: []interface{}{
   		app,
   	},
   })

   if err != nil {
   	println("Error:", err.Error())
   }
}

./app.go

提供给前端用的方法都在这里声明

init后原本的app.go

原本的app 除了NewApp(构造函数),startup(init函数)外仅提供了一个Greet函数 该函数的作用是前端传入一个name后 把name拼接成Hello %name%, It's show time!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main
import (
        "context"
        "fmt"
)

// 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
}

// 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)
}

最终完成时的app.go

提供了相当多的方法给前端 实际上这些大多都能在前端完成 不过不太擅长前端所以写在后端了

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
package main

import (
	"bufio"
	"context"
	"io"
	"math/rand"
	"os"
	"os/user"
	"time"

	"github.com/wailsapp/wails/v2/pkg/runtime"
)

// ctx 上下文
// Msgs 存储todos的数组
// say 存储
// App struct
type App struct {
	ctx  context.Context
	Msgs []string
	say  string
}

// NewApp App struct的构造函数
func NewApp() *App {
	app := &App{}
	app.Read()
	return app
}

// KillSelf 关闭程序
func (a *App) KillSelf() {
	os.Exit(0)
}

// MinWindow 窗口最小化
func (a *App) MinWindow() {
	runtime.WindowMinimise(a.ctx)
}

// 下边的那一句话
// RedomSay 随机获取一个合适的语句(say) 写入到app.say是为了防止随机到同一句话
func (a *App) RedomSay() string {
	says := []string{"如我所愿,一切皆好", "不可预见的未来是诗 写在纸上的未来是理想"}

	now := time.Now()
	mounth := now.Month()
	hour := now.Hour()

	// 春
	if mounth == 3 || mounth == 4 || mounth == 5 {
		says = append(says, "正人间四月 何不绽放如花")
		says = append(says, "山中何事?松花酿酒,春水煎茶")
	}

	// 夏
	if mounth == 6 || mounth == 7 || mounth == 8 {
		says = append(says, "生如夏花之绚烂")
		says = append(says, "今天会下雨吗")
	}

	// 秋
	if mounth == 9 || mounth == 10 || mounth == 11 {
		says = append(says, "空山新雨后,天气晚来秋")
	}

	// 冬
	if mounth == 12 || mounth == 1 || mounth == 2 {
		if (hour > 20 && hour <= 23) || (hour > 0 && hour < 6) {
			says = append(says, "秋月扬明晖,冬岭秀寒松")
		}
	}

	// 凌晨
	if hour >= 0 && hour < 6 {
		says = append(says, "永恒的月光和永动的齿轮 那是永远的良药")
		says = append(says, "在白天的残骸上 人们徒然睁着双眼")
		says = append(says, "白昼之光,岂知夜之深")
	}

	// 早晨
	if hour > 5 && hour < 8 {
		says = append(says, "洗漱之后 开始今天的安排吧")
		says = append(says, "日出雾露馀,青松如膏沐")
	}

	// 上午
	if hour > 7 && hour < 11 {
		says = append(says, "梦想和苦恼都已过去 这是新的一天")
	}

	// 中午
	if hour > 11 && hour < 14 {
		says = append(says, "午睡或者看会书 无论如何 这暧昧的时间属于你")
	}

	// 下午
	if hour > 13 && hour < 17 {
		says = append(says, "久坐是反对神圣精神的罪恶 感到烦闷不妨出去走走")
		says = append(says, "惟其不可能,才值得相信")
		if hour == 14 {
			says = append(says, "来杯下午茶吧")
		}
	}

	// 晚饭
	if hour > 16 && hour < 21 {
		says = append(says, "不要忘了晚饭哦")
		says = append(says, "看到密涅瓦的猫头鹰了吗")
	}

	// 晚上
	if hour > 20 && hour <= 23 {
		says = append(says, "保持清洁 洗漱是必要的")
		says = append(says, "给今天的工作收个尾吧")
	}

	sayLen := len(says)

	if sayLen == 0 {
		a.say = "如我所愿,一切皆好"
		return a.say
	}

	if sayLen == 1 {
		a.say = says[0]
		return a.say
	}

	var i int32
	for {
		i2 := rand.Int31n(int32(sayLen))
		if says[i2] != a.say {
			i = i2
			break
		}
	}

	a.say = says[i]
	return a.say
}

// startup 相当于init函数 在启动时执行
// 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
	// WindowSetPosition 设置启动位置 而不是总在屏幕中间
	// 可以在上面任意会被执行的函数里插入一个 fmt.Println(runtime.WindowGetPosition(a.ctx)) 用wails dev运行 来打印当前坐标 选择合适的坐标 然后在这里设置启动时就到那个位置
	// 当然如果您愿意的话 可以用某个文件 记录下关闭时的位置 然后读取 改用那个位置
	// runtime.WindowSetPosition(ctx, -1051, 456)
}

// SetH 设置窗口高度为h h具体的值在前端计算获得
func (a *App) SetH(h int) {
	// fmt.Println(runtime.WindowGetSize(a.ctx))
	// fmt.Println(h)
	runtime.WindowSetSize(a.ctx, 400, h)
}

// Add 添加一个安排
func (a *App) Add(todo string) {
	if todo == "" {
		return
	}
	a.Read()
	a.Msgs = append(a.Msgs, todo)
	a.Write()
	// fmt.Println(runtime.WindowGetPosition(a.ctx))
	// fmt.Println(a.Msgs)
}

// GetLen 获取当前
func (a *App) GetLen() int {
	return len(a.Msgs)
}

// GetMsgs 返回存有 todos的数组
func (a *App) GetMsgs() []string {
	a.Read()
	return a.Msgs
}

// Write 把a.msgs写入到用户目录下的mhtodolist
func (a *App) Write() {
	user, err := user.Current()
	if err != nil {
		panic(err)
	}
	file, err := os.OpenFile(user.HomeDir+"\\mhtodolist", os.O_WRONLY|os.O_CREATE, 0666)

	// 在写入前把文件清空 避免源文件比写入得到文件大的情况
	file.Truncate(0)

	// file, err := os.OpenFile("/static/mhtodolist",os.O_WRONLY|os.O_CREATE,0666)
	if err != nil {
		panic(err)
	}
	defer file.Close()
	writer := bufio.NewWriter(file)
	for _, j := range a.Msgs {
		if j[len(j)-1] == '\n' {
			writer.WriteString(j)
			continue
		}
		writer.WriteString(j + "\n")
	}
	writer.Flush()
}

// 没有用上 毕竟上面的GetMsgs已经把整个数组都拿去了 没必要来后端拿
// GetMsg 根据index获取一条 todo
func (a *App) GetMsg(index int) string {
	return a.Msgs[index]
}

// Del 根据index删除一个todo
func (a *App) Del(index int) {
	a.Msgs = append(a.Msgs[:index], a.Msgs[index+1:]...)
	a.Write()
}

// Set 编辑第index条信息
func (a *App) Set(index int, todo string) {
	if todo == "" {
		return
	}
	a.Read()
	a.Msgs[index] = todo
	a.Write()
	// fmt.Println(a.Msgs)
}

// Read 把mhtodolist内容读取到a.Msgs里
func (a *App) Read() {
	user, err := user.Current()
	if err != nil {
		panic(err)
	}
	file, err := os.OpenFile(user.HomeDir+"\\mhtodolist", os.O_RDONLY|os.O_CREATE, 111)
	// err = nil
	defer file.Close()

	reder := bufio.NewReader(file)
	arr := []string{}
	for {
		line, err := reder.ReadString('\n')
		// err = nil
		if err == io.EOF {
			if len(line) != 0 {
				arr = append(arr, line)
			}
			break
		}
		if err != nil {
			panic("读取失败:" + err.Error())
		}
		arr = append(arr, line)
	}
	a.Msgs = arr
}

App.vue

./frontend/src/目录下的前端项目

调用显示HelloWorld.vue

实际上可以直接把前端代码写这个文件里

不过我是直接改造那个init项目所以保留了它的结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <!--<img id="logo" alt="Wails logo" src="./assets/images/logo-universal.png"/>-->
  <HelloWorld/>
  </template>

<style>
#item {
  color:255,0,0,
}

</style>

components/HelloWorld.vue

不善前端 虽然写出来了 但总觉得是写了依托答辩 方法主要都是后端套壳 样式写的不明不白的 不做详细的解释了

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
<script setup>
import {reactive} from 'vue'
import {MinWindow,SetH,KillSelf,Set,GetMsgs,GetLen,Del,Add,RedomSay} from '../../wailsjs/go/main/App'
import {SwitchButton,Minus,Edit,Delete,Check,Close,Refresh} from '@element-plus/icons-vue'

// todos 存一些数据被后面使用
const todos = reactive({
  newTodo: "",
  len: 0,
  msgs: [],
  edit: -1,
  edTodo:"",
  height: 600,
  say:"",
})

// 初始化函数们(这么写对吗?一费奶五.jpg)
getMsgs()
getLen()
redomSay()
window.onload = initH

// 应该有我不知道的更好的写法
// initH 设置合适的高度 setTimeout100 是未来等待dom更新 不写或写短了可能会dom更新不及时 拿到旧的数据
function initH(){
  setTimeout(reSetH,100)
  // console.log(todos.height)
}

// redomSay 随机一句话 下面红色这句 加减话的话在App.go中加减if分支
function redomSay(){
  RedomSay().then( result => {
    todos.say = result
  })
}

// add 添加一个todo
function add() {
  Add(todos.newTodo).then(() => {
    getLen()
    getMsgs()
    todos.newTodo = ""
  })
  // setTimeout 在reSetH之前等待dom更新
  // 不知道有没有更好的办法
  setTimeout(reSetH,100)
}

function del(i) {
  Del(i).then(() =>{
    getLen()
    getMsgs()
  })
  setTimeout(reSetH,100)
}

function set(i,todo){
  Set(i,todo).then(() =>{
    getLen()
    getMsgs()
    todos.edit = -1
  })
}

function getMsgs(){
  GetMsgs().then(result =>{
    todos.msgs = result
  }
  )
}

function getLen(){
  GetLen().then( result => {
    todos.len = result
  })
}

function reSetH(){
  // setTimeout(1000)
  todos.height = document.documentElement.getElementsByTagName("main")[0].offsetHeight+200
  // alert(todos.height)
  SetH(todos.height).then(() =>{})
}
</script>

<template>
  <!-- <main> -->
    <!-- <a @click="KillSelf()"><Close/></a> -->
    <el-header id="todoHeader" style="height: 100px;">
      <el-icon class="header-icon" @click="MinWindow()" :size="20"><Minus/></el-icon>
      <el-icon class="header-icon" @click="KillSelf()" :size="20"><Close/></el-icon>
      <!-- <a @click="MinWindow()" class="Document"><Minus/></a> -->
      <!-- <button @click="reSetH()">setH</button> -->
      <!-- <button @click="setH()">test</button> -->
    <!-- <p>"h:"{{ todos.height }}</p> -->
    <!-- <h2 >todos</h2> -->
      <div>
        <input v-model="todos.newTodo" @keyup.enter = "add()" placeholder = "做点什么..."/>
        <button @click="add()" :size="10">add</button>
      </div>
      <div>
      待做: {{ todos.len }}
      </div>
    </el-header>
   

    <el-main style="background-color:rgba(128,110,133,0.5);text-align:center;width: 400px;">
    <div id="lists">
      <!-- <ul> -->
      <el-row :gutter="30" class = "todo-li" type="flex" justify="end" v-for="(i,item) of todos.msgs" v-bind:key="item">
      <!--<nobr>-->
        <el-col :span="19">
            <p v-if="todos.edit != item">
              <span @dblclick="todos.edit = item;todos.edTodo=todos.msgs[item]" style="color: aliceblue;">{{ i }}</span>
            </p>
            <p v-else>
              <el-input label-width="10px" type="text" @keyup.enter = "set(item,todos.edTodo)" v-model="todos.edTodo"/>
            </p>
          </el-col>
          <el-col :span="5">
            <p v-if="todos.edit != item">
              <el-icon class="edit-button" @click = "todos.edit = item;todos.edTodo=todos.msgs[item] " :size="20"><Edit/></el-icon>
              <el-icon id="finished-button" @click = "del(item)" :size="20"><Delete/></el-icon>
              <!-- <el-icon id="delete-button" @click = "del(item)" :size="20"><Delete/></el-icon> -->
            </p>
            <p v-else>
              <el-icon slot="reference" class="edit-button" @click = "set(item,todos.edTodo)" :size="20"><Check/></el-icon>
              <el-icon id="edit-quxiao-button" @click="todos.edit = -1" :size="20"><Close/></el-icon>
            </p>
          </el-col>
      <!--</nobr>-->
        </el-row>
      <!-- </ul> -->
    </div>
  </el-main>
    
  <el-footer id="footer">
    <!-- <img id="logo" alt="Wails logo" src="../assets/images/logo-universal.png"/> -->
    <!-- <p>如我所愿,一切皆好</p> -->
    <p id="say">{{ todos.say }}</p>
    <el-icon id="Refresh" @click="redomSay()"><Refresh/></el-icon>
  </el-footer>
  <!-- </main> -->
</template>

<style scoped>
/*
body{
  background-image:url('gradient2.png');
}
*/
#footer{
  color: #CB8986;
}

#footer:hover #Refresh{
  opacity: 0.8;
}

#footer:hover #say{
  opacity: 1;
}

#Refresh{
  opacity: 0.3;
}

#say{
  opacity: 0.7;
}

#todo{
  color: #ccc;
}

.el-row {
  margin-bottom: 0px;
}
.el-col {
  border-radius: 10px;
}

#todoHeader:hover .header-icon{
  opacity: 1;
}

.header-icon{
  opacity:0.3;
}
.header-icon:hover{
  color: rgb(214, 44, 44);
  vertical-align: right;
}

#lists{
  /* background-color:rgba(255,255,255,0.6); */
  /* border: 2px solid rgba(0,0,0,0.6); */
  border-radius: 5px;
  /* margin: auto; */
  text-align: left;
  max-height: 500px;
  min-height: 300px;
  overflow-y: hidden;
  overflow-y: scroll;
  /* opacity: 0.3; */
  /* width: 10; */
}

#lists::-webkit-scrollbar {
    height: 0;
    width: 0;
}

#edit-input{
  /* width: 10; */
  border: transparent;
}

span {
  position: relative;
  /* left: 20px; */
  word-wrap: break-word;
  display: inline-block;
  color:black;
  border-bottom:2px solid grey;
  width: 240px;
}

#edit-quxiao-button{
  position: absolute;
  bottom: 15px;
  right: 24px;
  background-color: rgba(255, 255, 255, 0.603);
  border: none;
  /* color: white; */
  color: black;
  border: 2px solid #484848;
  padding: 5px 5px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 3px;
  border-radius: 20%;
}

#edit-quxiao-button:hover {
    box-shadow: 0px 0px 0px 2.5px rgba(255,255,255,0.2);
}

.edit-button{
  position: absolute;
  bottom: 15px;
  right: 60px;
  background-color: rgba(255, 255, 255, 0.603);
  border: none;
  /* color: white; */
  color: black;
  border: 2px solid #484848;
  padding: 5px 5px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 3px;
  border-radius: 20%;
  /* text-align:; */
}

.edit-button:hover {
    box-shadow: 0px 0px 0px 2.5px rgba(255,255,255,0.2);
}

/*
#add-button{
  background-color: rgba(255, 255, 255);
  border: none;
  color: black;
  padding: 5px 5px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 3px;
  border-radius: 20%;
}
#add-button:hover {
    box-shadow: 0px 0px 0px 2.5px rgba(255,255,255,0.2);
}
*/


#delete-button{
  position: absolute;
  bottom: 11.5px;
  right: 8px;
  /* background-color: #fff; */
  border: none;
  color: rgb(128,128,128);
  /* color: black; */
  /* border: px solid #484848; */
  padding: 5px 5px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 3px;
  border-radius: 20%;
}

#delete-button:hover {
    /* box-shadow: 0px 0px 0px 2.5px rgba(255,255,255,0.2); */
    color: rgb(252, 99, 99);
}

#finished-button{
  position: absolute;
  bottom: 15px;
  right: 24px;
  background-color: rgba(255, 255, 255, 0.603);
  border: none;
  /* color: white; */
  color: black;
  border: 2px solid #484848;
  padding: 5px 5px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 3px;
  border-radius: 20%;
}

#finished-button:hover {
    box-shadow: 0px 0px 0px 2.5px rgba(255,255,255,0.2);
}

#todoHeader {
  user-select:none;
  --wails-draggable:drag;
  display: block;
  width: 100%;
  height: 5 px;
  color: grey;
}
</style>

其他…

  • 在前端使用某个包时加入了./frontend/src/todolist.d.ts文件 文件内容仅有一行

    declare module ’element-plus/dist/locale/zh-cn.mjs';

  • 请多关照

demo2-chatGPT

*仅仅是连接个人的海外服务器 让它请求chatGPT 并返回数据给本地

*本案例中会给出服务器端代码 但不会把其中key以及前端请求的接口公开 而是使用对应的中文替换应有的数据 还请谅解

最终结果

90510ffe8777360d9100a4347a3c02d

文件

还是像之前那样新建

./main.go

除了窗口大小其他应该都是默认

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
	"embed"

	"github.com/wailsapp/wails/v2"
	"github.com/wailsapp/wails/v2/pkg/options"
	"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)

//go:embed all:frontend/dist
var assets embed.FS

func main() {
	// Create an instance of the app structure
	app := NewApp()

	// Create application with options
	err := wails.Run(&options.App{
		Title:  "chatGPT",
		Width:  860,
		Height: 680,
		AssetServer: &assetserver.Options{
			Assets: assets,
		},
		BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
		OnStartup:        app.startup,
		Bind: []interface{}{
			app,
		},
	})

	if err != nil {
		println("Error:", err.Error())
	}
}

./app.go

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package main

import (
	"bufio"
	"context"
	"encoding/json"
	"io/ioutil"
	"net/http"
	"os"
	"os/user"
	"strings"
	"time"

	"github.com/wailsapp/wails/v2/pkg/runtime"
	"gopkg.in/yaml.v2"
)

// App struct
type App struct {
	ctx  context.Context
	Msgs []string
	Asks []string
}

// 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
// startup 初始化函数
func (a *App) startup(ctx context.Context) {
	a.ctx = ctx
	// runtime.WindowSetPosition(a.ctx, -1080, 994)
	a.setWindowPosition()
}

func (a *App) GetMsgs() []string {
	return a.Msgs
}

func (a *App) GetAsks() []string {
	return a.Asks
}

// setWindowPosition 设置显示的位置 不需要被前端使用 所以没有公开
func (a *App) setWindowPosition() {
	user, err := user.Current()
	if err != nil {
		return
	}
	f, err := os.OpenFile(user.HomeDir+`\Documents\mhhqdz\chatGPT\conf.yml`, os.O_RDONLY, 0666)
	// f, err := os.OpenFile(`.\frontend\src\assets\conf.yml`, os.O_RDWR, 0666)
	// 如果文件打开错误 大概率是没创建 不管它就行 这是正常的 毕竟连这个按钮都隐藏了
	if err != nil {
		return
	}
	defer f.Close()

	data, err := ioutil.ReadAll(f)
	if err != nil {
		panic(err)
	}
	m := make(map[string]int)
	if err := yaml.Unmarshal([]byte(data), &m); err != nil {
		panic(err)
	}
	// fmt.Println(m["windowPositionX"], m["windowPositionY"])
	runtime.WindowSetPosition(a.ctx, m["windowPositionX"], m["windowPositionY"])
}

// FixedWindowPosition 固定窗口位置为启动位置
func (a *App) FixedWindowPosition() {
	user, err := user.Current()
	if err != nil {
		return
	}
	f, err := os.OpenFile(user.HomeDir+`\Documents\mhhqdz\chatGPT\conf.yml`, os.O_RDWR|os.O_CREATE, 0666)
	if err != nil {
		err := os.MkdirAll(user.HomeDir+`\Documents\mhhqdz\chatGPT`, os.ModePerm)
		if err != nil {
			panic(err)
		}
		f, err = os.OpenFile(user.HomeDir+`\Documents\mhhqdz\chatGPT\conf.yml`, os.O_RDWR|os.O_CREATE, 0666)
		if err != nil {
			panic(err)
		}
	}
	defer f.Close()

	m := make(map[string]int)
	x, y := runtime.WindowGetPosition(a.ctx)
	m["windowPositionX"] = x
	m["windowPositionY"] = y
	out, err := yaml.Marshal(&m)
	if err != nil {
		panic(err)
	}
	// fmt.Println(string(out))
	f.Truncate(0)
	writer := bufio.NewWriter(f)
	writer.Write(out)
	writer.Flush()
}

// setMsgs 以队列的方式存储过往信息 不知到怎么传指针到前端 所以用了数组
func (a *App) setMsgs(s1, s2 string) {
	if len(a.Msgs) < 10 {
		a.Msgs = append(a.Msgs, s1)
		a.Asks = append(a.Asks, s2)
		return
	}
	for i := 0; i < 9; i++ {
		a.Msgs[i] = a.Msgs[i+1]
		a.Asks[i] = a.Asks[i+1]
	}
	a.Msgs[9] = s1
	a.Asks[9] = s2
}

// wails官方的输入输出示例 因为类型一样所以名字都没改 直接拿来用了
// Greet 请求服务器 并返回结果
func (a *App) Greet(ask string) string {
	// ask1 用于后面存入 ask队列
	ask1 := ask
	// 这一堆字符替换是为了绕开特殊字符 不转会有些无法正确请求
	ask = strings.ReplaceAll(ask, "%", "%25")
	ask = strings.ReplaceAll(ask, " ", "%20")
	ask = strings.ReplaceAll(ask, "?", "%3F")
	ask = strings.ReplaceAll(ask, "+", "%2B")
	ask = strings.ReplaceAll(ask, "#", "%23")
	ask = strings.ReplaceAll(ask, "&", "%26")

	client := http.Client{
		Timeout: 300 * time.Second,
	}
    // xx.xx.xx.xx代表你的服务器公网ip 80换成你的服务器开放的端口
    // /chatGPT?msg=ask 拿到ask之后用自己喜欢的处理方式就行 不一定是这么拼接的
    // Get 出于安全 不应该使用Get请求 但Get测试方便一些 按您的喜欢来即可 
    r, err := client.Get("http://xx.xx.xx.xx:80/chatGPT?msg=" + ask)
	if err != nil {
		return "error:" + err.Error()
	}
	m := map[string]string{}
	b, err := ioutil.ReadAll(r.Body)
	if err != nil {
		return err.Error()
	}
	json.Unmarshal(b, &m)
	s := m["message"]
	a.setMsgs(s, ask1)
	return s
}

App.vue

把components干掉了 直接在App.vue里写(开摆了)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 项目结构
PS D:\go\src\wailsStudy\chatGPT\frontend> ls

    Directory: D:\go\src\wailsStudy\chatGPT\frontend

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----           2023/3/28    14:09                dist
d----           2023/3/28    13:34                node_modules
d----           2023/3/28    13:34                src
d----           2023/3/28    14:09                wailsjs
-a---           2023/3/25     9:57            327 index.html
-a---           2023/3/25    14:29         103100 package-lock.json
-a---           2023/3/25    14:29            345 package.json
-a---           2023/3/25    14:29             32 package.json.md5
-a---           2023/3/25     9:57            394 README.md
-a---           2023/3/25     9:57            154 vite.config.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<script setup>
import {reactive} from 'vue'
import {Greet,GetAsks,GetMsgs,FixedWindowPosition} from '../wailsjs/go/main/App'

const data = reactive({
  name: "",
  msgs:[],
  asks:[],
  flag:false,
})

function greet() {
  let a = data.name
  data.name=""
  data.flag=true
  Greet(a).then(result => {
    getAsks()
    getMsgs()
    data.flag=false
  })
}

function getMsgs(){
  GetMsgs().then(result =>{
    data.msgs = result
  }
  )
}

function getAsks(){
  GetAsks().then(result =>{
    data.asks = result
  }
  )
}

</script>

<template style="overflow: hidden;">
  <!-- <img id="logo" alt="Wails logo" src="./assets/images/logo-universal.png"/> -->
  <main>
    <div style="overflow: hidden;">
    <div id="input" class="input-box">
      <input id="name" v-model="data.name" @keyup.enter="greet()" autocomplete="off" class="input" type="text"/>
      <button class="btn" @click="greet">Send</button>
    </div>
    <div v-if="data.flag" >
      <p class="text">少女祈祷中<dot class=" dot">...</dot></p>
    </div>
    <!-- 使窗口在这个位置启动启动 因为不好看所以注释掉了-->
    <!-- <button @click="FixedWindowPosition()" >固定为启动时位置</button> -->
    <!-- <div id="result" class="result" style="white-space:pre-line;text-align: left;">{{ data.resultText }}</div> -->
    
      <p v-for="(i,item) of data.msgs"  v-bind:key="item">
        <p style="text-align: right;margin-right: 4%;color: red;">{{ data.asks[data.msgs.length-item-1] }}</p>
        <p style="text-align: left;margin-left: 2%;">{{ data.msgs[data.msgs.length-item-1] }}</p>
      </p>
    </div>
    <!-- <ReactMarkdown>*World*</ReactMarkdown> -->
  </main>
</template>

<style>
    .dot{
      display: inline-block;
      text-align: center;
      margin: 0 auto;
      overflow: hidden;
      height: 1em;
      line-height: 1em;
      vertical-align: -.25em;
      }
      .dot::before{
        display: block;
        content: '...\A.. \A.  ';
        white-space: pre-wrap;
        animation: dot 1.5s infinite step-start both;
        }
        @keyframes dot {
          33%{
            transform: translateY(-2em);
            }
          66%{
            transform: translateY(-1em);
            }
        }
</style>

服务器那边的代码

因为国内网络特殊 所以没有办法在国内直接请求 如果您不受此限制 则完全可以直接在上面的程序里请求

写的比较简单 更多需求请按照您的喜好来实现

*在国内使用一些使用者较多的魔法工具 来访问openai的话可能会被封号 还请注意

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

import (
        "context"
        "fmt"
        "net/http"

        "github.com/gin-gonic/gin"
        openai "github.com/sashabaranov/go-openai"
)

func talk(msg string) (string, error) {
        if len(msg) == 0 {
                return "", fmt.Errorf("请输入信息")
        }
    	// openai的key
    	// 使用了go-openai的包
        client := openai.NewClient("sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
        resp, err := client.CreateChatCompletion(
                context.Background(),
                openai.ChatCompletionRequest{
                        Model: openai.GPT3Dot5Turbo,
                        Messages: []openai.ChatCompletionMessage{
                                {
                                        Role:    openai.ChatMessageRoleUser,
                                        Content: msg,
                                },
                        },
                },
        )

        if err != nil {
                return "", err
        }
        return resp.Choices[0].Message.Content, nil
}

func chatGPT(c *gin.Context) {
        msg := c.Query("msg")
        s, err := talk(msg)
        if err != nil {
                c.JSON(http.StatusAccepted, gin.H{
                        "err":err.Error(),
                })
                return
        }
        fmt.Println(s)
        c.JSON(http.StatusAccepted, gin.H{
                "message": s,
        })
}

func main() {
        r := gin.Default()
        r.GET("/chatGPT", chatGPT)
    	r.Run("内网ip:开放端口")
}
updatedupdated2023-03-282023-03-28