输入框多列 fyne

 如上图效果,在没有自定义器件的情况下,尝试使用border布局可以实现。

configForm := widget.NewForm(
		&widget.FormItem{Text: "IP:", Widget: container.NewBorder(nil, nil, nil, port, ip)},
		&widget.FormItem{Text: "用户名:", Widget: userName},
		passwordItem,
	)

我们希望IP表单项目,IP的输入框更宽,端口输入框稍窄一些。在border布局中,将上、下,左的位置全部置空,右的位置用来填充端口输入框,中间的位置给IP输入框,即中心区域,扩展填充可用空间。

修改后的代码为: package main import ( "fmt" "image" "log" "net/http" "os" "path/filepath" "runtime" "strings" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" "main.go/res" "main.go/dataModel/CookieModel" "main.go/dataModel/ShopModel" "main.go/dataModel/SkuModel" "main.go/dataModel/UserModel" "main.go/tuuz/database" ) // 全局状态 type AppState struct { Window fyne.Window // 添加窗口引用 CurrentUser UserModel.UserInfo Shops []ShopModel.Account ProductTabs *container.AppTabs StatusBar *widget.Label ShopListBinding binding.UntypedList LoginForm *widget.Form LeftPanel *container.Split // 存储左侧面板引用 FilterFilePath string // 存储过滤文件路径 FilterKeywords []string // 存储过滤关键字 } func main() { os.Setenv("PLAYWRIGHT_BROWSERS_PATH", "./browsers") database.Init() UserModel.UserInit() ShopModel.ShopInit() CookieModel.CreateCookieInfoTable() SkuModel.ProductInit() myApp := app.New() myWindow := myApp.NewWindow("店铺管理工具") myWindow.Resize(fyne.NewSize(1200, 800)) // 初始化应用状态 appState := &AppState{ FilterFilePath: getDefaultFilterPath(), // 设置默认过滤文件路径 } // 尝试加载默认过滤文件 go loadFilterFile(appState) // 创建状态栏 appState.StatusBar = widget.NewLabel("就绪") statusBar := container.NewHBox(layout.NewSpacer(), appState.StatusBar) // 创建主布局 mainContent := createMainUI(myWindow, appState) // 设置整体布局 content := container.NewBorder(nil, statusBar, nil, nil, mainContent) myWindow.SetContent(content) // 启动时尝试自动登录(如果有保存的用户) go tryAutoLogin(appState) myWindow.ShowAndRun() } // 获取默认过滤文件路径 func getDefaultFilterPath() string { // 根据操作系统设置默认路径 if runtime.GOOS == "windows" { return filepath.Join(os.Getenv("USERPROFILE"), "Documents", "filter.txt") } return filepath.Join(os.Getenv("HOME"), "filter.txt") } // 加载过滤文件 func loadFilterFile(appState *AppState) { if appState.FilterFilePath == "" { return } // 检查文件是否存在 if _, err := os.Stat(appState.FilterFilePath); os.IsNotExist(err) { // 文件不存在,创建空文件 err := os.WriteFile(appState.FilterFilePath, []byte{}, 0644) if err != nil { log.Printf("创建过滤文件失败: %v", err) } return } // 读取文件内容 content, err := os.ReadFile(appState.FilterFilePath) if err != nil { log.Printf("读取过滤文件失败: %v", err) return } // 解析关键字 lines := strings.Split(string(content), "\n") appState.FilterKeywords = []string{} for _, line := range lines { trimmed := strings.TrimSpace(line) if trimmed != "" { appState.FilterKeywords = append(appState.FilterKeywords, trimmed) } } // 更新状态栏 fyne.DoAndWait(func() { appState.StatusBar.SetText(fmt.Sprintf("已加载 %d 个过滤关键字", len(appState.FilterKeywords))) }) } // 自定义布局:固定宽度布局 type fixedWidthLayout struct { width float32 } func (f *fixedWidthLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { if len(objects) == 0 { return fyne.NewSize(f.width, 0) } min := objects[0].MinSize() return fyne.NewSize(f.width, min.Height) } func (f *fixedWidthLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { // 布局所有子元素(只有一个),宽度固定,高度使用容器的高度 for _, child := range objects { child.Resize(fyne.NewSize(f.width, size.Height)) } } // 修改主布局函数 - 确保右侧面板正确填充空间 func createMainUI(window fyne.Window, appState *AppState) fyne.CanvasObject { appState.Window = window // 保存窗口引用 // 创建左侧面板(登录 + 店铺列表 + 过滤功能) leftPanel := createLeftPanel(window, appState) // 使用自定义布局固定左侧宽度为300像素 fixedLeft := container.New(&fixedWidthLayout{width: 300}, leftPanel) // 右侧面板(商品TAB展示) appState.ProductTabs = container.NewAppTabs() appState.ProductTabs.SetTabLocation(container.TabLocationTop) // 标签在顶部 // 创建右侧面板容器 - 确保填充剩余空间 rightPanel := container.NewBorder( widget.NewLabelWithStyle("商品信息", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), nil, nil, nil, container.NewMax(appState.ProductTabs), // 使用Max确保标签页填充空间 ) // 主布局(左右固定宽度布局) return container.NewBorder( nil, nil, fixedLeft, // 左侧固定宽度面板 nil, rightPanel, // 右侧主内容区 ) } // 创建过滤功能面板 func createFilterPanel(appState *AppState) fyne.CanvasObject { // 创建文件路径标签 pathLabel := widget.NewLabel("过滤文件: " + appState.FilterFilePath) pathLabel.Wrapping = fyne.TextWrapWord // 创建选择文件按钮 selectButton := widget.NewButton("选择过滤文件", func() { dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) { if err != nil { dialog.ShowError(err, appState.Window) return } if reader == nil { return // 用户取消 } // 更新文件路径 appState.FilterFilePath = reader.URI().Path() pathLabel.SetText("过滤文件: " + appState.FilterFilePath) // 加载过滤文件 go loadFilterFile(appState) }, appState.Window) }) // 创建刷新按钮 refreshButton := widget.NewButton("刷新过滤", func() { if appState.FilterFilePath != "" { appState.StatusBar.SetText("刷新过滤关键字...") go loadFilterFile(appState) } else { appState.StatusBar.SetText("请先选择过滤文件") } }) // 创建按钮容器 buttonContainer := container.NewHBox( selectButton, refreshButton, ) // 创建关键字计数标签 keywordCount := widget.NewLabel(fmt.Sprintf("关键字数量: %d", len(appState.FilterKeywords))) keywordCount.TextStyle = fyne.TextStyle{Bold: true} // 创建面板 return container.NewVBox( widget.NewSeparator(), widget.NewLabelWithStyle("商品过滤", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), pathLabel, keywordCount, buttonContainer, ) } // 创建左侧面板 func createLeftPanel(window fyne.Window, appState *AppState) fyne.CanvasObject { // 登录表单 loginPanel := createLoginForm(appState) // 店铺列表 shopListPanel := createShopListPanel(appState) // 过滤功能面板 filterPanel := createFilterPanel(appState) // 左侧布局(上中下分割) mainPanel := container.NewVBox( loginPanel, shopListPanel, filterPanel, ) // 添加间距 return container.NewPadded(mainPanel) } // 创建登录表单 - 优化布局版本 func createLoginForm(appState *AppState) fyne.CanvasObject { usernameEntry := widget.NewEntry() passwordEntry := widget.NewPasswordEntry() // 设置输入框的占位符 usernameEntry.PlaceHolder = "输入邮箱地址" passwordEntry.PlaceHolder = "输入密码" // 尝试从数据库加载用户 user, err := UserModel.Api_find_by_username(usernameEntry.Text) if err == nil && user.LoginName != "" { usernameEntry.SetText(user.LoginName) } // 创建登录按钮 loginButton := widget.NewButton("登录", func() { appState.StatusBar.SetText("登录中...") go func() { // 模拟登录过程 time.Sleep(500 * time.Millisecond) shops := ShopModel.Api_select_struct(nil) if len(shops) == 0 { fyne.DoAndWait(func() { appState.StatusBar.SetText("获取店铺信息为空") }) return } appState.Shops = shops appState.CurrentUser, _ = UserModel.Api_find_by_username(usernameEntry.Text) // 更新UI fyne.DoAndWait(func() { appState.StatusBar.SetText(fmt.Sprintf("登录成功! 共 %d 个店铺", len(shops))) updateShopListBinding(appState) // 切换登录状态显示 switchToLoggedInState(appState, usernameEntry.Text) }) }() }) // 创建表单布局 - 优化后的版本 // 使用Form布局让输入框占据剩余空间 form := widget.NewForm( widget.NewFormItem("邮箱:", usernameEntry), widget.NewFormItem("密码:", passwordEntry), ) // // 设置表单项为水平布局 // form.Items[0].TextStyle = fyne.TextStyle{Bold: true} // form.Items[1].TextStyle = fyne.TextStyle{Bold: true} // 创建表单容器 formContainer := container.NewVBox( layout.NewSpacer(), form, layout.NewSpacer(), container.NewCenter(loginButton), layout.NewSpacer(), ) // 添加标题和整体布局 title := widget.NewLabelWithStyle("登录面板", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}) // 使用Padded容器添加内边距 return container.NewPadded( container.NewBorder( title, nil, nil, nil, formContainer, ), ) } // 切换到登录状态显示 func switchToLoggedInState(appState *AppState, username string) { // 创建用户信息显示 userInfo := container.NewVBox( widget.NewLabelWithStyle("登录状态", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), widget.NewSeparator(), container.NewHBox( widget.NewLabel("用户:"), widget.NewLabel(username), ), container.NewHBox( widget.NewLabel("店铺数量:"), widget.NewLabel(fmt.Sprintf("%d", len(appState.Shops))), ), widget.NewSeparator(), ) // 创建注销按钮 logoutButton := widget.NewButton("注销", func() { // 重置状态 appState.CurrentUser = UserModel.UserInfo{} appState.Shops = []ShopModel.Account{} appState.ProductTabs.Items = []*container.TabItem{} // 清空标签页 appState.ProductTabs.Refresh() // 刷新标签页 // 更新UI -- 不能主线程中执行, fyne.DoAndWait 提示错误 appState.StatusBar.SetText("已注销") updateShopListBinding(appState) switchToLoginForm(appState) }) // 将注销按钮居中 centeredLogoutButton := container.NewCenter(logoutButton) // 组合所有组件 loggedInPanel := container.NewVBox( userInfo, layout.NewSpacer(), centeredLogoutButton, layout.NewSpacer(), ) // 替换左侧面板的顶部内容 appState.LeftPanel.Leading = container.NewPadded(loggedInPanel) appState.LeftPanel.Refresh() // 刷新布局 } // 切换回登录表单 func switchToLoginForm(appState *AppState) { // 重新创建登录表单 loginForm := createLoginForm(appState) // 替换左侧面板的顶部内容 appState.LeftPanel.Leading = loginForm appState.LeftPanel.Refresh() // 刷新VSplit容器 } // 尝试自动登录 - 添加主线程UI更新 func tryAutoLogin(appState *AppState) { // 获取所有用户 users := UserModel.Api_select_struct(nil) if len(users) == 0 { fyne.DoAndWait(func() { appState.StatusBar.SetText("获取已经存在的账号为空") }) return } // 尝试使用第一个用户自动登录 user := users[0] fyne.DoAndWait(func() { appState.StatusBar.SetText(fmt.Sprintf("尝试自动登录: %s...", user.LoginName)) }) // 更新登录表单 if appState.LoginForm != nil { // 获取用户名输入框 usernameItem := appState.LoginForm.Items[0] usernameEntry, ok := usernameItem.Widget.(*widget.Entry) if !ok { fyne.DoAndWait(func() { appState.StatusBar.SetText("自动登录失败: 用户名控件类型错误") }) return } // 获取密码输入框 - 这里使用 *widget.Entry 类型 passwordItem := appState.LoginForm.Items[1] passwordEntry, ok := passwordItem.Widget.(*widget.Entry) if !ok { fyne.DoAndWait(func() { appState.StatusBar.SetText("自动登录失败: 密码控件类型错误") }) return } // 在主线程更新UI fyne.DoAndWait(func() { usernameEntry.SetText(user.LoginName) passwordEntry.SetText(user.LoginPass) appState.StatusBar.SetText("正在自动登录...") }) // 触发登录 appState.LoginForm.OnSubmit() } } // 修改后的异步加载店铺头像函数 func loadShopAvatar(img *canvas.Image, url string) { if url == "" { // 使用默认头像 fyne.DoAndWait(func() { img.Resource = fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "account") img.Refresh() }) return } // 创建HTTP客户端(可设置超时) client := &http.Client{ Timeout: 10 * time.Second, } resp, err := client.Get(url) if err != nil { log.Printf("加载头像失败: %v", err) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Printf("头像请求失败: %s", resp.Status) return } // 解码图片 imgData, _, err := image.Decode(resp.Body) if err != nil { log.Printf("解码头像失败: %v", err) return } // 在主线程更新UI fyne.DoAndWait(func() { img.Image = imgData img.Refresh() }) } // 修改后的 createShopListPanel 函数 func createShopListPanel(appState *AppState) fyne.CanvasObject { // 创建绑定数据 appState.ShopListBinding = binding.NewUntypedList() // 创建列表控件 - 使用自定义模板 list := widget.NewListWithData( appState.ShopListBinding, func() fyne.CanvasObject { // 创建包含头像和名称的水平容器 avatar := canvas.NewImageFromResource(nil) avatar.SetMinSize(fyne.NewSize(40, 40)) avatar.FillMode = canvas.ImageFillContain nameLabel := widget.NewLabel("") statusIcon := widget.NewIcon(nil) return container.NewHBox( avatar, container.NewVBox( nameLabel, ), layout.NewSpacer(), statusIcon, ) }, // 修改后的列表项更新函数 func(item binding.DataItem, obj fyne.CanvasObject) { // 安全类型断言 hbox, ok := obj.(*fyne.Container) if !ok { log.Println("错误:传入对象不是容器") return } // 检查容器结构是否如预期 if len(hbox.Objects) < 4 { log.Println("错误:容器子元素数量不足") return } // 获取头像组件(直接类型断言) avatar, ok := hbox.Objects[0].(*canvas.Image) if !ok { log.Println("错误:第一个子元素不是图像") return } // 获取名称标签(通过嵌套容器) nameContainer, ok := hbox.Objects[1].(*fyne.Container) if !ok || len(nameContainer.Objects) == 0 { log.Println("错误:名称容器无效") return } nameLabel, ok := nameContainer.Objects[0].(*widget.Label) if !ok { log.Println("错误:名称标签无效") return } // 获取状态图标 statusIcon, ok := hbox.Objects[3].(*widget.Icon) if !ok { log.Println("错误:状态图标无效") return } // 获取店铺数据 val, err := item.(binding.Untyped).Get() if err != nil { log.Printf("获取数据失败: %v", err) return } shop, ok := val.(ShopModel.Account) if !ok { log.Println("错误:数据类型不匹配") return } // 设置店铺名称 nameLabel.SetText(shop.AccountName) // 设置状态图标 if shop.CanLogin { statusIcon.SetResource(res.ResShuffleSvg) } else { statusIcon.SetResource(fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "error")) } // 异步加载头像(使用原有loadShopAvatar函数) go loadShopAvatar(avatar, shop.AccountAvatar) }, ) // 添加点击事件 - 使用主线程更新UI list.OnSelected = func(id widget.ListItemID) { shop := appState.Shops[id] // 在主线程更新状态栏 appState.StatusBar.SetText(fmt.Sprintf("加载 %s 的商品...", shop.AccountName)) go func() { // 加载商品数据 products, err := loadProductsForShop(shop, appState) if err != nil { fyne.DoAndWait(func() { appState.StatusBar.SetText("加载商品失败: " + err.Error()) }) return } // 在主线程更新UI fyne.DoAndWait(func() { appState.StatusBar.SetText(fmt.Sprintf("已加载 %d 个商品", len(products))) addOrUpdateProductTab(appState, shop, products) }) }() } // 创建滚动容器 scrollContainer := container.NewScroll(list) scrollContainer.SetMinSize(fyne.NewSize(280, 200)) // 设置最小尺寸 return container.NewBorder( widget.NewLabel("店铺列表"), nil, nil, nil, scrollContainer, // 使用可滚动的列表 ) } // 更新店铺列表绑定数据 func updateShopListBinding(appState *AppState) { // 清空绑定数据 appState.ShopListBinding.Set(make([]interface{}, 0)) // 添加新数据 values := make([]interface{}, len(appState.Shops)) for i, shop := range appState.Shops { values[i] = shop } appState.ShopListBinding.Set(values) } // 应用商品过滤 func applyProductFilter(products []SkuModel.DataItem, keywords []string) []SkuModel.DataItem { if len(keywords) == 0 { return products // 没有关键字,返回所有商品 } filtered := []SkuModel.DataItem{} for _, product := range products { exclude := false for _, keyword := range keywords { if strings.Contains(strings.ToLower(product.Name), strings.ToLower(keyword)) { exclude = true break } } if !exclude { filtered = append(filtered, product) } } return filtered } // 为店铺加载商品数据 func loadProductsForShop(shop ShopModel.Account, appState *AppState) ([]SkuModel.DataItem, error) { // 获取店铺的Cookie信息 // cookieInfo, found := CookieModel.Api_find_by_subject_id(shop.SubjectID) // if !found { // return nil, fmt.Errorf("未找到店铺的Cookie信息") // } // 模拟API调用获取商品数据 time.Sleep(500 * time.Millisecond) // 模拟网络延迟 // 模拟返回数据 products := []SkuModel.DataItem{ {ProductID: "1001", Name: "高端智能手机", MarketPrice: 99900, DiscountPrice: 100}, {ProductID: "1002", Name: "无线蓝牙耳机", MarketPrice: 199900, DiscountPrice: 50}, {ProductID: "1003", Name: "智能手表", MarketPrice: 299900, DiscountPrice: 30}, {ProductID: "1004", Name: "平板电脑", MarketPrice: 399900, DiscountPrice: 20}, {ProductID: "1005", Name: "笔记本电脑", MarketPrice: 499900, DiscountPrice: 10}, } // 应用过滤 filteredProducts := applyProductFilter(products, appState.FilterKeywords) return filteredProducts, nil } // 修改 addOrUpdateProductTab 函数 - 确保商品列表填充标签页空间 func addOrUpdateProductTab(appState *AppState, shop ShopModel.Account, products []SkuModel.DataItem) { tabTitle := shop.AccountName // 检查是否已存在该TAB for _, tab := range appState.ProductTabs.Items { if tab.Text == tabTitle { // 更新现有TAB tab.Content = container.NewMax(createProductList(products)) appState.ProductTabs.Refresh() return } } // 创建新TAB newTab := container.NewTabItem( tabTitle, container.NewMax(createProductList(products)), ) appState.ProductTabs.Append(newTab) appState.ProductTabs.Select(newTab) } // 创建商品列表 - 修复表格填充问题 func createProductList(products []SkuModel.DataItem) fyne.CanvasObject { // 创建表格 table := widget.NewTable( func() (int, int) { return len(products) + 1, 4 // 行数=商品数+表头,列数=4 }, func() fyne.CanvasObject { return widget.NewLabel("模板文本") }, func(id widget.TableCellID, cell fyne.CanvasObject) { label := cell.(*widget.Label) if id.Row == 0 { // 表头 switch id.Col { case 0: label.SetText("商品ID") case 1: label.SetText("商品名称") case 2: label.SetText("价格") case 3: label.SetText("库存") } label.TextStyle.Bold = true return } // 数据行 product := products[id.Row-1] switch id.Col { case 0: label.SetText(product.ProductID) case 1: label.SetText(product.Name) case 2: label.SetText(fmt.Sprintf("¥%.2f", float64(product.MarketPrice)/100)) case 3: label.SetText(fmt.Sprintf("%d", product.DiscountPrice)) } }, ) // 设置列宽 table.SetColumnWidth(0, 100) table.SetColumnWidth(1, 300) table.SetColumnWidth(2, 100) table.SetColumnWidth(3, 100) // 创建滚动容器 scrollContainer := container.NewScroll(table) scrollContainer.SetMinSize(fyne.NewSize(600, 400)) // 返回可滚动的表格容器 return scrollContainer } 点击登陆按钮后,出现错误为: panic: runtime error: invalid memory address or nil pointer dereference [signal 0xc0000005 code=0x1 addr=0x48 pc=0x7ff793c25bea] goroutine 1 [running, locked to thread]: main.switchToLoggedInState(0xc0003c2000, {0xc004da8cd0, 0x1}) E:/GoProject/main.go:377 +0x9ea main.createLoginForm.func1.1.2() E:/GoProject/main.go:296 +0xbd fyne.io/fyne/v2/internal/driver/glfw.(*gLDriver).runGL(0xc0001b9610?) C:/Users/Administrator/go/pkg/mod/fyne.io/fyne/v2@v2.6.1/internal/driver/glfw/loop.go:145 +0x185 fyne.io/fyne/v2/internal/driver/glfw.(*gLDriver).Run(0xc0002ff080) C:/Users/Administrator/go/pkg/mod/fyne.io/fyne/v2@v2.6.1/internal/driver/glfw/driver.go:162 +0x72 fyne.io/fyne/v2/app.(*fyneApp).Run(0xc0002ff130) C:/Users/Administrator/go/pkg/mod/fyne.io/fyne/v2@v2.6.1/app/app.go:77 +0x102 fyne.io/fyne/v2/internal/driver/glfw.(*window).ShowAndRun(0xc0002e24e0) C:/Users/Administrator/go/pkg/mod/fyne.io/fyne/v2@v2.6.1/internal/driver/glfw/window.go:222 +0x64 main.main() E:/GoProject/main.go:79 +0x3f2 exit status 2
07-23
这是最新修改的能正常运行的 main.go 代码: package main import ( "fmt" "image" "log" "net/http" "os" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" "main.go/dataModel/CookieModel" "main.go/dataModel/ShopModel" "main.go/dataModel/SkuModel" "main.go/dataModel/UserModel" "main.go/tuuz/database" ) // 全局状态 type AppState struct { CurrentUser UserModel.UserInfo Shops []ShopModel.Account ProductTabs *container.AppTabs StatusBar *widget.Label ShopListBinding binding.UntypedList LoginForm *widget.Form } func main() { os.Setenv("PLAYWRIGHT_BROWSERS_PATH", "./browsers") database.Init() UserModel.UserInit() ShopModel.ShopInit() CookieModel.CreateCookieInfoTable() SkuModel.ProductInit() myApp := app.New() myWindow := myApp.NewWindow("店铺管理工具") myWindow.Resize(fyne.NewSize(1200, 800)) // 初始化应用状态 appState := &AppState{} // 创建状态栏 appState.StatusBar = widget.NewLabel("就绪") statusBar := container.NewHBox(layout.NewSpacer(), appState.StatusBar) // 创建主布局 mainContent := createMainUI(myWindow, appState) // 设置整体布局 content := container.NewBorder(nil, statusBar, nil, nil, mainContent) myWindow.SetContent(content) // 启动时尝试自动登录(如果有保存的用户) go tryAutoLogin(appState) myWindow.ShowAndRun() } // 创建主UI布局 func createMainUI(window fyne.Window, appState *AppState) fyne.CanvasObject { // 左侧面板(登录 + 店铺列表) leftPanel := createLeftPanel(window, appState) // 右侧面板(商品TAB展示) appState.ProductTabs = container.NewAppTabs() rightPanel := container.NewBorder( widget.NewLabel("商品信息"), nil, nil, nil, appState.ProductTabs, ) // 主布局(左右分割) split := container.NewHSplit(leftPanel, rightPanel) split.SetOffset(0.3) // 左侧占30% return split } // 创建左侧面板 func createLeftPanel(window fyne.Window, appState *AppState) fyne.CanvasObject { // 登录表单 loginPanel := createLoginForm(appState) // 店铺列表 shopListPanel := createShopListPanel(appState) // 左侧布局(上下分割) return container.NewVSplit(loginPanel, shopListPanel) } // 创建登录表单 func createLoginForm(appState *AppState) fyne.CanvasObject { usernameEntry := widget.NewEntry() passwordEntry := widget.NewPasswordEntry() // 这是正确的密码输入框创建方式 // 尝试从数据库加载用户 user, err := UserModel.Api_find_by_username(usernameEntry.Text) if err == nil && user.LoginName != "" { usernameEntry.SetText(user.LoginName) } // 创建表单 form := &widget.Form{ Items: []*widget.FormItem{ {Text: "邮箱", Widget: usernameEntry}, {Text: "密码", Widget: passwordEntry}, // 这里直接使用 passwordEntry }, OnSubmit: func() { appState.StatusBar.SetText("登录中...") go func() { // shops, err := service.LoginOrAutoLogin(usernameEntry.Text, passwordEntry.Text) // if err != nil { // appState.StatusBar.SetText("登录失败: " + err.Error()) // return // } // // 更新UI状态 // appState.CurrentUser, _ = UserModel.Api_find_by_username(usernameEntry.Text) // appState.Shops = shops // appState.StatusBar.SetText(fmt.Sprintf("登录成功! 共 %d 个店铺", len(shops))) shops := ShopModel.Api_select_struct(nil) if len(shops) == 0 { appState.StatusBar.SetText(fmt.Sprintf("获取已经存在的店铺信息为空")) return } appState.Shops = shops // 更新店铺列表 updateShopListBinding(appState) }() }, } appState.LoginForm = form return container.NewVBox( widget.NewLabel("登录面板"), form, ) } // 尝试自动登录 func tryAutoLogin(appState *AppState) { // 获取所有用户 users := UserModel.Api_select_struct(nil) if len(users) == 0 { appState.StatusBar.SetText(fmt.Sprintf("获取已经存在的账号为空")) return } // 尝试使用第一个用户自动登录 user := users[0] appState.StatusBar.SetText(fmt.Sprintf("尝试自动登录: %s...", user.LoginName)) // 更新登录表单 if appState.LoginForm != nil { // 获取用户名输入框 usernameItem := appState.LoginForm.Items[0] usernameEntry, ok := usernameItem.Widget.(*widget.Entry) if !ok { appState.StatusBar.SetText("自动登录失败: 用户名控件类型错误") return } // 获取密码输入框 - 这里使用 *widget.Entry 类型 passwordItem := appState.LoginForm.Items[1] passwordEntry, ok := passwordItem.Widget.(*widget.Entry) if !ok { appState.StatusBar.SetText("自动登录失败: 密码控件类型错误") return } usernameEntry.SetText(user.LoginName) passwordEntry.SetText(user.LoginPass) // 触发登录 appState.StatusBar.SetText("正在自动登录...") appState.LoginForm.OnSubmit() } } // 修改后的异步加载店铺头像函数 func loadShopAvatar(img *canvas.Image, url string) { if url == "" { // 使用默认头像 fyne.DoAndWait(func() { img.Resource = fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "account") img.Refresh() }) return } // 创建HTTP客户端(可设置超时) client := &http.Client{ Timeout: 10 * time.Second, } resp, err := client.Get(url) if err != nil { log.Printf("加载头像失败: %v", err) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Printf("头像请求失败: %s", resp.Status) return } // 解码图片 imgData, _, err := image.Decode(resp.Body) if err != nil { log.Printf("解码头像失败: %v", err) return } fyne.DoAndWait(func() { img.Image = imgData img.Refresh() }) } // 修改后的 createShopListPanel 函数 func createShopListPanel(appState *AppState) fyne.CanvasObject { // 创建绑定数据 appState.ShopListBinding = binding.NewUntypedList() // 创建列表控件 - 使用自定义模板 list := widget.NewListWithData( appState.ShopListBinding, func() fyne.CanvasObject { // 创建包含头像和名称的水平容器 avatar := canvas.NewImageFromResource(nil) avatar.SetMinSize(fyne.NewSize(40, 40)) avatar.FillMode = canvas.ImageFillContain nameLabel := widget.NewLabel("") statusIcon := widget.NewIcon(nil) return container.NewHBox( avatar, container.NewVBox( nameLabel, ), layout.NewSpacer(), statusIcon, ) }, // 修改后的列表项更新函数 func(item binding.DataItem, obj fyne.CanvasObject) { // 安全类型断言 hbox, ok := obj.(*fyne.Container) if !ok { log.Println("错误:传入对象不是容器") return } // 检查容器结构是否如预期 if len(hbox.Objects) < 4 { log.Println("错误:容器子元素数量不足") return } // 获取头像组件(直接类型断言) avatar, ok := hbox.Objects[0].(*canvas.Image) if !ok { log.Println("错误:第一个子元素不是图像") return } // 获取名称标签(通过嵌套容器) nameContainer, ok := hbox.Objects[1].(*fyne.Container) if !ok || len(nameContainer.Objects) == 0 { log.Println("错误:名称容器无效") return } nameLabel, ok := nameContainer.Objects[0].(*widget.Label) if !ok { log.Println("错误:名称标签无效") return } // 获取状态图标 statusIcon, ok := hbox.Objects[3].(*widget.Icon) if !ok { log.Println("错误:状态图标无效") return } // 获取店铺数据 val, err := item.(binding.Untyped).Get() if err != nil { log.Printf("获取数据失败: %v", err) return } shop, ok := val.(ShopModel.Account) if !ok { log.Println("错误:数据类型不匹配") return } // 设置店铺名称 nameLabel.SetText(shop.AccountName) // 设置状态图标 if shop.CanLogin { statusIcon.SetResource(fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "success")) } else { statusIcon.SetResource(fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "error")) } // 异步加载头像(使用原有loadShopAvatar函数) go loadShopAvatar(avatar, shop.AccountAvatar) }, ) // 添加点击事件 list.OnSelected = func(id widget.ListItemID) { shop := appState.Shops[id] appState.StatusBar.SetText(fmt.Sprintf("加载 %s 的商品...", shop.AccountName)) go func() { // 加载商品数据 products, err := loadProductsForShop(shop) if err != nil { appState.StatusBar.SetText("加载商品失败: " + err.Error()) return } // 更新UI appState.StatusBar.SetText(fmt.Sprintf("已加载 %d 个商品", len(products))) addOrUpdateProductTab(appState, shop, products) }() } return container.NewBorder( widget.NewLabel("店铺列表"), nil, nil, nil, list, ) } // 更新店铺列表绑定数据 func updateShopListBinding(appState *AppState) { // 清空绑定数据 appState.ShopListBinding.Set(make([]interface{}, 0)) // 添加新数据 values := make([]interface{}, len(appState.Shops)) for i, shop := range appState.Shops { values[i] = shop } appState.ShopListBinding.Set(values) } // 为店铺加载商品数据 func loadProductsForShop(shop ShopModel.Account) ([]SkuModel.DataItem, error) { // 获取店铺的Cookie信息 // cookieInfo, found := CookieModel.Api_find_by_subject_id(shop.SubjectID) // if !found { // return nil, fmt.Errorf("未找到店铺的Cookie信息") // } // 模拟API调用获取商品数据 time.Sleep(1 * time.Second) // 模拟网络延迟 // 这里应该是实际的API调用 // products, err := SkuModel.GetSkuList(shop.SubjectID, cookieInfo.Token, cookieInfo.VerifyFp) // 模拟返回数据 return []SkuModel.DataItem{ {ProductID: "1001", Name: "商品A", MarketPrice: 999, DiscountPrice: 100}, {ProductID: "1002", Name: "商品B", MarketPrice: 1999, DiscountPrice: 50}, {ProductID: "1003", Name: "商品C", MarketPrice: 2999, DiscountPrice: 30}, }, nil } // 添加或更新商品TAB func addOrUpdateProductTab(appState *AppState, shop ShopModel.Account, products []SkuModel.DataItem) { tabTitle := shop.AccountName //fmt.Sprintf("%s (%d)", shop.AccountName, shop.AccountID) // 检查是否已存在该TAB for _, tab := range appState.ProductTabs.Items { if tab.Text == tabTitle { // 更新现有TAB tab.Content = createProductList(products) appState.ProductTabs.Refresh() return } } // 创建新TAB newTab := container.NewTabItem(tabTitle, createProductList(products)) appState.ProductTabs.Append(newTab) appState.ProductTabs.Select(newTab) } // 创建商品列表 func createProductList(products []SkuModel.DataItem) fyne.CanvasObject { // 创建表格 table := widget.NewTable( func() (int, int) { return len(products) + 1, 4 // 行数=商品数+表头,列数=4 }, func() fyne.CanvasObject { return widget.NewLabel("模板文本") }, func(id widget.TableCellID, cell fyne.CanvasObject) { label := cell.(*widget.Label) if id.Row == 0 { // 表头 switch id.Col { case 0: label.SetText("商品ID") case 1: label.SetText("商品名称") case 2: label.SetText("价格") case 3: label.SetText("库存") } label.TextStyle.Bold = true return } // 数据行 product := products[id.Row-1] switch id.Col { case 0: label.SetText(product.ProductID) case 1: label.SetText(product.Name) case 2: label.SetText(fmt.Sprintf("¥%.2f", product.MarketPrice/100)) case 3: label.SetText(fmt.Sprintf("%d", product.MarketPrice)) } }, ) // 设置列宽 table.SetColumnWidth(0, 100) table.SetColumnWidth(1, 300) table.SetColumnWidth(2, 100) table.SetColumnWidth(3, 100) return table } 提示的警告日志为: 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:341 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:341 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:341 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:341 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:341 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:405 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:405 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:405 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:405 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:405 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:405 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:405 2025/07/22 13:12:04 *** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] *** 2025/07/22 13:12:04 From: E:/GoProject/main.go:405 请帮修复这些错误或警告
07-23
package main import ( "crypto/md5" "errors" "fmt" "image" "image/color" "io" "log" "net/http" "os" "path/filepath" "runtime" "strconv" "strings" "sync" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" "github.com/playwright-community/playwright-go" "main.go/dataModel/CookieModel" "main.go/dataModel/ShopModel" "main.go/dataModel/SkuModel" "main.go/dataModel/UserModel" "main.go/res" "main.go/tuuz/database" ) // PlaywrightService 管理Playwright实例 type PlaywrightService struct { PW *playwright.Playwright Browser playwright.Browser Context playwright.BrowserContext Page playwright.Page } // 新增分页状态结构体 type PaginationState struct { CurrentPage int PageSize int TotalPages int TotalProducts int Products []SkuModel.DataItem } // 全局状态 type AppState struct { Window fyne.Window CurrentUser UserModel.UserInfo Shops []ShopModel.Account ProductTabs *container.AppTabs StatusBar *widget.Label ShopListBinding binding.UntypedList LoginForm *widget.Form LeftPanel *fyne.Container FilterFilePath string FilterKeywords []string ShopListPanel *fyne.Container FilterPanel *fyne.Container KeywordCount *widget.Label TabShopMap map[string]ShopModel.Account SplitContainer *container.Split TopPanel *fyne.Container ContentPanel *fyne.Container NeedsRefresh bool LastRefreshTime time.Time PaginationStates map[string]*PaginationState Playwright *PlaywrightService // Playwright服务 UrlEntry *widget.Entry // URL输入框 } // 添加状态检查快捷键 func addStateDebugShortcut(window fyne.Window, appState *AppState) { window.Canvas().SetOnTypedKey(func(ev *fyne.KeyEvent) { if ev.Name == fyne.KeyF5 { refreshLeftPanel(appState) appState.StatusBar.SetText("手动刷新UI") } else if ev.Name == fyne.KeyS { fmt.Println("===== 应用状态快照 =====") fmt.Printf("当前用户: %s\n", appState.CurrentUser.LoginName) fmt.Printf("店铺数量: %d\n", len(appState.Shops)) fmt.Printf("最后刷新时间: %s\n", appState.LastRefreshTime.Format("15:04:05.000")) fmt.Println("=======================") } }) } // 初始化Playwright服务 func initPlaywrightService() (*PlaywrightService, error) { pw, err := playwright.Run() if err != nil { return nil, fmt.Errorf("启动Playwright失败: %w", err) } browser, err := pw.Chromium.Launch(playwright.BrowserTypeLaunchOptions{ Headless: playwright.Bool(false), }) if err != nil { return nil, fmt.Errorf("启动浏览器失败: %w", err) } context, err := browser.NewContext() if err != nil { return nil, fmt.Errorf("创建上下文失败: %w", err) } page, err := context.NewPage() if err != nil { return nil, fmt.Errorf("创建页面失败: %w", err) } return &PlaywrightService{ PW: pw, Browser: browser, Context: context, Page: page, }, nil } func main() { os.Setenv("PLAYWRIGHT_BROWSERS_PATH", "./browsers") database.Init() UserModel.UserInit() ShopModel.ShopInit() CookieModel.CreateCookieInfoTable() SkuModel.ProductInit() // 创建缓存目录 if err := os.MkdirAll("cacheimg", 0755); err != nil { log.Printf("创建缓存目录失败: %v", err) } myApp := app.New() myWindow := myApp.NewWindow("店铺管理工具") myWindow.Resize(fyne.NewSize(1200, 800)) // 初始化Playwright服务 pwService, err := initPlaywrightService() if err != nil { log.Fatalf("初始化Playwright失败: %v", err) } defer func() { if err := pwService.Browser.Close(); err != nil { log.Printf("关闭浏览器失败: %v", err) } // 修复错误1: 使用正确的停止方法 if err := pwService.PW.Stop(); err != nil { log.Printf("停止Playwright失败: %v", err) } }() // 初始化应用状态 appState := &AppState{ FilterFilePath: getDefaultFilterPath(), TabShopMap: make(map[string]ShopModel.Account), LastRefreshTime: time.Now(), PaginationStates: make(map[string]*PaginationState), Playwright: pwService, // 注入Playwright服务 } // 注册调试快捷键 addStateDebugShortcut(myWindow, appState) // 启动状态监听器 startStateListener(appState) // 尝试加载默认过滤文件 go loadFilterFile(appState) // 创建状态栏 appState.StatusBar = widget.NewLabel("就绪") // 创建URL访问控件 appState.UrlEntry = widget.NewEntry() appState.UrlEntry.SetPlaceHolder("输入URL") visitButton := widget.NewButton("访问", func() { url := appState.UrlEntry.Text if url == "" { appState.StatusBar.SetText("请输入URL") return } appState.StatusBar.SetText(fmt.Sprintf("正在访问: %s...", url)) go func() { // 访问URL // 修复错误2和3: 使用正确的返回类型和状态码访问方式 response, err := visitUrlWithPlaywright(appState, url) if err != nil { fyne.DoAndWait(func() { appState.StatusBar.SetText(fmt.Sprintf("访问失败: %v", err)) }) return } fyne.DoAndWait(func() { // 使用Status()方法获取状态码 appState.StatusBar.SetText(fmt.Sprintf("访问完成! 状态码: %d", response.Status())) }) }() }) // 创建底部控制栏 bottomControlBar := container.NewBorder( nil, nil, nil, visitButton, appState.UrlEntry, ) // 创建底部区域(状态栏 + URL控件) bottomArea := container.NewVBox( bottomControlBar, widget.NewSeparator(), container.NewHBox(layout.NewSpacer(), appState.StatusBar), ) // 创建主布局 mainContent := createMainUI(myWindow, appState) // 设置整体布局 content := container.NewBorder( nil, // 顶部 bottomArea, // 底部(包含URL控件和状态栏) nil, // 左侧 nil, // 右侧 mainContent, ) myWindow.SetContent(content) // 启动时尝试自动登录 go tryAutoLogin(appState) myWindow.ShowAndRun() } // 使用Playwright访问URL并拦截响应 // 修复错误2和3: 使用接口类型作为返回类型 func visitUrlWithPlaywright(appState *AppState, url string) (playwright.Response, error) { // 设置响应拦截器 appState.Playwright.Page.OnResponse(func(response playwright.Response) { log.Printf("响应: %s - %d", response.URL(), response.Status()) }) // 导航到URL response, err := appState.Playwright.Page.Goto(url) if err != nil { return nil, fmt.Errorf("导航失败: %w", err) } // 等待页面加载完成 if err := appState.Playwright.Page.WaitForLoadState(playwright.PageWaitForLoadStateOptions{ State: playwright.LoadStateNetworkidle, }); err != nil { return nil, fmt.Errorf("等待页面加载失败: %w", err) } return response, nil } // 新增状态监听器 - 定期检查状态变化 func startStateListener(appState *AppState) { go func() { for { time.Sleep(100 * time.Millisecond) // 每100ms检查一次 if appState.NeedsRefresh { fyne.DoAndWait(func() { refreshLeftPanel(appState) appState.NeedsRefresh = false }) } } }() } // 获取默认过滤文件路径 func getDefaultFilterPath() string { if runtime.GOOS == "windows" { return filepath.Join("filter.txt") } return filepath.Join(os.Getenv("HOME"), "filter.txt") } // 修改 refreshAllProductTabs 函数 func refreshAllProductTabs(appState *AppState) { if appState.ProductTabs == nil || len(appState.ProductTabs.Items) == 0 { return } // 遍历所有标签页并刷新 for _, tab := range appState.ProductTabs.Items { // 通过标签页标题获取店铺 shop, exists := appState.TabShopMap[tab.Text] if !exists { continue } // 重新加载商品 go func(shop ShopModel.Account) { products, err := loadProductsForShop(shop, appState) if err != nil { fyne.DoAndWait(func() { appState.StatusBar.SetText(fmt.Sprintf("刷新 %s 商品失败: %s", shop.AccountName, err.Error())) }) return } fyne.DoAndWait(func() { // 更新标签页内容 tab.Content = container.NewMax(createProductTable(products)) appState.ProductTabs.Refresh() appState.StatusBar.SetText(fmt.Sprintf("已刷新 %s 的商品", shop.AccountName)) }) }(shop) } } // 加载过滤文件 func loadFilterFile(appState *AppState) { if appState.FilterFilePath == "" { log.Printf("加载本地过滤文件失败: %s", appState.FilterFilePath) return } if _, err := os.Stat(appState.FilterFilePath); os.IsNotExist(err) { err := os.WriteFile(appState.FilterFilePath, []byte{}, 0644) if err != nil { log.Printf("创建过滤文件失败: %v", err) } return } content, err := os.ReadFile(appState.FilterFilePath) if err != nil { log.Printf("读取过滤文件失败: %v", err) return } lines := strings.Split(string(content), "\n") appState.FilterKeywords = []string{} for _, line := range lines { trimmed := strings.TrimSpace(line) if trimmed != "" { appState.FilterKeywords = append(appState.FilterKeywords, trimmed) } } fyne.DoAndWait(func() { appState.StatusBar.SetText(fmt.Sprintf("已加载 %d 个过滤关键字", len(appState.FilterKeywords))) // 更新关键字数量标签 if appState.KeywordCount != nil { // 修正为 KeywordCount appState.KeywordCount.SetText(fmt.Sprintf("关键字数量: %d", len(appState.FilterKeywords))) } // 刷新所有已打开的商品标签页 refreshAllProductTabs(appState) }) } // 修改 createMainUI 函数 - 保存分割布局引用 func createMainUI(window fyne.Window, appState *AppState) fyne.CanvasObject { appState.Window = window // 创建整个左侧面板 leftPanel := createLeftPanel(window, appState) appState.LeftPanel = leftPanel // 右侧面板 appState.ProductTabs = container.NewAppTabs() appState.ProductTabs.SetTabLocation(container.TabLocationTop) rightPanel := container.NewBorder( widget.NewLabelWithStyle("商品信息", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), nil, nil, nil, container.NewMax(appState.ProductTabs), ) // 使用HSplit布局 - 保存引用 split := container.NewHSplit(leftPanel, rightPanel) split.SetOffset(0.25) appState.SplitContainer = split // 保存分割布局引用 return split } // 修改createFilterPanel函数 - 返回容器并保存引用 func createFilterPanel(appState *AppState) *fyne.Container { // 创建文件路径标签 pathLabel := widget.NewLabel("过滤文件: " + appState.FilterFilePath) pathLabel.Wrapping = fyne.TextWrapWord // 创建选择文件按钮 selectButton := widget.NewButton("选择过滤文件", func() { dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) { if err != nil { dialog.ShowError(err, appState.Window) return } if reader == nil { return // 用户取消 } // 更新文件路径 appState.FilterFilePath = reader.URI().Path() pathLabel.SetText("过滤文件: " + appState.FilterFilePath) // 加载过滤文件 go func() { loadFilterFile(appState) // 刷新所有已打开的商品标签页 refreshAllProductTabs(appState) }() }, appState.Window) }) // 创建刷新按钮 refreshButton := widget.NewButton("刷新过滤", func() { if appState.FilterFilePath != "" { appState.StatusBar.SetText("刷新过滤关键字...") go func() { loadFilterFile(appState) // 刷新所有已打开的商品标签页 refreshAllProductTabs(appState) }() } else { appState.StatusBar.SetText("请先选择过滤文件") } }) // 创建"增加商品"按钮 addProductsButton := widget.NewButton("增加商品", func() { if appState.ProductTabs.Selected() == nil { appState.StatusBar.SetText("请先选择一个店铺标签页") return } shopName := appState.ProductTabs.Selected().Text appState.StatusBar.SetText(fmt.Sprintf("为 %s 增加1000条商品...", shopName)) go func() { // 生成1000条模拟商品 newProducts := make([]SkuModel.DataItem, 1000) for i := 0; i < 1000; i++ { newProducts[i] = SkuModel.DataItem{ ProductID: fmt.Sprintf("ADD%04d", i+1), Name: fmt.Sprintf("%s - 新增商品%d", shopName, i+1), MarketPrice: (i + 1000) * 1000, // 从1000开始 DiscountPrice: (i + 1000) * 800, // 折扣价 Img: "https://p3-aio.ecombdimg.com/obj/ecom-shop-material/jpeg_m_c3042f069cc881202925e3ebecec509b_sx_285253_www790-1232", Pics: []string{ "https://p3-aio.ecombdimg.com/obj/ecom-shop-material/jpeg_m_ebf42d1ffd3990cb0d016e692d54061a_sx_303601_www790-1232", "https://p3-aio.ecombdimg.com/obj/ecom-shop-material/jpeg_m_20da83e457ae1a2c254d56eb058223d0_sx_200127_www750-611", "https://p3-aio.ecombdimg.com/obj/ecom-shop-material/jpeg_m_20da83e457ae1a2c254d56eb058223d0_sx_200127_www750-611", "https://p3-aio.ecombdimg.com/obj/ecom-shop-material/jpeg_m_b774f0f89ebf73ad6533b2d9481c8c12_sx_616294_www750-1599", "https://p3-aio.ecombdimg.com/obj/ecom-shop-material/jpeg_m_bb759148a1bfea0b8d04d53c2cbd9142_sx_289701_www790-1232", "https://p3-aio.ecombdimg.com/obj/ecom-shop-material/jpeg_m_b774f0f89ebf73ad6533b2d9481c8c12_sx_616294_www750-1599", "https://p3-aio.ecombdimg.com/obj/ecom-shop-material/jpeg_m_20da83e457ae1a2c254d56eb058223d0_sx_200127_www750-611", "https://p3-aio.ecombdimg.com/obj/ecom-shop-material/jpeg_m_96664bdd76ae61e0c92c00b1466e23c3_sx_499102_www750-1621", }, } } fyne.DoAndWait(func() { // 获取该店铺的TabState tabState, exists := appState.PaginationStates[shopName] if !exists { // 如果不存在,则创建一个新的TabState tabState = &PaginationState{ PageSize: 10, CurrentPage: 1, } appState.PaginationStates[shopName] = tabState } // 添加到现有商品列表 tabState.Products = append(tabState.Products, newProducts...) // 刷新当前标签页 refreshCurrentProductTab(appState, shopName, tabState.Products) appState.StatusBar.SetText(fmt.Sprintf("已为 %s 增加1000条商品,总数: %d", shopName, len(tabState.Products))) }) }() }) // 修改按钮容器,添加新按钮 buttonContainer := container.NewHBox( selectButton, refreshButton, addProductsButton, // 新增按钮 ) // 创建关键字计数标签 - 保存引用 keywordCount := widget.NewLabel(fmt.Sprintf("关键字数量: %d", len(appState.FilterKeywords))) keywordCount.TextStyle = fyne.TextStyle{Bold: true} appState.KeywordCount = keywordCount // 创建面板容器 panel := container.NewVBox( widget.NewSeparator(), widget.NewLabelWithStyle("商品过滤", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), pathLabel, keywordCount, buttonContainer, ) return panel } // 修改 createLoggedInPanel 函数 - 确保注销时直接刷新 func createLoggedInPanel(appState *AppState) fyne.CanvasObject { return container.NewVBox( widget.NewLabelWithStyle("登录状态", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), widget.NewSeparator(), container.NewHBox( widget.NewLabel("用户:"), widget.NewLabel(appState.CurrentUser.LoginName), ), container.NewHBox( widget.NewLabel("店铺数量:"), widget.NewLabel(fmt.Sprintf("%d", len(appState.Shops))), ), widget.NewSeparator(), container.NewCenter( widget.NewButton("注销", func() { // 重置状态 appState.CurrentUser = UserModel.UserInfo{} appState.Shops = nil appState.ProductTabs.Items = nil appState.ProductTabs.Refresh() appState.TabShopMap = make(map[string]ShopModel.Account) // 直接调用刷新函数 refreshLeftPanel(appState) appState.StatusBar.SetText("已注销") }), ), ) } // 重构创建顶部面板函数 - 确保状态正确反映 func createTopPanel(appState *AppState) *fyne.Container { // 添加调试日志 fmt.Printf("创建顶部面板: 登录状态=%t, 用户名=%s\n", appState.CurrentUser.LoginName != "", appState.CurrentUser.LoginName) var content fyne.CanvasObject if appState.CurrentUser.LoginName != "" { content = createLoggedInPanel(appState) } else { content = createLoginForm(appState) } return container.NewMax(content) } // 重构 createContentPanel 函数 - 添加详细日志 func createContentPanel(appState *AppState) *fyne.Container { // 添加详细调试日志 fmt.Printf("创建内容面板: 登录状态=%t, 用户名=%s, 店铺数量=%d\n", appState.CurrentUser.LoginName != "", appState.CurrentUser.LoginName, len(appState.Shops)) if appState.CurrentUser.LoginName != "" { if len(appState.Shops) > 0 { return createShopListPanel(appState) } return container.NewCenter( widget.NewLabel("没有可用的店铺"), ) } return container.NewCenter( widget.NewLabel("请先登录查看店铺列表"), ) } // 重构刷新函数 - 确保完全重建UI func refreshLeftPanel(appState *AppState) { if appState.SplitContainer == nil { return } // 添加详细调试信息 fmt.Printf("刷新左侧面板 - 时间: %s, 用户: %s, 店铺数量: %d\n", time.Now().Format("15:04:05.000"), appState.CurrentUser.LoginName, len(appState.Shops)) // 创建新的左侧面板 newLeftPanel := createLeftPanel(appState.Window, appState) // 添加调试背景色(登录状态不同颜色不同) var debugColor color.Color if appState.CurrentUser.LoginName != "" { debugColor = color.NRGBA{R: 0, G: 100, B: 0, A: 30} // 登录状态绿色半透明 } else { debugColor = color.NRGBA{R: 100, G: 0, B: 0, A: 30} // 未登录状态红色半透明 } debugPanel := container.NewMax( canvas.NewRectangle(debugColor), newLeftPanel, ) // 替换分割布局中的左侧面板 appState.SplitContainer.Leading = debugPanel appState.LeftPanel = debugPanel // 刷新分割布局 appState.SplitContainer.Refresh() // 强制重绘整个窗口 appState.Window.Content().Refresh() appState.LastRefreshTime = time.Now() } // 重构 createLeftPanel 函数 - 确保使用正确的状态 func createLeftPanel(window fyne.Window, appState *AppState) *fyne.Container { // 创建顶部面板(用户状态/登录表单) topPanel := createTopPanel(appState) // 创建内容面板(店铺列表或提示) contentPanel := createContentPanel(appState) // 创建过滤面板 filterPanel := createFilterPanel(appState) // 使用Border布局 return container.NewBorder( topPanel, // 顶部 filterPanel, // 底部 nil, nil, // 左右 contentPanel, // 中间内容 ) } // 修改登录按钮回调 - 确保状态正确更新 func createLoginForm(appState *AppState) fyne.CanvasObject { usernameEntry := widget.NewEntry() passwordEntry := widget.NewPasswordEntry() usernameEntry.PlaceHolder = "输入邮箱地址" passwordEntry.PlaceHolder = "输入密码" // 登录按钮回调 loginButton := widget.NewButton("登录", func() { appState.StatusBar.SetText("登录中...") go func() { // 模拟网络延迟 time.Sleep(500 * time.Millisecond) // 获取店铺信息 shops := ShopModel.Api_select_struct(nil) fyne.DoAndWait(func() { if len(shops) == 0 { appState.StatusBar.SetText("获取店铺信息为空") return } // 更新应用状态 appState.Shops = shops appState.CurrentUser, _ = UserModel.Api_find_by_username(usernameEntry.Text) // 更新店铺列表绑定 updateShopListBinding(appState) // 新增:更新绑定数据 // 添加状态更新日志 fmt.Printf("登录成功 - 用户: %s, 店铺数量: %d\n", appState.CurrentUser.LoginName, len(appState.Shops)) if appState.CurrentUser.LoginName == "" { appState.CurrentUser.LoginName = "1" } appState.StatusBar.SetText(fmt.Sprintf("登录成功! 共 %d 个店铺", len(shops))) // 直接刷新UI refreshLeftPanel(appState) }) }() }) form := widget.NewForm( widget.NewFormItem("邮箱:", usernameEntry), widget.NewFormItem("密码:", passwordEntry), ) appState.LoginForm = form return container.NewVBox( widget.NewLabelWithStyle("登录面板", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), form, container.NewCenter(loginButton), ) } // 修改自动登录函数 - 添加详细日志 func tryAutoLogin(appState *AppState) { // 获取所有用户 users := UserModel.Api_select_struct(nil) if len(users) == 0 { fyne.DoAndWait(func() { appState.StatusBar.SetText("获取已经存在的账号为空") }) return } // 尝试使用第一个用户自动登录 user := users[0] fyne.DoAndWait(func() { appState.StatusBar.SetText(fmt.Sprintf("尝试自动登录: %s...", user.LoginName)) }) // 获取用户名输入框 if appState.LoginForm == nil || len(appState.LoginForm.Items) < 2 { fyne.DoAndWait(func() { appState.StatusBar.SetText("自动登录失败: 登录表单尚未初始化") }) return } usernameItem := appState.LoginForm.Items[0] usernameEntry, ok := usernameItem.Widget.(*widget.Entry) if !ok { fyne.DoAndWait(func() { appState.StatusBar.SetText("自动登录失败: 用户名控件类型错误") }) return } passwordItem := appState.LoginForm.Items[1] passwordEntry, ok := passwordItem.Widget.(*widget.Entry) if !ok { fyne.DoAndWait(func() { appState.StatusBar.SetText("自动登录失败: 密码控件类型错误") }) return } // 触发登录 fyne.DoAndWait(func() { usernameEntry.SetText(user.LoginName) passwordEntry.SetText(user.LoginPass) appState.StatusBar.SetText("正在自动登录...") // 更新应用状态 appState.CurrentUser = user appState.Shops = ShopModel.Api_select_struct(nil) // 更新店铺列表绑定 updateShopListBinding(appState) // 新增 // 添加自动登录日志 fmt.Printf("自动登录成功 - 用户: %s, 店铺数量: %d\n", appState.CurrentUser.LoginName, len(appState.Shops)) // 直接刷新UI refreshLeftPanel(appState) }) } // 修改后的异步加载店铺头像函数 func loadShopAvatar(img *canvas.Image, url string) { if url == "" { // 使用默认头像 fyne.DoAndWait(func() { img.Resource = fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "account") img.Refresh() }) return } // 创建HTTP客户端(可设置超时) client := &http.Client{ Timeout: 10 * time.Second, } resp, err := client.Get(url) if err != nil { log.Printf("加载头像失败: %v", err) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Printf("头像请求失败: %s", resp.Status) return } // 解码图片 imgData, _, err := image.Decode(resp.Body) if err != nil { log.Printf("解码头像失败: %v", err) return } // 在主线程更新UI fyne.DoAndWait(func() { img.Image = imgData img.Refresh() }) } // 修改后的 createShopListPanel 函数 func createShopListPanel(appState *AppState) *fyne.Container { // 创建绑定数据 if appState.ShopListBinding == nil { appState.ShopListBinding = binding.NewUntypedList() } else { // 确保绑定数据是最新的 updateShopListBinding(appState) } // 创建列表控件 list := widget.NewListWithData( appState.ShopListBinding, func() fyne.CanvasObject { avatar := canvas.NewImageFromResource(nil) avatar.SetMinSize(fyne.NewSize(40, 40)) avatar.FillMode = canvas.ImageFillContain nameLabel := widget.NewLabel("") statusIcon := widget.NewIcon(nil) return container.NewHBox( avatar, container.NewVBox(nameLabel), layout.NewSpacer(), statusIcon, ) }, func(item binding.DataItem, obj fyne.CanvasObject) { hbox, ok := obj.(*fyne.Container) if !ok || len(hbox.Objects) < 4 { return } avatar, _ := hbox.Objects[0].(*canvas.Image) nameContainer, _ := hbox.Objects[1].(*fyne.Container) nameLabel, _ := nameContainer.Objects[0].(*widget.Label) statusIcon, _ := hbox.Objects[3].(*widget.Icon) val, err := item.(binding.Untyped).Get() if err != nil { return } shop, ok := val.(ShopModel.Account) if !ok { return } nameLabel.SetText(shop.AccountName) if shop.CanLogin { statusIcon.SetResource(res.ResShuffleSvg) } else { statusIcon.SetResource(fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "error")) } go loadShopAvatar(avatar, shop.AccountAvatar) }, ) list.OnSelected = func(id widget.ListItemID) { if id < 0 || id >= len(appState.Shops) { return } shop := appState.Shops[id] appState.StatusBar.SetText(fmt.Sprintf("加载 %s 的商品...", shop.AccountName)) go func() { products, err := loadProductsForShop(shop, appState) if err != nil { fyne.DoAndWait(func() { appState.StatusBar.SetText("加载商品失败: " + err.Error()) }) return } fyne.DoAndWait(func() { appState.StatusBar.SetText(fmt.Sprintf("已加载 %d 个商品", len(products))) addOrUpdateProductTab(appState, shop, products) }) }() } // 创建滚动容器 - 设置最小高度确保可滚动 scrollContainer := container.NewScroll(list) scrollContainer.SetMinSize(fyne.NewSize(280, 200)) // 最小高度200确保可滚动 // 使用Max容器确保填充空间 return container.NewMax( container.NewBorder( widget.NewLabel("店铺列表"), nil, nil, nil, scrollContainer, ), ) } // 更新店铺列表绑定数据 func updateShopListBinding(appState *AppState) { if appState.ShopListBinding == nil { appState.ShopListBinding = binding.NewUntypedList() } values := make([]interface{}, len(appState.Shops)) for i, shop := range appState.Shops { values[i] = shop } appState.ShopListBinding.Set(values) } // 应用商品过滤 func applyProductFilter(products []SkuModel.DataItem, keywords []string) []SkuModel.DataItem { if len(keywords) == 0 { return products // 没有关键字,返回所有商品 } filtered := []SkuModel.DataItem{} for _, product := range products { exclude := false for _, keyword := range keywords { if strings.Contains(strings.ToLower(product.Name), strings.ToLower(keyword)) { exclude = true break } } if !exclude { filtered = append(filtered, product) } } return filtered } // 修改 loadProductsForShop 函数,生成更多模拟数据 func loadProductsForShop(shop ShopModel.Account, appState *AppState) ([]SkuModel.DataItem, error) { // 模拟API调用获取商品数据 time.Sleep(500 * time.Millisecond) // 模拟网络延迟 // 生成100条模拟商品数据 products := make([]SkuModel.DataItem, 100) for i := 0; i < 100; i++ { products[i] = SkuModel.DataItem{ ProductID: fmt.Sprintf("SKU%04d", i+1), Name: fmt.Sprintf("%s - 商品%d", shop.AccountName, i+1), MarketPrice: i * 1000, DiscountPrice: i * 1000, Img: "https://p3-aio.ecombdimg.com/obj/ecom-shop-material/jpeg_m_c3042f069cc881202925e3ebecec509b_sx_285253_www790-1232", Pics: []string{ "https://p3-aio.ecombdimg.com/obj/ecom-shop-material/jpeg_m_ebf42d1ffd3990cb0d016e692d54061a_sx_303601_www790-1232", "https://p3-aio.ecombdimg.com/obj/ecom-shop-material/jpeg_m_20da83e457ae1a2c254d56eb058223d0_sx_200127_www750-611", "https://p3-aio.ecombdimg.com/obj/ecom-shop-material/jpeg_m_20da83e457ae1a2c254d56eb058223d0_sx_200127_www750-611", "https://p3-aio.ecombdimg.com/obj/ecom-shop-material/jpeg_m_b774f0f89ebf73ad6533b2d9481c8c12_sx_616294_www750-1599", "https://p3-aio.ecombdimg.com/obj/ecom-shop-material/jpeg_m_bb759148a1bfea0b8d04d53c2cbd9142_sx_289701_www790-1232", "https://p3-aio.ecombdimg.com/obj/ecom-shop-material/jpeg_m_b774f0f89ebf73ad6533b2d9481c8c12_sx_616294_www750-1599", "https://p3-aio.ecombdimg.com/obj/ecom-shop-material/jpeg_m_20da83e457ae1a2c254d56eb058223d0_sx_200127_www750-611", "https://p3-aio.ecombdimg.com/obj/ecom-shop-material/jpeg_m_96664bdd76ae61e0c92c00b1466e23c3_sx_499102_www750-1621", }, } } // 应用过滤 filteredProducts := applyProductFilter(products, appState.FilterKeywords) return filteredProducts, nil } // 修改 addOrUpdateProductTab 函数,添加分页支持 func addOrUpdateProductTab(appState *AppState, shop ShopModel.Account, products []SkuModel.DataItem) { tabTitle := shop.AccountName // 获取或创建分页状态 pagination, exists := appState.PaginationStates[tabTitle] if !exists { // 初始化分页状态 pagination = &PaginationState{ PageSize: 10, CurrentPage: 1, TotalProducts: len(products), } appState.PaginationStates[tabTitle] = pagination } else { // 更新商品总数 pagination.TotalProducts = len(products) } // 计算总页数 pagination.TotalPages = (pagination.TotalProducts + pagination.PageSize - 1) / pagination.PageSize if pagination.TotalPages == 0 { pagination.TotalPages = 1 } // 获取当前页数据 currentPageProducts := getCurrentPageProducts(pagination, products) // 检查是否已存在该TAB for _, tab := range appState.ProductTabs.Items { if tab.Text == tabTitle { // 修改调用,传入店铺名称 tab.Content = createProductListWithPagination(appState, currentPageProducts, tabTitle, products) // 更新映射 appState.TabShopMap[tabTitle] = shop appState.ProductTabs.Refresh() return } } // 创建新TAB newTab := container.NewTabItem( tabTitle, createProductListWithPagination(appState, currentPageProducts, tabTitle, products), ) // 添加到映射 appState.TabShopMap[tabTitle] = shop appState.ProductTabs.Append(newTab) appState.ProductTabs.Select(newTab) } // 修改 getCurrentPageProducts 函数 func getCurrentPageProducts(pagination *PaginationState, products []SkuModel.DataItem) []SkuModel.DataItem { start := (pagination.CurrentPage - 1) * pagination.PageSize if start >= len(products) { start = 0 } end := start + pagination.PageSize if end > len(products) { end = len(products) } return products[start:end] } // 修改 createProductListWithPagination 函数 func createProductListWithPagination(appState *AppState, currentPageProducts []SkuModel.DataItem, shopName string, allProducts []SkuModel.DataItem) fyne.CanvasObject { // 创建表格 table := createProductTable(currentPageProducts) // 创建分页控件 - 传入店铺名称 pagination := createPaginationControls(appState, shopName, allProducts) // 创建布局:表格在上,分页控件在下 return container.NewBorder(nil, pagination, nil, nil, table) } // 定义固定行高布局 type fixedHeightLayout struct { height float32 } func (f *fixedHeightLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { for _, o := range objects { o.Resize(fyne.NewSize(size.Width, f.height)) } } func (f *fixedHeightLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { return fyne.NewSize(0, f.height) } // 新增图片加载状态管理 type imageLoadState struct { loaded bool resource fyne.Resource } var ( imageCache = struct { sync.RWMutex m map[string]fyne.Resource }{m: make(map[string]fyne.Resource)} imageLoadStates = struct { sync.RWMutex m map[string]*imageLoadState }{m: make(map[string]*imageLoadState)} ) func createProductTable(products []SkuModel.DataItem) fyne.CanvasObject { // 创建表格 table := widget.NewTable( func() (int, int) { return len(products) + 1, 5 }, func() fyne.CanvasObject { hbox := container.NewHBox() return container.New(&fixedHeightLayout{height: 60}, hbox) }, func(id widget.TableCellID, cell fyne.CanvasObject) { fixedContainer := cell.(*fyne.Container) hbox := fixedContainer.Objects[0].(*fyne.Container) hbox.Objects = nil if id.Row == 0 { // 表头 switch id.Col { case 0: hbox.Add(widget.NewLabel("商品ID")) case 1: hbox.Add(widget.NewLabel("商品名称")) case 2: hbox.Add(widget.NewLabel("价格")) case 3: hbox.Add(widget.NewLabel("图片")) case 4: hbox.Add(widget.NewLabel("库存")) } return } if id.Row-1 >= len(products) { return } product := products[id.Row-1] switch id.Col { case 0: hbox.Add(widget.NewLabel(product.ProductID)) case 1: hbox.Add(widget.NewLabel(product.Name)) case 2: hbox.Add(widget.NewLabel(fmt.Sprintf("¥%.2f", float64(product.MarketPrice)/100))) case 3: // 图片列 maxDisplay := 4 if len(product.Pics) < maxDisplay { maxDisplay = len(product.Pics) } for i := 0; i < maxDisplay; i++ { if i >= len(product.Pics) { break } // 使用异步图片组件 img := NewAsyncImage(product.Pics[i]) hbox.Add(img) } case 4: hbox.Add(widget.NewLabel(fmt.Sprintf("%d", product.DiscountPrice))) } }, ) // 设置列宽 table.SetColumnWidth(0, 100) table.SetColumnWidth(1, 300) table.SetColumnWidth(2, 100) table.SetColumnWidth(3, 180) table.SetColumnWidth(4, 100) // 创建滚动容器 scrollContainer := container.NewScroll(table) scrollContainer.SetMinSize(fyne.NewSize(800, 400)) return scrollContainer } // 自定义优化表格 type optimizedTable struct { widget.BaseWidget products []SkuModel.DataItem table *widget.Table } func newOptimizedTable(products []SkuModel.DataItem) *optimizedTable { t := &optimizedTable{products: products} t.ExtendBaseWidget(t) return t } func (t *optimizedTable) CreateRenderer() fyne.WidgetRenderer { if t.table == nil { t.table = widget.NewTable( func() (int, int) { return len(t.products) + 1, 5 }, func() fyne.CanvasObject { hbox := container.NewHBox() return container.New(&fixedHeightLayout{height: 60}, hbox) }, t.updateCell, ) t.table.SetColumnWidth(0, 100) t.table.SetColumnWidth(1, 300) t.table.SetColumnWidth(2, 100) t.table.SetColumnWidth(3, 180) t.table.SetColumnWidth(4, 100) } return widget.NewSimpleRenderer(t.table) } func (t *optimizedTable) updateCell(id widget.TableCellID, cell fyne.CanvasObject) { fixedContainer := cell.(*fyne.Container) hbox := fixedContainer.Objects[0].(*fyne.Container) hbox.Objects = nil if id.Row == 0 { // 表头 switch id.Col { case 0: hbox.Add(widget.NewLabel("商品ID")) case 1: hbox.Add(widget.NewLabel("商品名称")) case 2: hbox.Add(widget.NewLabel("价格")) case 3: hbox.Add(widget.NewLabel("图片")) case 4: hbox.Add(widget.NewLabel("库存")) } return } if id.Row-1 >= len(t.products) { return } product := t.products[id.Row-1] switch id.Col { case 0: hbox.Add(widget.NewLabel(product.ProductID)) case 1: hbox.Add(widget.NewLabel(product.Name)) case 2: hbox.Add(widget.NewLabel(fmt.Sprintf("¥%.2f", float64(product.MarketPrice)/100))) case 3: // 图片列 maxDisplay := 4 if len(product.Pics) < maxDisplay { maxDisplay = len(product.Pics) } for i := 0; i < maxDisplay; i++ { if i >= len(product.Pics) { break } url := product.Pics[i] fileName := filepath.Join("cacheimg", getCacheFileName(url)) img := canvas.NewImageFromResource(nil) img.SetMinSize(fyne.NewSize(40, 40)) img.FillMode = canvas.ImageFillContain imageLoadStates.RLock() state, exists := imageLoadStates.m[fileName] imageLoadStates.RUnlock() if exists && state.loaded { img.Resource = state.resource } else { img.Resource = fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "question") go loadImageForCell(img, url) } hbox.Add(img) } case 4: hbox.Add(widget.NewLabel(fmt.Sprintf("%d", product.DiscountPrice))) } } // 优化图片加载函数 func loadImageForCell(img *canvas.Image, url string) { fileName := filepath.Join("cacheimg", getCacheFileName(url)) // 检查内存缓存 imageCache.RLock() cachedRes, exists := imageCache.m[fileName] imageCache.RUnlock() if exists { fyne.DoAndWait(func() { img.Resource = cachedRes img.Refresh() // 更新加载状态 imageLoadStates.Lock() imageLoadStates.m[fileName] = &imageLoadState{loaded: true, resource: cachedRes} imageLoadStates.Unlock() }) return } // 检查磁盘缓存 if _, err := os.Stat(fileName); err == nil { res := fyne.NewStaticResource(filepath.Base(fileName), readFile(fileName)) fyne.DoAndWait(func() { img.Resource = res img.Refresh() // 添加到内存缓存 imageCache.Lock() imageCache.m[fileName] = res imageCache.Unlock() // 更新加载状态 imageLoadStates.Lock() imageLoadStates.m[fileName] = &imageLoadState{loaded: true, resource: res} imageLoadStates.Unlock() }) return } // 异步下载图片 go func() { client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Get(url) if err != nil { log.Printf("下载图片失败: %v", err) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Printf("图片请求失败: %s", resp.Status) return } // 创建缓存文件 file, err := os.Create(fileName) if err != nil { log.Printf("创建缓存文件失败: %v", err) return } defer file.Close() // 保存图片 _, err = io.Copy(file, resp.Body) if err != nil { log.Printf("保存图片失败: %v", err) return } // 创建资源 res := fyne.NewStaticResource(filepath.Base(fileName), readFile(fileName)) fyne.DoAndWait(func() { img.Resource = res img.Refresh() // 添加到内存缓存 imageCache.Lock() imageCache.m[fileName] = res imageCache.Unlock() // 更新加载状态 imageLoadStates.Lock() imageLoadStates.m[fileName] = &imageLoadState{loaded: true, resource: res} imageLoadStates.Unlock() }) }() } // 修改 createPaginationControls 函数 func createPaginationControls(appState *AppState, shopName string, allProducts []SkuModel.DataItem) *fyne.Container { // 获取该店铺的分页状态 pagination, exists := appState.PaginationStates[shopName] if !exists { // 如果不存在,创建默认状态 pagination = &PaginationState{ PageSize: 10, CurrentPage: 1, TotalProducts: len(allProducts), TotalPages: (len(allProducts) + 9) / 10, } appState.PaginationStates[shopName] = pagination } // 使用闭包捕获当前店铺名称 refreshForShop := func() { refreshCurrentProductTab(appState, shopName, allProducts) } // 上一页按钮 prevBtn := widget.NewButton("上一页", func() { if pagination.CurrentPage > 1 { pagination.CurrentPage-- refreshForShop() } }) // 页码信息 pageInfo := widget.NewLabel(fmt.Sprintf("第 %d 页/共 %d 页", pagination.CurrentPage, pagination.TotalPages)) // 下一页按钮 nextBtn := widget.NewButton("下一页", func() { if pagination.CurrentPage < pagination.TotalPages { pagination.CurrentPage++ refreshForShop() } }) // 跳转输入框 jumpEntry := widget.NewEntry() jumpEntry.SetPlaceHolder("页码") jumpEntry.Validator = func(s string) error { _, err := strconv.Atoi(s) if err != nil { return errors.New("请输入数字") } return nil } jumpBtn := widget.NewButton("跳转", func() { page, err := strconv.Atoi(jumpEntry.Text) if err == nil && page >= 1 && page <= pagination.TotalPages { pagination.CurrentPage = page refreshForShop() } }) // 页面大小选择器 pageSizeSelect := widget.NewSelect([]string{"5", "10", "20", "50"}, nil) pageSizeSelect.SetSelected(fmt.Sprintf("%d", pagination.PageSize)) pageSizeSelect.OnChanged = func(value string) { size, _ := strconv.Atoi(value) pagination.PageSize = size pagination.CurrentPage = 1 // 重新计算总页数 pagination.TotalPages = (len(allProducts) + pagination.PageSize - 1) / pagination.PageSize if pagination.TotalPages == 0 { pagination.TotalPages = 1 } refreshForShop() } pageSizeLabel := widget.NewLabel("每页:") // 布局 return container.NewHBox( prevBtn, pageSizeLabel, pageSizeSelect, pageInfo, nextBtn, jumpEntry, jumpBtn, ) } func refreshCurrentProductTab(appState *AppState, shopName string, allProducts []SkuModel.DataItem) { currentTab := appState.ProductTabs.Selected() if currentTab == nil { return } // 获取该店铺的分页状态 pagination, exists := appState.PaginationStates[shopName] if !exists { pagination = &PaginationState{ PageSize: 10, CurrentPage: 1, TotalProducts: len(allProducts), } appState.PaginationStates[shopName] = pagination } // 更新商品总数 pagination.TotalProducts = len(allProducts) // 计算总页数 pagination.TotalPages = (pagination.TotalProducts + pagination.PageSize - 1) / pagination.PageSize if pagination.TotalPages == 0 { pagination.TotalPages = 1 } // 获取当前页数据 currentPageProducts := getCurrentPageProducts(pagination, allProducts) // 检查内容是否真的需要更新 currentContent := currentTab.Content if paginationContent, ok := currentContent.(*fyne.Container); ok { if len(paginationContent.Objects) > 0 { if tableContainer, ok := paginationContent.Objects[0].(*container.Scroll); ok { if existingTable, ok := tableContainer.Content.(*widget.Table); ok { // 获取表格的行数 rows, _ := existingTable.Length() // 如果行数相同,只刷新数据 if rows == len(currentPageProducts)+1 { // 使用温和刷新 - 只更新文本内容 refreshTableData(existingTable, currentPageProducts) appState.ProductTabs.Refresh() return } } } } } // 需要完全更新内容 currentTab.Content = createProductListWithPagination(appState, currentPageProducts, shopName, allProducts) appState.ProductTabs.Refresh() } // 温和刷新 - 只更新文本内容,不重建图片 func refreshTableData(table *widget.Table, products []SkuModel.DataItem) { table.Length = func() (int, int) { return len(products) + 1, 5 } // 只刷新文本列 table.UpdateCell = func(id widget.TableCellID, template fyne.CanvasObject) { fixedContainer := template.(*fyne.Container) hbox := fixedContainer.Objects[0].(*fyne.Container) if id.Row == 0 { return // 表头不变 } if id.Row-1 >= len(products) { return } product := products[id.Row-1] // 只更新文本列,保留图片列不变 switch id.Col { case 0, 1, 2, 4: // 清除旧的文本控件 var newObjects []fyne.CanvasObject for _, obj := range hbox.Objects { if _, isLabel := obj.(*widget.Label); !isLabel { newObjects = append(newObjects, obj) } } hbox.Objects = newObjects // 添加新的文本控件 switch id.Col { case 0: hbox.Add(widget.NewLabel(product.ProductID)) case 1: hbox.Add(widget.NewLabel(product.Name)) case 2: hbox.Add(widget.NewLabel(fmt.Sprintf("¥%.2f", float64(product.MarketPrice)/100))) case 4: hbox.Add(widget.NewLabel(fmt.Sprintf("%d", product.DiscountPrice))) } } } table.Refresh() } // 读取文件内容 func readFile(path string) []byte { data, err := os.ReadFile(path) if err != nil { log.Printf("读取缓存文件失败: %v", err) return []byte{} } return data } // 修改 loadProductImages 函数 - 添加缓存支持和并发控制 func loadProductImages(container *fyne.Container, urls []string, maxDisplay int) { // 使用工作池限制并发数 sem := make(chan struct{}, 4) // 最多4个并发下载 for i, url := range urls { if i >= maxDisplay { break } // 获取缓存文件名 fileName := filepath.Join("cacheimg", getCacheFileName(url)) // 检查内存缓存 imageCache.RLock() cachedRes, exists := imageCache.m[fileName] imageCache.RUnlock() if exists { fyne.DoAndWait(func() { if i < len(container.Objects) { img := canvas.NewImageFromResource(cachedRes) img.SetMinSize(fyne.NewSize(40, 40)) img.FillMode = canvas.ImageFillContain container.Objects[i] = img container.Refresh() } }) continue } // 检查磁盘缓存 if _, err := os.Stat(fileName); err == nil { // 从文件加载并缓存 fyne.DoAndWait(func() { if i < len(container.Objects) { res := fyne.NewStaticResource(filepath.Base(fileName), readFile(fileName)) img := canvas.NewImageFromResource(res) img.SetMinSize(fyne.NewSize(40, 40)) img.FillMode = canvas.ImageFillContain container.Objects[i] = img container.Refresh() // 添加到内存缓存 imageCache.Lock() imageCache.m[fileName] = res imageCache.Unlock() } }) continue } // 启动并发下载 sem <- struct{}{} go func(i int, url, fileName string) { defer func() { <-sem }() // 下载图片 client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Get(url) if err != nil { log.Printf("下载图片失败: %v", err) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Printf("图片请求失败: %s", resp.Status) return } // 创建缓存文件 file, err := os.Create(fileName) if err != nil { log.Printf("创建缓存文件失败: %v", err) return } defer file.Close() // 保存图片 _, err = io.Copy(file, resp.Body) if err != nil { log.Printf("保存图片失败: %v", err) return } // 从缓存文件创建资源 res := fyne.NewStaticResource(filepath.Base(fileName), readFile(fileName)) // 更新UI fyne.DoAndWait(func() { if i < len(container.Objects) { img := canvas.NewImageFromResource(res) img.SetMinSize(fyne.NewSize(40, 40)) img.FillMode = canvas.ImageFillContain container.Objects[i] = img container.Refresh() // 添加到内存缓存 imageCache.Lock() imageCache.m[fileName] = res imageCache.Unlock() } }) }(i, url, fileName) } } // 生成缓存文件名 func getCacheFileName(url string) string { // 使用URL的MD5作为文件名 hash := md5.Sum([]byte(url)) ext := filepath.Ext(url) if ext == "" { ext = ".jpg" // 默认使用jpg扩展名 } return fmt.Sprintf("%x%s", hash, ext) } // 图片加载服务 type ImageLoaderService struct { queue chan *ImageLoadTask cache map[string]fyne.Resource cacheMux sync.RWMutex } type ImageLoadTask struct { URL string Callback func(fyne.Resource) } // 创建图片加载服务 func NewImageLoaderService(workers int) *ImageLoaderService { service := &ImageLoaderService{ queue: make(chan *ImageLoadTask, 1000), cache: make(map[string]fyne.Resource), } // 启动工作池 for i := 0; i < workers; i++ { go service.worker() } return service } func (s *ImageLoaderService) worker() { client := &http.Client{ Timeout: 10 * time.Second, Transport: &http.Transport{ MaxIdleConns: 10, IdleConnTimeout: 30 * time.Second, DisableCompression: true, }, } for task := range s.queue { // 先检查缓存 s.cacheMux.RLock() cached, exists := s.cache[task.URL] s.cacheMux.RUnlock() if exists { task.Callback(cached) continue } // 下载图片 resp, err := client.Get(task.URL) if err != nil || resp.StatusCode != http.StatusOK { log.Printf("图片加载失败: %s, 错误: %v", task.URL, err) continue } // 创建资源 data, err := io.ReadAll(resp.Body) resp.Body.Close() if err != nil { continue } // 生成资源ID hash := md5.Sum([]byte(task.URL)) resourceID := fmt.Sprintf("img_%x", hash) res := fyne.NewStaticResource(resourceID, data) // 更新缓存 s.cacheMux.Lock() s.cache[task.URL] = res s.cacheMux.Unlock() // 执行回调 task.Callback(res) } } // 添加加载任务 func (s *ImageLoaderService) LoadImage(url string, callback func(fyne.Resource)) { task := &ImageLoadTask{ URL: url, Callback: callback, } s.queue <- task } // 全局图片加载服务 var imageLoader = NewImageLoaderService(5) // 5个工作线程 // 图片显示组件 type AsyncImage struct { widget.BaseWidget url string resource fyne.Resource image *canvas.Image } func NewAsyncImage(url string) *AsyncImage { img := &AsyncImage{ url: url, image: canvas.NewImageFromResource(nil), } img.image.SetMinSize(fyne.NewSize(40, 40)) img.image.FillMode = canvas.ImageFillContain // 设置占位符 img.image.Resource = fyne.Theme.Icon(fyne.CurrentApp().Settings().Theme(), "question") img.ExtendBaseWidget(img) // 启动异步加载 if url != "" { imageLoader.LoadImage(url, img.onImageLoaded) } return img } func (i *AsyncImage) onImageLoaded(res fyne.Resource) { // 只在资源确实加载完成时更新 if res != nil { i.resource = res fyne.DoAndWait(func() { i.image.Resource = res i.Refresh() }) } } func (i *AsyncImage) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(i.image) } 这个是修改后的代码,加载速度基本上满足要求,但是任然有BUG存在,1、在不特定行,图片显示在不同的列,其他列的数据也会显示异常。2、当前是“第X页/共Y页”的显示不更新,一直是“第1页”
最新发布
07-28
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值