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页”
最新发布