实现V4目录管理后台前端
This commit is contained in:
1 parent
739fccd955
commit
f40f90553f
19 files changed
+1654
-11
No files matched your search
@@ -60,6 +60,7 @@ describe('reservation router', () => {
|
||||
expect(router.resolve('/system/roles').name).toBe('system-roles')
|
||||
expect(router.resolve('/system/menus').name).toBe('system-menus')
|
||||
expect(router.resolve('/system/hotels').name).toBe('system-hotels')
|
||||
expect(router.resolve('/system/reservation-catalogs').name).toBe('system-reservation-catalogs')
|
||||
expect(router.resolve('/system/audits').name).toBe('system-audits')
|
||||
expect(router.resolve('/system/not-ready').name).toBe('not-found')
|
||||
})
|
||||
@@ -81,6 +82,7 @@ describe('reservation router', () => {
|
||||
expect(router.resolve('/system/roles').meta.permission).toBe('SYSTEM_ROLE_MANAGE')
|
||||
expect(router.resolve('/system/menus').meta.permission).toBe('SYSTEM_MENU_MANAGE')
|
||||
expect(router.resolve('/system/hotels').meta.permission).toBe('HOTEL_MANAGE')
|
||||
expect(router.resolve('/system/reservation-catalogs').meta.permission).toBe('RESERVATION_CATALOG_MANAGE')
|
||||
expect(router.resolve('/system/audits').meta.permission).toBe('SYSTEM_ADMIN_CONSOLE_ACCESS')
|
||||
})
|
||||
|
||||
@@ -152,6 +154,19 @@ describe('reservation router', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('redirects the System Admin entry to catalog management when it is the first allowed admin page', async () => {
|
||||
const guard = createAuthGuard(() =>
|
||||
createAuthStoreForGuard({
|
||||
hasPermission: (permission: string) =>
|
||||
permission === 'SYSTEM_ADMIN_CONSOLE_ACCESS' || permission === 'RESERVATION_CATALOG_MANAGE',
|
||||
}),
|
||||
)
|
||||
|
||||
await expect(guard(createRoute('/system', { permission: 'SYSTEM_ADMIN_CONSOLE_ACCESS' }))).resolves.toBe(
|
||||
'/system/reservation-catalogs',
|
||||
)
|
||||
})
|
||||
|
||||
it('redirects the root route to the default page when the backend menu points to root', async () => {
|
||||
const guard = createAuthGuard(() =>
|
||||
createAuthStoreForGuard({
|
||||
|
||||
@@ -7,12 +7,18 @@ import {
|
||||
createAdminHotel,
|
||||
createAdminMenu,
|
||||
createAdminRole,
|
||||
createAdminReservationCatalogAccount,
|
||||
createAdminReservationCatalogRateCode,
|
||||
createAdminReservationCatalogRoomType,
|
||||
createAdminUser,
|
||||
fetchAdminAudits,
|
||||
fetchAdminHotels,
|
||||
fetchAdminMenuTree,
|
||||
fetchAdminMenus,
|
||||
fetchAdminPermissions,
|
||||
fetchAdminReservationCatalogAccounts,
|
||||
fetchAdminReservationCatalogRateCodes,
|
||||
fetchAdminReservationCatalogRoomTypes,
|
||||
fetchAdminRoleDetail,
|
||||
fetchAdminRoles,
|
||||
fetchAdminUserDetail,
|
||||
@@ -22,6 +28,9 @@ import {
|
||||
updateAdminHotelStatus,
|
||||
updateAdminMenu,
|
||||
updateAdminMenuTreeOrder,
|
||||
updateAdminReservationCatalogAccountStatus,
|
||||
updateAdminReservationCatalogRateCodeStatus,
|
||||
updateAdminReservationCatalogRoomTypeStatus,
|
||||
updateAdminRole,
|
||||
updateAdminUser,
|
||||
} from '@/services/systemAdminService'
|
||||
@@ -300,4 +309,109 @@ describe('systemAdminService', () => {
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('fetches Reservation V4 catalog management lists with supported filters', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockJsonResponse({ items: [], page: {} }))
|
||||
|
||||
await fetchAdminReservationCatalogAccounts({
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
keyword: 'qbd',
|
||||
status: 'ACTIVE',
|
||||
page_num: 2,
|
||||
page_size: 10,
|
||||
})
|
||||
await fetchAdminReservationCatalogRoomTypes({ hotel_id: 'HOTEL-TEST', status: 'DISABLED' })
|
||||
await fetchAdminReservationCatalogRateCodes({ keyword: 'group' })
|
||||
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/api/admin/reservation/catalogs/accounts?hotel_id=HOTEL-TEST&keyword=qbd&status=ACTIVE&page_num=2&page_size=10',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/api/admin/reservation/catalogs/room-types?hotel_id=HOTEL-TEST&status=DISABLED',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'/api/admin/reservation/catalogs/rate-codes?keyword=group',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('sends Reservation V4 catalog create and status operations to admin endpoints', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockJsonResponse({ id: '1' }))
|
||||
|
||||
await createAdminReservationCatalogAccount({
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
account_code: 'ACME',
|
||||
account_name: 'Acme Travel',
|
||||
market_code: 'LEISURE',
|
||||
source_code: 'TRAVEL_AGENT',
|
||||
})
|
||||
await updateAdminReservationCatalogAccountStatus('10001', { status: 'DISABLED' })
|
||||
await createAdminReservationCatalogRoomType({
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
code: 'FAM',
|
||||
display_name: 'Family Room',
|
||||
sort_order: 300,
|
||||
})
|
||||
await updateAdminReservationCatalogRoomTypeStatus('20001', { status: 'ACTIVE' })
|
||||
await createAdminReservationCatalogRateCode({
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
code: 'CORP',
|
||||
display_name: 'Corporate',
|
||||
})
|
||||
await updateAdminReservationCatalogRateCodeStatus('30001', { status: 'DISABLED' })
|
||||
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/api/admin/reservation/catalogs/accounts',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
account_code: 'ACME',
|
||||
account_name: 'Acme Travel',
|
||||
market_code: 'LEISURE',
|
||||
source_code: 'TRAVEL_AGENT',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/api/admin/reservation/catalogs/accounts/10001/status',
|
||||
expect.objectContaining({
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ status: 'DISABLED' }),
|
||||
}),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'/api/admin/reservation/catalogs/room-types',
|
||||
expect.objectContaining({ method: 'POST' }),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'/api/admin/reservation/catalogs/room-types/20001/status',
|
||||
expect.objectContaining({
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ status: 'ACTIVE' }),
|
||||
}),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
5,
|
||||
'/api/admin/reservation/catalogs/rate-codes',
|
||||
expect.objectContaining({ method: 'POST' }),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
6,
|
||||
'/api/admin/reservation/catalogs/rate-codes/30001/status',
|
||||
expect.objectContaining({
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ status: 'DISABLED' }),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,274 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import zhCN from '@/i18n/locales/zh-CN'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import SystemReservationCatalogsView from '@/views/system/SystemReservationCatalogsView.vue'
|
||||
|
||||
vi.mock('@/services/systemAdminService', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/services/systemAdminService')>()
|
||||
return {
|
||||
...actual,
|
||||
createAdminReservationCatalogAccount: vi.fn(),
|
||||
createAdminReservationCatalogRateCode: vi.fn(),
|
||||
createAdminReservationCatalogRoomType: vi.fn(),
|
||||
fetchAdminReservationCatalogAccounts: vi.fn(),
|
||||
fetchAdminReservationCatalogRateCodes: vi.fn(),
|
||||
fetchAdminReservationCatalogRoomTypes: vi.fn(),
|
||||
updateAdminReservationCatalogAccountStatus: vi.fn(),
|
||||
updateAdminReservationCatalogRateCodeStatus: vi.fn(),
|
||||
updateAdminReservationCatalogRoomTypeStatus: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
const systemAdminService = await import('@/services/systemAdminService')
|
||||
|
||||
describe('SystemReservationCatalogsView', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(systemAdminService.createAdminReservationCatalogAccount).mockReset()
|
||||
vi.mocked(systemAdminService.createAdminReservationCatalogRateCode).mockReset()
|
||||
vi.mocked(systemAdminService.createAdminReservationCatalogRoomType).mockReset()
|
||||
vi.mocked(systemAdminService.fetchAdminReservationCatalogAccounts).mockReset()
|
||||
vi.mocked(systemAdminService.fetchAdminReservationCatalogRateCodes).mockReset()
|
||||
vi.mocked(systemAdminService.fetchAdminReservationCatalogRoomTypes).mockReset()
|
||||
vi.mocked(systemAdminService.updateAdminReservationCatalogAccountStatus).mockReset()
|
||||
vi.mocked(systemAdminService.updateAdminReservationCatalogRateCodeStatus).mockReset()
|
||||
vi.mocked(systemAdminService.updateAdminReservationCatalogRoomTypeStatus).mockReset()
|
||||
vi.mocked(systemAdminService.fetchAdminReservationCatalogAccounts).mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
id: '10001',
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
account_code: 'QBD_TRAVEL',
|
||||
account_name: 'Q.B.D. Travel',
|
||||
market_code: 'LEISURE',
|
||||
source_code: 'TRAVEL_AGENT',
|
||||
status: 'ACTIVE',
|
||||
catalog_source: 'SYSTEM_MANAGED',
|
||||
external_account_id: null,
|
||||
catalog_version: 'catalog-v1',
|
||||
metadata_json: null,
|
||||
version: 1,
|
||||
created_at: '2026-07-19T02:00:00Z',
|
||||
updated_at: '2026-07-19T02:00:00Z',
|
||||
},
|
||||
],
|
||||
page: {
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
total: 1,
|
||||
},
|
||||
})
|
||||
vi.mocked(systemAdminService.fetchAdminReservationCatalogRoomTypes).mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
id: '20001',
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
catalog_type: 'ROOM_TYPE',
|
||||
code: 'TWN',
|
||||
display_name: 'Twin',
|
||||
status: 'ACTIVE',
|
||||
catalog_source: 'SYSTEM_MANAGED',
|
||||
external_id: null,
|
||||
sort_order: 100,
|
||||
catalog_version: 'catalog-v1',
|
||||
metadata_json: null,
|
||||
version: 1,
|
||||
created_at: '2026-07-19T02:00:00Z',
|
||||
updated_at: '2026-07-19T02:00:00Z',
|
||||
},
|
||||
],
|
||||
page: {
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
total: 1,
|
||||
},
|
||||
})
|
||||
vi.mocked(systemAdminService.fetchAdminReservationCatalogRateCodes).mockResolvedValue({
|
||||
items: [],
|
||||
page: {
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
total: 0,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('loads Account catalog for the selected hotel and creates with fixed Market and Source defaults', async () => {
|
||||
vi.mocked(systemAdminService.createAdminReservationCatalogAccount).mockResolvedValue({
|
||||
id: '10002',
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
account_code: 'ACME',
|
||||
account_name: 'Acme Travel',
|
||||
market_code: 'LEISURE',
|
||||
source_code: 'TRAVEL_AGENT',
|
||||
status: 'ACTIVE',
|
||||
catalog_source: 'SYSTEM_MANAGED',
|
||||
external_account_id: null,
|
||||
catalog_version: 'catalog-v1',
|
||||
metadata_json: null,
|
||||
version: 1,
|
||||
created_at: '2026-07-19T02:00:00Z',
|
||||
updated_at: '2026-07-19T02:00:00Z',
|
||||
})
|
||||
|
||||
const wrapper = mountCatalogView()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('Reservation V4 目录管理')
|
||||
expect(wrapper.text()).toContain('QBD_TRAVEL')
|
||||
expect(systemAdminService.fetchAdminReservationCatalogAccounts).toHaveBeenCalledWith({
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
keyword: '',
|
||||
status: '',
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
|
||||
await wrapper.find('[data-testid="catalog-account-code"]').setValue('ACME')
|
||||
await wrapper.find('[data-testid="catalog-account-name"]').setValue('Acme Travel')
|
||||
await wrapper.find('[data-testid="catalog-create-form"]').trigger('submit')
|
||||
await flushPromises()
|
||||
|
||||
expect(systemAdminService.createAdminReservationCatalogAccount).toHaveBeenCalledWith({
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
account_code: 'ACME',
|
||||
account_name: 'Acme Travel',
|
||||
market_code: 'LEISURE',
|
||||
source_code: 'TRAVEL_AGENT',
|
||||
external_account_id: undefined,
|
||||
catalog_version: undefined,
|
||||
metadata_json: undefined,
|
||||
})
|
||||
expect(wrapper.text()).toContain('新增后默认启用')
|
||||
})
|
||||
|
||||
it('switches to Room Type catalog and toggles status by catalog id', async () => {
|
||||
vi.mocked(systemAdminService.updateAdminReservationCatalogRoomTypeStatus).mockResolvedValue({
|
||||
id: '20001',
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
catalog_type: 'ROOM_TYPE',
|
||||
code: 'TWN',
|
||||
display_name: 'Twin',
|
||||
status: 'DISABLED',
|
||||
catalog_source: 'SYSTEM_MANAGED',
|
||||
external_id: null,
|
||||
sort_order: 100,
|
||||
catalog_version: 'catalog-v1',
|
||||
metadata_json: null,
|
||||
version: 2,
|
||||
created_at: '2026-07-19T02:00:00Z',
|
||||
updated_at: '2026-07-19T03:00:00Z',
|
||||
})
|
||||
|
||||
const wrapper = mountCatalogView()
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.find('[data-testid="catalog-tab-room-types"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(systemAdminService.fetchAdminReservationCatalogRoomTypes).toHaveBeenCalledWith({
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
keyword: '',
|
||||
status: '',
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
expect(wrapper.text()).toContain('TWN')
|
||||
|
||||
await wrapper.find('[data-testid="catalog-status-20001"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(systemAdminService.updateAdminReservationCatalogRoomTypeStatus).toHaveBeenCalledWith('20001', {
|
||||
status: 'DISABLED',
|
||||
})
|
||||
})
|
||||
|
||||
it('switches to Rate Code catalog and creates through the dedicated endpoint', async () => {
|
||||
vi.mocked(systemAdminService.createAdminReservationCatalogRateCode).mockResolvedValue({
|
||||
id: '30001',
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
catalog_type: 'RATE_CODE',
|
||||
code: 'CORP',
|
||||
display_name: 'Corporate',
|
||||
status: 'ACTIVE',
|
||||
catalog_source: 'SYSTEM_MANAGED',
|
||||
external_id: null,
|
||||
sort_order: 100,
|
||||
catalog_version: 'catalog-v1',
|
||||
metadata_json: null,
|
||||
version: 1,
|
||||
created_at: '2026-07-19T02:00:00Z',
|
||||
updated_at: '2026-07-19T02:00:00Z',
|
||||
})
|
||||
|
||||
const wrapper = mountCatalogView()
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.find('[data-testid="catalog-tab-rate-codes"]').trigger('click')
|
||||
await flushPromises()
|
||||
await wrapper.find('[data-testid="catalog-code"]').setValue('CORP')
|
||||
await wrapper.find('[data-testid="catalog-display-name"]').setValue('Corporate')
|
||||
await wrapper.find('[data-testid="catalog-create-form"]').trigger('submit')
|
||||
await flushPromises()
|
||||
|
||||
expect(systemAdminService.fetchAdminReservationCatalogRateCodes).toHaveBeenCalledWith({
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
keyword: '',
|
||||
status: '',
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
expect(systemAdminService.createAdminReservationCatalogRateCode).toHaveBeenCalledWith({
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
code: 'CORP',
|
||||
display_name: 'Corporate',
|
||||
external_id: undefined,
|
||||
sort_order: 100,
|
||||
catalog_version: undefined,
|
||||
metadata_json: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function mountCatalogView() {
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
const authStore = useAuthStore()
|
||||
authStore.applyLoginResult({
|
||||
access_token: 'test-token',
|
||||
token_type: 'Bearer',
|
||||
expires_at: '2026-07-19T12:00:00Z',
|
||||
user: {
|
||||
id: 'u1',
|
||||
username: 'admin',
|
||||
display_name: '系统管理员',
|
||||
super_admin: false,
|
||||
},
|
||||
default_hotel_id: 'HOTEL-TEST',
|
||||
hotels: [
|
||||
{
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
hotel_name: '测试酒店',
|
||||
time_zone: 'Asia/Bangkok',
|
||||
default_hotel: true,
|
||||
},
|
||||
],
|
||||
permissions: ['SYSTEM_ADMIN_CONSOLE_ACCESS', 'RESERVATION_CATALOG_MANAGE'],
|
||||
menus: [],
|
||||
})
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'zh-CN',
|
||||
messages: {
|
||||
'zh-CN': zhCN,
|
||||
},
|
||||
})
|
||||
return mount(SystemReservationCatalogsView, {
|
||||
global: {
|
||||
plugins: [pinia, i18n],
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user