diff --git a/admin-ui/src/api/admin/sys-api.js b/admin-ui/src/api/admin/sys-api.js index 1d4ecc9..7829c59 100644 --- a/admin-ui/src/api/admin/sys-api.js +++ b/admin-ui/src/api/admin/sys-api.js @@ -17,30 +17,3 @@ export function getSysApi(id) { }) } -// 新增SysApi -export function addSysApi(data) { - return request({ - url: '/api/v1/sys-api', - method: 'post', - data: data - }) -} - -// 修改SysApi -export function updateSysApi(data) { - return request({ - url: '/api/v1/sys-api/' + data.id, - method: 'put', - data: data - }) -} - -// 删除SysApi -export function delSysApi(data) { - return request({ - url: '/api/v1/sys-api', - method: 'delete', - data: data - }) -} - diff --git a/admin-ui/src/api/admin/sys-login-log.js b/admin-ui/src/api/admin/sys-login-log.js index e2f417e..d2a9922 100644 --- a/admin-ui/src/api/admin/sys-login-log.js +++ b/admin-ui/src/api/admin/sys-login-log.js @@ -9,20 +9,3 @@ export function listSysLoginlog(query) { }) } -// 查询SysLoginlog详细 -export function getSysLoginlog(ID) { - return request({ - url: '/api/v1/sys-login-log/' + ID, - method: 'get' - }) -} - -// 删除SysLoginlog -export function delSysLoginlog(data) { - return request({ - url: '/api/v1/sys-login-log', - method: 'delete', - data: data - }) -} - diff --git a/admin-ui/src/api/admin/sys-menu.js b/admin-ui/src/api/admin/sys-menu.js index fd544c5..63a10a2 100644 --- a/admin-ui/src/api/admin/sys-menu.js +++ b/admin-ui/src/api/admin/sys-menu.js @@ -32,30 +32,3 @@ export function roleMenuTreeselect(roleId) { method: 'get' }) } - -// 新增菜单 -export function addMenu(data) { - return request({ - url: '/api/v1/menu', - method: 'post', - data: data - }) -} - -// 修改菜单 -export function updateMenu(data, id) { - return request({ - url: '/api/v1/menu/' + id, - method: 'put', - data: data - }) -} - -// 删除菜单 -export function delMenu(data) { - return request({ - url: '/api/v1/menu', - method: 'delete', - data: data - }) -} diff --git a/admin-ui/src/store/getters.js b/admin-ui/src/store/getters.js index f7b1531..25ae08d 100644 --- a/admin-ui/src/store/getters.js +++ b/admin-ui/src/store/getters.js @@ -7,6 +7,7 @@ const getters = { token: state => state.user.token, avatar: state => state.user.avatar, name: state => state.user.name, + userId: state => state.user.userId, introduction: state => state.user.introduction, roles: state => state.user.roles, permisaction: state => state.user.permisaction, diff --git a/admin-ui/src/store/modules/user.js b/admin-ui/src/store/modules/user.js index 0811a8e..d8fa2c8 100644 --- a/admin-ui/src/store/modules/user.js +++ b/admin-ui/src/store/modules/user.js @@ -6,6 +6,7 @@ import storage from '@/utils/storage' const state = { token: getToken(), name: '', + userId: undefined, avatar: '', introduction: '', roles: [], @@ -23,6 +24,9 @@ const mutations = { SET_NAME: (state, name) => { state.name = name }, + SET_USER_ID: (state, userId) => { + state.userId = userId + }, SET_AVATAR: (state, avatar) => { if (avatar.indexOf('http') !== -1) { state.avatar = avatar @@ -63,7 +67,7 @@ const actions = { return resolve() } - const { roles, name, avatar, introduction, permissions } = response.data + const { roles, name, avatar, introduction, permissions, userId } = response.data // roles must be a non-empty array if (!roles || roles.length <= 0) { @@ -72,6 +76,7 @@ const actions = { commit('SET_PERMISSIONS', permissions) commit('SET_ROLES', roles) commit('SET_NAME', name) + commit('SET_USER_ID', userId) commit('SET_AVATAR', avatar) commit('SET_INTRODUCTION', introduction) resolve(response) @@ -85,6 +90,7 @@ const actions = { return new Promise((resolve, reject) => { logout(state.token).then(() => { commit('SET_TOKEN', '') + commit('SET_USER_ID', undefined) commit('SET_ROLES', []) commit('SET_PERMISSIONS', []) removeToken() @@ -113,6 +119,7 @@ const actions = { resetToken({ commit }) { return new Promise(resolve => { commit('SET_TOKEN', '') + commit('SET_USER_ID', undefined) removeToken() resolve() }) diff --git a/admin-ui/src/views/admin/sys-api/index.vue b/admin-ui/src/views/admin/sys-api/index.vue index 9a8e243..9bd6fc4 100644 --- a/admin-ui/src/views/admin/sys-api/index.vue +++ b/admin-ui/src/views/admin/sys-api/index.vue @@ -1,426 +1,62 @@ - + + diff --git a/admin-ui/src/views/admin/sys-login-log/index.vue b/admin-ui/src/views/admin/sys-login-log/index.vue index 83cd990..3bbbb93 100644 --- a/admin-ui/src/views/admin/sys-login-log/index.vue +++ b/admin-ui/src/views/admin/sys-login-log/index.vue @@ -1,287 +1,62 @@ - + + diff --git a/admin-ui/src/views/admin/sys-menu/index.vue b/admin-ui/src/views/admin/sys-menu/index.vue index f7e241a..f2c0b3b 100644 --- a/admin-ui/src/views/admin/sys-menu/index.vue +++ b/admin-ui/src/views/admin/sys-menu/index.vue @@ -1,634 +1,61 @@ - diff --git a/admin-ui/src/views/admin/sys-role/index.vue b/admin-ui/src/views/admin/sys-role/index.vue index 83dc8cd..20c2904 100644 --- a/admin-ui/src/views/admin/sys-role/index.vue +++ b/admin-ui/src/views/admin/sys-role/index.vue @@ -1,184 +1,35 @@ + + diff --git a/admin-ui/src/views/admin/sys-user/index.vue b/admin-ui/src/views/admin/sys-user/index.vue index c847386..879c7f9 100644 --- a/admin-ui/src/views/admin/sys-user/index.vue +++ b/admin-ui/src/views/admin/sys-user/index.vue @@ -1,683 +1,137 @@ + + diff --git a/admin-ui/tests/e2e/system-management.spec.ts b/admin-ui/tests/e2e/system-management.spec.ts new file mode 100644 index 0000000..6a3be20 --- /dev/null +++ b/admin-ui/tests/e2e/system-management.spec.ts @@ -0,0 +1,92 @@ +import { expect, test } from '@playwright/test' + +const baseURL = process.env.ADMIN_UI_URL || 'http://127.0.0.1:9528' + +const group = (title: string, path: string, children: object[]) => ({ + menuName: path.split('/').pop(), + title, + icon: 'ri:settings-3-line', + path, + component: 'Layout', + visible: '0', + children +}) + +const page = (title: string, path: string, component: string) => ({ + menuName: path.split('/').pop(), title, path, component, visible: '0', children: [] +}) + +const navigation = [ + group('生成配置', '/chorus/configuration', [ + page('上游服务商', '/chorus/providers', 'chorus/providers/index'), + page('模型配置', '/chorus/models', 'chorus/models/index'), + page('路由策略', '/chorus/routes', 'chorus/routes/index'), + page('提示词模板', '/chorus/templates', 'chorus/templates/index') + ]), + group('运行监控', '/chorus/monitoring', [ + page('上游健康', '/chorus/health', 'chorus/health/index'), + page('生成记录', '/chorus/generations', 'chorus/generations/index') + ]), + group('用户与访问', '/chorus/access', [ + page('终端用户', '/chorus/users', 'chorus/users/index'), + page('API 密钥', '/chorus/api-keys', 'chorus/api-keys/index') + ]), + group('系统管理', '/chorus/system', [ + page('管理员账号', '/chorus/system/admins', 'admin/sys-user/index'), + page('角色权限', '/chorus/system/roles', 'admin/sys-role/index'), + page('菜单结构', '/chorus/system/menus', 'admin/sys-menu/index'), + page('接口清单', '/chorus/system/apis', 'admin/sys-api/index'), + page('登录日志', '/chorus/system/login-logs', 'admin/sys-login-log/index') + ]) +] + +test.beforeEach(async ({ context, page: browserPage }) => { + await context.addCookies([{ name: 'Admin-Token', value: 'test-token', domain: '127.0.0.1', path: '/' }]) + await browserPage.route('**/api/v1/**', async route => { + const url = new URL(route.request().url()) + let data: unknown = {} + if (url.pathname.endsWith('/getinfo')) { + data = { roles: ['admin'], permissions: ['*:*:*'], name: '本地管理员', avatar: '', introduction: '', userId: 1 } + } else if (url.pathname.endsWith('/menurole')) { + data = navigation + } else if (url.pathname.endsWith('/sys-user')) { + data = { list: [{ userId: 1, username: 'local-admin', nickName: '本地管理员', roleId: 1, status: '2' }], count: 1 } + } else if (url.pathname.endsWith('/role')) { + data = { list: [{ roleId: 1, roleName: 'Chorus Operator', roleKey: 'chorus_operator', roleSort: 100, status: '2' }], count: 1 } + } else if (url.pathname.endsWith('/menu')) { + data = navigation + } else if (url.pathname.endsWith('/sys-api') || url.pathname.endsWith('/sys-login-log')) { + data = { list: [], count: 0 } + } + await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data }) }) + }) +}) + +test('renders four groups and all thirteen confirmed entries', async ({ page: browserPage }) => { + await browserPage.goto(`${baseURL}/#/chorus/system/admins`) + await expect(browserPage.getByRole('heading', { name: '管理员账号' })).toBeVisible() + for (const title of ['生成配置', '运行监控', '用户与访问', '系统管理']) { + await expect(browserPage.getByText(title, { exact: true }).first()).toBeVisible() + } + for (const title of navigation.flatMap(item => item.children.map(child => child.title))) { + await expect(browserPage.getByText(title, { exact: true }).first()).toBeAttached() + } + await expect(browserPage.getByText('当前账号', { exact: true }).first()).toBeVisible() + await browserPage.screenshot({ path: 'test-results/system-admin-desktop.png', fullPage: true }) +}) + +test('keeps system inventory pages read-only on desktop and narrow screens', async ({ page: browserPage }) => { + for (const [path, heading] of [['menus', '菜单结构'], ['apis', '接口清单'], ['login-logs', '登录日志']]) { + await browserPage.goto(`${baseURL}/#/chorus/system/${path}`) + await expect(browserPage.getByRole('heading', { name: heading })).toBeVisible() + await expect(browserPage.getByText('只读', { exact: true })).toBeVisible() + await expect(browserPage.getByRole('button', { name: /新增|编辑|删除/ })).toHaveCount(0) + } + + await browserPage.setViewportSize({ width: 390, height: 844 }) + await browserPage.goto(`${baseURL}/#/chorus/system/admins`) + await expect(browserPage.getByRole('heading', { name: '管理员账号' })).toBeVisible() + const overflow = await browserPage.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth) + expect(overflow).toBeFalsy() + await browserPage.screenshot({ path: 'test-results/system-admin-narrow.png', fullPage: true }) +}) diff --git a/admin-ui/tests/unit/admin/system-management.spec.js b/admin-ui/tests/unit/admin/system-management.spec.js new file mode 100644 index 0000000..319fe08 --- /dev/null +++ b/admin-ui/tests/unit/admin/system-management.spec.js @@ -0,0 +1,39 @@ +import fs from 'fs' +import path from 'path' + +const read = relativePath => fs.readFileSync(path.resolve(__dirname, `../../../${relativePath}`), 'utf8') + +describe('Chorus system management pages', () => { + test.each([ + ['sys-menu', '菜单结构'], + ['sys-api', '接口清单'], + ['sys-login-log', '登录日志'] + ])('%s is explicitly read-only and exposes no write action', (view, heading) => { + const source = read(`src/views/admin/${view}/index.vue`) + expect(source).toContain(`

${heading}

`) + expect(source).toContain('>只读') + expect(source).not.toMatch(/handle(Add|Update|Delete)/) + expect(source).not.toMatch(/新增|编辑|删除[^。]/) + }) + + test('read-only API modules only issue GET requests', () => { + for (const api of ['sys-menu', 'sys-api', 'sys-login-log']) { + const source = read(`src/api/admin/${api}.js`) + expect(source).not.toMatch(/method: '(post|put|delete)'/) + } + }) + + test('administrator page omits legacy organization and contact dependencies', () => { + const source = read('src/views/admin/sys-user/index.vue') + expect(source).toContain('

管理员账号

') + expect(source).toContain('当前账号') + expect(source).not.toMatch(/部门|岗位|手机号|邮箱|sys-dept|sys-post/) + }) + + test('role page presents the built-in role protection boundary', () => { + const source = read('src/views/admin/sys-role/index.vue') + expect(source).toContain("const PROTECTED_ROLE_KEY = 'chorus_operator'") + expect(source).toContain('内置保护') + expect(source).not.toMatch(/数据权限|sys-dept/) + }) +})