feat(#142): group GoAuto admin navigation
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Architecture-and-Code-Map
|
||||
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Architecture-and-Code-Map.-
|
||||
wiki_revision: 07e1ba0c8ee38e7758cf142a272af95fdd046523
|
||||
synchronized_at: 2026-08-28T15:20:51Z
|
||||
wiki_revision: dc5ccd2104e8ded715c0c0bb23d3827dc9a7fd7a
|
||||
synchronized_at: 2026-08-29T01:39:35Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 架构与代码地图
|
||||
@@ -268,3 +268,12 @@ PddProductDetailCollector
|
||||
- `pdd_product_color_image` 保存图片路径、类型、字节数、宽高、来源任务与设备;来源任务继续关联其 `rule_snapshot`。同商品同颜色唯一,更新成功后删除被替换的受控目录旧文件。
|
||||
- 上传处理沿用既有 `/static/uploadfile` 静态能力,但使用独立 Agent 受控接口和 `goauto-color` 子目录,不复用公共上传入口。
|
||||
- 图片旁路独立于结构化结果状态机。Android 先安全提交结果,再上传图片;任何图片异常只写本地脱敏诊断,不触发失败提交或任务状态回滚。
|
||||
|
||||
|
||||
## Admin GoAuto 分组导航与权限(#142)
|
||||
|
||||
- `server/app/goauto/access/modules.go` 是 11 个 GoAuto 页面模块及两个一级菜单组的代码事实来源;本地迁移 `1787885600000_goauto_menu_groups.go` 创建结构性父菜单,并把既有页面菜单直接迁入父组。
|
||||
- 「采集采购」依次包含:SYB 商品、SYB 同步记录、档口入库码、虾皮商品、PDD 商品、采集任务、采购管理;「采采管理」依次包含:SYB 店铺、采集规则、设备列表、AI 规格匹配。
|
||||
- 迁移保留页面菜单 ID、路由、组件和 API 关联,移除旧的 11 个一级模块根菜单;导航固定为“分组 → 页面”两级,不增加第三级。
|
||||
- Admin 角色继续通过角色菜单查询取得全部 GoAuto 页面;采购员获得两个父组和除 AI 规格匹配外的 10 个页面,AI 页面及入口均不可见。
|
||||
- Web 继续通过 `/api/v1/menurole` 动态生成路由;直接访问组内页面时展开对应父组并高亮当前页面。
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Business-Rules-and-Glossary
|
||||
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Business-Rules-and-Glossary.-
|
||||
wiki_revision: 79bb0b8452c95ca34d3193f0178199614c997edd
|
||||
synchronized_at: 2026-08-28T15:21:02Z
|
||||
wiki_revision: add9c709daea958eb0db9e2f313e8ca5276072a4
|
||||
synchronized_at: 2026-08-29T01:39:44Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 业务规则与术语
|
||||
@@ -350,3 +350,11 @@ synchronized_at: 2026-08-28T15:21:02Z
|
||||
- 服务端只接受该任务已提交颜色维度中的 JPEG,校验 Device Token、任务归属、终态、内容、尺寸和数量;按“PDD 商品 + 颜色值”维护最新一张,记录来源 task、device,并沿任务关联不可变规则快照。
|
||||
- 只允许保存裁剪后的商品图片区域;裁剪结果不得包含账号、地址、订单、支付及其他个人数据。原始控件树、XML、整屏截图仍禁止保存。
|
||||
- 诊断阶段 `COLOR_IMAGE` 只记录成功及不支持、定位失败、裁剪失败、压缩失败、上传失败分类,不记录颜色文案、坐标或图片内容。
|
||||
|
||||
|
||||
## Admin 采集采购与采采管理导航(#142)
|
||||
|
||||
- 「采集采购」固定顺序为:SYB 商品、SYB 同步记录、档口入库码、虾皮商品、PDD 商品、采集任务、采购管理。
|
||||
- 「采采管理」固定顺序为:SYB 店铺、采集规则、设备列表、AI 规格匹配。
|
||||
- Admin 可见两个分组及全部 11 个页面;采购员可见两个分组和除 AI 规格匹配外的 10 个页面。父组只承担导航结构,不新增业务权限。
|
||||
- 分组迁移保留既有页面菜单 ID、URL、组件和 API 权限关联,删除旧的 11 个一级模块根菜单;菜单只保留“分组 → 页面”两级。
|
||||
|
||||
@@ -32,6 +32,57 @@ type ModuleDefinition struct {
|
||||
APIs []APIPermission
|
||||
}
|
||||
|
||||
const (
|
||||
MenuGroupCollectionPurchase = "collection_purchase"
|
||||
MenuGroupCollectionManagement = "collection_management"
|
||||
)
|
||||
|
||||
// MenuGroupDefinition describes the two structural navigation groups used by
|
||||
// Admin. ModuleKeys is ordered and therefore also defines child menu sorting.
|
||||
type MenuGroupDefinition struct {
|
||||
Key string
|
||||
MenuName string
|
||||
Title string
|
||||
Path string
|
||||
Icon string
|
||||
Sort int
|
||||
ModuleKeys []string
|
||||
}
|
||||
|
||||
var goAutoMenuGroupMetadata = []MenuGroupDefinition{
|
||||
{
|
||||
Key: MenuGroupCollectionPurchase,
|
||||
MenuName: "GoAutoCollectionPurchase",
|
||||
Title: "采集采购",
|
||||
Path: "/collection-purchase",
|
||||
Icon: "shopping",
|
||||
Sort: 50,
|
||||
ModuleKeys: []string{
|
||||
ModuleSYBProducts,
|
||||
ModuleSYBSyncRuns,
|
||||
ModuleSYBInnerCodes,
|
||||
ModuleShopeeProducts,
|
||||
ModulePDDProducts,
|
||||
ModuleCollectionTasks,
|
||||
ModulePurchaseTasks,
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: MenuGroupCollectionManagement,
|
||||
MenuName: "GoAutoCollectionManagement",
|
||||
Title: "采采管理",
|
||||
Path: "/collection-management",
|
||||
Icon: "system",
|
||||
Sort: 51,
|
||||
ModuleKeys: []string{
|
||||
ModuleSYBShops,
|
||||
ModuleCollectionRules,
|
||||
ModuleDevices,
|
||||
ModuleAIMatching,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var goAutoModuleMetadata = []ModuleDefinition{
|
||||
{Key: ModuleDevices, Title: "设备列表", Path: "/devices", RouteName: "GoAutoDeviceList", Component: "/goauto/devices/index", Icon: "monitor", Sort: 50, PurchaserDefault: true},
|
||||
{Key: ModulePDDProducts, Title: "PDD 商品", Path: "/pdd-products", RouteName: "GoAutoPddProducts", Component: "/goauto/pdd-products/index", Icon: "shopping", Sort: 51, PurchaserDefault: true},
|
||||
@@ -61,6 +112,17 @@ func GoAutoModules() []ModuleDefinition {
|
||||
return modules
|
||||
}
|
||||
|
||||
// GoAutoMenuGroups returns deep copies so callers cannot change global menu
|
||||
// ordering while building migrations or permission trees.
|
||||
func GoAutoMenuGroups() []MenuGroupDefinition {
|
||||
groups := make([]MenuGroupDefinition, len(goAutoMenuGroupMetadata))
|
||||
copy(groups, goAutoMenuGroupMetadata)
|
||||
for index := range groups {
|
||||
groups[index].ModuleKeys = append([]string(nil), goAutoMenuGroupMetadata[index].ModuleKeys...)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
func moduleKeyForAPI(path string) string {
|
||||
switch {
|
||||
case strings.HasPrefix(path, "/api/admin/v1/devices"):
|
||||
|
||||
@@ -61,6 +61,46 @@ func TestGoAutoMenuSortsFitMySQLSignedTinyInt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoAutoMenuGroupsCoverModulesExactlyOnce(t *testing.T) {
|
||||
groups := GoAutoMenuGroups()
|
||||
if len(groups) != 2 {
|
||||
t.Fatalf("got %d groups, want 2", len(groups))
|
||||
}
|
||||
if groups[0].Title != "采集采购" || groups[1].Title != "采采管理" {
|
||||
t.Fatalf("unexpected group titles: %#v", groups)
|
||||
}
|
||||
|
||||
wantOrder := [][]string{
|
||||
{ModuleSYBProducts, ModuleSYBSyncRuns, ModuleSYBInnerCodes, ModuleShopeeProducts, ModulePDDProducts, ModuleCollectionTasks, ModulePurchaseTasks},
|
||||
{ModuleSYBShops, ModuleCollectionRules, ModuleDevices, ModuleAIMatching},
|
||||
}
|
||||
seen := make(map[string]int)
|
||||
for groupIndex, group := range groups {
|
||||
if group.Sort < 0 || group.Sort > 127 {
|
||||
t.Fatalf("group %s sort %d exceeds MySQL signed TINYINT", group.Key, group.Sort)
|
||||
}
|
||||
if len(group.ModuleKeys) != len(wantOrder[groupIndex]) {
|
||||
t.Fatalf("group %s modules = %#v, want %#v", group.Key, group.ModuleKeys, wantOrder[groupIndex])
|
||||
}
|
||||
for index, key := range group.ModuleKeys {
|
||||
if key != wantOrder[groupIndex][index] {
|
||||
t.Fatalf("group %s module %d = %s, want %s", group.Key, index, key, wantOrder[groupIndex][index])
|
||||
}
|
||||
seen[key]++
|
||||
}
|
||||
}
|
||||
for _, module := range GoAutoModules() {
|
||||
if seen[module.Key] != 1 {
|
||||
t.Fatalf("module %s belongs to %d groups, want exactly one", module.Key, seen[module.Key])
|
||||
}
|
||||
}
|
||||
|
||||
groups[0].ModuleKeys[0] = "mutated"
|
||||
if GoAutoMenuGroups()[0].ModuleKeys[0] != ModuleSYBProducts {
|
||||
t.Fatal("GoAutoMenuGroups must return deep copies")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoAutoRoutesAreGeneratedFromServerMenus(t *testing.T) {
|
||||
_, fileName, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package version_local
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"go-admin/app/goauto/access"
|
||||
"go-admin/cmd/migrate/migration"
|
||||
migrationmodels "go-admin/cmd/migrate/migration/models"
|
||||
common "go-admin/common/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
_, fileName, _, _ := runtime.Caller(0)
|
||||
migration.Migrate.SetVersion(migration.GetFilename(fileName), migrateGoAutoMenuGroups)
|
||||
}
|
||||
|
||||
func migrateGoAutoMenuGroups(db *gorm.DB, version string) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := ensureGoAutoMenuGroups(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&common.Migration{Version: version}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func ensureGoAutoMenuGroups(db *gorm.DB) error {
|
||||
modules := make(map[string]access.ModuleDefinition)
|
||||
for _, module := range access.GoAutoModules() {
|
||||
modules[module.Key] = module
|
||||
}
|
||||
|
||||
var purchaser migrationmodels.SysRole
|
||||
if err := db.Where("role_key = ?", access.RolePurchaser).First(&purchaser).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allowedPurchaserMenus := make([]migrationmodels.SysMenu, 0, len(access.GoAutoModules())+len(access.GoAutoMenuGroups()))
|
||||
for _, group := range access.GoAutoMenuGroups() {
|
||||
groupMenu, _, err := upsertGoAutoMenu(db, migrationmodels.SysMenu{
|
||||
MenuName: group.MenuName,
|
||||
Title: group.Title,
|
||||
Icon: group.Icon,
|
||||
Path: group.Path,
|
||||
MenuType: "M",
|
||||
Action: "无",
|
||||
ParentId: 0,
|
||||
Component: "Layout",
|
||||
Sort: group.Sort,
|
||||
Visible: "0",
|
||||
IsFrame: "1",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.Model(&groupMenu).Update("paths", fmt.Sprintf("/0/%d", groupMenu.MenuId)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
allowedPurchaserMenus = append(allowedPurchaserMenus, groupMenu)
|
||||
|
||||
for childIndex, moduleKey := range group.ModuleKeys {
|
||||
module, ok := modules[moduleKey]
|
||||
if !ok {
|
||||
return fmt.Errorf("GoAuto menu group %s references unknown module %s", group.Key, moduleKey)
|
||||
}
|
||||
var child migrationmodels.SysMenu
|
||||
if err := db.Unscoped().Where("menu_name = ?", module.RouteName).First(&child).Error; err != nil {
|
||||
return fmt.Errorf("find GoAuto child menu %s: %w", module.RouteName, err)
|
||||
}
|
||||
updates := map[string]any{
|
||||
"parent_id": groupMenu.MenuId,
|
||||
"paths": fmt.Sprintf("/0/%d/%d", groupMenu.MenuId, child.MenuId),
|
||||
"sort": childIndex + 1,
|
||||
"deleted_at": nil,
|
||||
}
|
||||
if err := db.Unscoped().Model(&child).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if module.PurchaserHardHidden {
|
||||
if err := db.Model(&purchaser).Association("SysMenu").Delete(&child); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if module.PurchaserDefault {
|
||||
allowedPurchaserMenus = append(allowedPurchaserMenus, child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.Model(&purchaser).Association("SysMenu").Append(allowedPurchaserMenus); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldRootNames := make([]string, 0, len(access.GoAutoModules()))
|
||||
for _, module := range access.GoAutoModules() {
|
||||
oldRootNames = append(oldRootNames, module.RouteName+"Root")
|
||||
}
|
||||
var oldRoots []migrationmodels.SysMenu
|
||||
if err := db.Unscoped().Where("menu_name IN ?", oldRootNames).Find(&oldRoots).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(oldRoots) == 0 {
|
||||
return nil
|
||||
}
|
||||
oldRootIDs := make([]int, 0, len(oldRoots))
|
||||
for _, root := range oldRoots {
|
||||
oldRootIDs = append(oldRootIDs, root.MenuId)
|
||||
}
|
||||
if err := db.Table("sys_role_menu").Where("menu_id IN ?", oldRootIDs).Delete(nil).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.Table("sys_menu_api_rule").Where("sys_menu_menu_id IN ?", oldRootIDs).Delete(nil).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Unscoped().Where("menu_id IN ?", oldRootIDs).Delete(&migrationmodels.SysMenu{}).Error
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package version_local
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"go-admin/app/goauto/access"
|
||||
migrationmodels "go-admin/cmd/migrate/migration/models"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestEnsureGoAutoMenuGroups(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = db.AutoMigrate(
|
||||
&migrationmodels.SysRole{}, &migrationmodels.SysMenu{}, &migrationmodels.SysApi{},
|
||||
&purchaserCasbinRule{},
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
custom := migrationmodels.SysMenu{MenuName: "UserCustomMenu", Title: "用户自定义菜单", Path: "/custom"}
|
||||
if err = db.Create(&custom).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = ensureGoAutoMenusAndPurchaserBindings(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
childIDs := make(map[string]int)
|
||||
for _, module := range access.GoAutoModules() {
|
||||
var child migrationmodels.SysMenu
|
||||
if err = db.Where("menu_name = ?", module.RouteName).First(&child).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
childIDs[module.RouteName] = child.MenuId
|
||||
}
|
||||
|
||||
if err = ensureGoAutoMenuGroups(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertGoAutoGroupedMenuState(t, db, childIDs)
|
||||
if err = ensureGoAutoMenuGroups(db); err != nil {
|
||||
t.Fatalf("group reconciliation must be repeatable: %v", err)
|
||||
}
|
||||
assertGoAutoGroupedMenuState(t, db, childIDs)
|
||||
|
||||
var customAfter migrationmodels.SysMenu
|
||||
if err = db.First(&customAfter, custom.MenuId).Error; err != nil {
|
||||
t.Fatal("migration must not remove a user menu:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertGoAutoGroupedMenuState(t *testing.T, db *gorm.DB, childIDs map[string]int) {
|
||||
t.Helper()
|
||||
modules := make(map[string]access.ModuleDefinition)
|
||||
for _, module := range access.GoAutoModules() {
|
||||
modules[module.Key] = module
|
||||
var oldRootCount int64
|
||||
if err := db.Unscoped().Model(&migrationmodels.SysMenu{}).
|
||||
Where("menu_name = ?", module.RouteName+"Root").Count(&oldRootCount).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if oldRootCount != 0 {
|
||||
t.Fatalf("old root %s still exists", module.RouteName+"Root")
|
||||
}
|
||||
}
|
||||
|
||||
var purchaser migrationmodels.SysRole
|
||||
if err := db.Where("role_key = ?", access.RolePurchaser).First(&purchaser).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, group := range access.GoAutoMenuGroups() {
|
||||
var parent migrationmodels.SysMenu
|
||||
if err := db.Where("menu_name = ?", group.MenuName).First(&parent).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if parent.ParentId != 0 || parent.Title != group.Title || parent.Path != group.Path || parent.Paths != fmt.Sprintf("/0/%d", parent.MenuId) {
|
||||
t.Fatalf("unexpected group menu: %#v", parent)
|
||||
}
|
||||
assertRoleHasMenu(t, db, purchaser.RoleId, parent.MenuId, true)
|
||||
|
||||
for index, moduleKey := range group.ModuleKeys {
|
||||
module := modules[moduleKey]
|
||||
var child migrationmodels.SysMenu
|
||||
if err := db.Preload("SysApi").Where("menu_name = ?", module.RouteName).First(&child).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if child.MenuId != childIDs[module.RouteName] {
|
||||
t.Fatalf("child %s ID changed from %d to %d", module.RouteName, childIDs[module.RouteName], child.MenuId)
|
||||
}
|
||||
if child.ParentId != parent.MenuId || child.Paths != fmt.Sprintf("/0/%d/%d", parent.MenuId, child.MenuId) || child.Sort != index+1 {
|
||||
t.Fatalf("unexpected grouped child %s: %#v", module.RouteName, child)
|
||||
}
|
||||
if child.Path != module.Path+"/index" || child.Component != module.Component || len(child.SysApi) != len(module.APIs) {
|
||||
t.Fatalf("route or API association changed for %s: %#v", module.RouteName, child)
|
||||
}
|
||||
assertRoleHasMenu(t, db, purchaser.RoleId, child.MenuId, !module.PurchaserHardHidden)
|
||||
}
|
||||
}
|
||||
|
||||
var goAutoCount int64
|
||||
if err := db.Model(&migrationmodels.SysMenu{}).Where("menu_name LIKE ?", "GoAuto%").Count(&goAutoCount).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := int64(len(access.GoAutoModules()) + len(access.GoAutoMenuGroups())); goAutoCount != want {
|
||||
t.Fatalf("got %d GoAuto rows, want %d", goAutoCount, want)
|
||||
}
|
||||
}
|
||||
@@ -14,26 +14,52 @@ const modules = [
|
||||
['/ai-matching-settings', '/goauto/ai-matching-settings/index', 'GoAutoAiMatchingSettings', 'AI 规格匹配', 'setting']
|
||||
] as const
|
||||
|
||||
function menu(module: typeof modules[number]) {
|
||||
const menuGroups = [
|
||||
{
|
||||
path: '/collection-purchase',
|
||||
name: 'GoAutoCollectionPurchase',
|
||||
title: '采集采购',
|
||||
icon: 'shopping',
|
||||
modules: [modules[3], modules[5], modules[6], modules[2], modules[1], modules[8], modules[9]]
|
||||
},
|
||||
{
|
||||
path: '/collection-management',
|
||||
name: 'GoAutoCollectionManagement',
|
||||
title: '采采管理',
|
||||
icon: 'system',
|
||||
modules: [modules[4], modules[7], modules[0], modules[10]]
|
||||
}
|
||||
] as const
|
||||
|
||||
function childMenu(module: typeof modules[number]) {
|
||||
const [path, component, name, title, icon] = module
|
||||
return {
|
||||
path,
|
||||
component: 'Layout',
|
||||
path: `${path}/index`,
|
||||
component,
|
||||
visible: '0',
|
||||
menuName: `${name}Root`,
|
||||
menuName: name,
|
||||
title,
|
||||
icon,
|
||||
children: [{
|
||||
path: 'index',
|
||||
component,
|
||||
visible: '0',
|
||||
menuName: name,
|
||||
title,
|
||||
icon
|
||||
}]
|
||||
icon
|
||||
}
|
||||
}
|
||||
|
||||
function groupedMenus(visibleModules: typeof modules[number][]) {
|
||||
const allowed = new Set(visibleModules.map(module => module[2]))
|
||||
return menuGroups.flatMap(group => {
|
||||
const children = group.modules.filter(module => allowed.has(module[2])).map(childMenu)
|
||||
if (children.length === 0) return []
|
||||
return [{
|
||||
path: group.path,
|
||||
component: 'Layout',
|
||||
visible: '0',
|
||||
menuName: group.name,
|
||||
title: group.title,
|
||||
icon: group.icon,
|
||||
children
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
async function mockSession(page: any, role: 'admin' | 'purchaser', visibleModules: typeof modules[number][]) {
|
||||
await page.context().addCookies([{
|
||||
name: 'Admin-Token',
|
||||
@@ -48,28 +74,37 @@ async function mockSession(page: any, role: 'admin' | 'purchaser', visibleModule
|
||||
return route.fulfill({ json: { code: 200, data: { roles: [role], name: role, avatar: '', introduction: '', permissions: [] } } })
|
||||
}
|
||||
if (url.pathname.endsWith('/api/v1/menurole')) {
|
||||
return route.fulfill({ json: { code: 200, data: visibleModules.map(menu) } })
|
||||
return route.fulfill({ json: { code: 200, data: groupedMenus(visibleModules) } })
|
||||
}
|
||||
return route.fulfill({ json: { code: 200, data: [] } })
|
||||
})
|
||||
}
|
||||
|
||||
test('管理员菜单由服务端返回并包含全部 GoAuto 模块', async ({ page }) => {
|
||||
test('管理员深链接展开所属分组并保留全部 GoAuto 页面', async ({ page }) => {
|
||||
await mockSession(page, 'admin', [...modules])
|
||||
await page.goto('/#/dashboard')
|
||||
await page.goto('/#/pdd-products/index')
|
||||
|
||||
for (const module of modules) {
|
||||
await expect(page.locator('.sidebar-container').getByText(module[3], { exact: true }).first()).toBeVisible()
|
||||
}
|
||||
await expect(page).toHaveURL(/#\/pdd-products\/index$/)
|
||||
await expect(page.locator('.sidebar-container').getByText('采集采购', { exact: true }).first()).toBeVisible()
|
||||
await expect(page.locator('.sidebar-container').getByText('采采管理', { exact: true }).first()).toBeVisible()
|
||||
await expect(page.locator('.sidebar-container').getByText('PDD 商品', { exact: true }).first()).toBeVisible()
|
||||
await expect(page.locator('.sidebar-container .is-active').getByText('PDD 商品', { exact: true })).toBeVisible()
|
||||
|
||||
await page.goto('/#/ai-matching-settings/index')
|
||||
await expect(page).toHaveURL(/#\/ai-matching-settings\/index$/)
|
||||
await expect(page.locator('.sidebar-container').getByText('AI 规格匹配', { exact: true }).first()).toBeVisible()
|
||||
await expect(page.locator('.sidebar-container .is-active').getByText('AI 规格匹配', { exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
test('采购员只看到授权菜单且不能通过地址访问 AI 配置', async ({ page }) => {
|
||||
const purchaserModules = [modules[4], modules[9]]
|
||||
const purchaserModules = modules.filter(module => module[2] !== 'GoAutoAiMatchingSettings')
|
||||
await mockSession(page, 'purchaser', purchaserModules)
|
||||
await page.goto('/#/dashboard')
|
||||
await page.goto('/#/devices/index')
|
||||
|
||||
await expect(page.locator('.sidebar-container').getByText('采集采购', { exact: true }).first()).toBeVisible()
|
||||
await expect(page.locator('.sidebar-container').getByText('采采管理', { exact: true }).first()).toBeVisible()
|
||||
await expect(page.locator('.sidebar-container').getByText('SYB 店铺', { exact: true }).first()).toBeVisible()
|
||||
await expect(page.locator('.sidebar-container').getByText('采购管理', { exact: true }).first()).toBeVisible()
|
||||
await expect(page.locator('.sidebar-container').getByText('设备列表', { exact: true }).first()).toBeVisible()
|
||||
await expect(page.locator('.sidebar-container').getByText('AI 规格匹配', { exact: true })).toHaveCount(0)
|
||||
|
||||
await page.goto('/#/ai-matching-settings/index')
|
||||
|
||||
Reference in New Issue
Block a user