实现V4目录管理后台前端

This commit is contained in:
andy committed 2026-07-20 00:09:58 +07:00
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({
+114
View File
@@ -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],
},
})
}