feat(auth): 后台登录、角色权限与会话访问策略

管理员与客服分角色登录,权限由后端判定;前端补齐登录、人员与组织管理页面。
This commit is contained in:
WangShen007 committed 2026-09-24 21:35:00 +08:00
1 parent 5e0615f36d
commit e24d3e2a65
123 files changed
+25293

No files matched your search

+291
View File
@@ -0,0 +1,291 @@
/*
Copyright 2024-2026 ChatterMate
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import api from './api'
import type {
FaqGenerationJob,
FaqItem,
FaqImportMode,
FaqListResponse,
FaqSeoFields,
FaqStatus,
GenerateEstimate,
HelpCenterDomain,
HelpCenterSettings,
HelpCenterSettingsUpdate,
} from '@/types/faq'
function errorMessage(error: any, fallback: string): Error {
return new Error(error.response?.data?.detail || fallback)
}
// Backend bulk endpoints cap faq_ids at 200 (MAX_BULK_IDS in schemas/faq.py);
// split larger selections so a select-all over a big FAQ set still works.
const MAX_BULK_IDS = 200
function chunk<T>(items: T[], size: number): T[][] {
const batches: T[][] = []
for (let i = 0; i < items.length; i += size) batches.push(items.slice(i, i + size))
return batches
}
export const faqService = {
async getFaqs(params: { status?: FaqStatus; category?: string; q?: string; page?: number; page_size?: number } = {}): Promise<FaqListResponse> {
try {
const response = await api.get('/help-center/faqs', { params: { page_size: 200, ...params } })
return response.data
} catch (error: any) {
throw errorMessage(error, 'Failed to load FAQs')
}
},
async uploadImage(file: File): Promise<string> {
try {
const formData = new FormData()
formData.append('file', file)
const response = await api.post('/help-center/faqs/image', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})
return response.data.url as string
} catch (error: any) {
throw errorMessage(error, 'Failed to upload image')
}
},
async createFaq(
payload: {
question: string
answer: string
category?: string
status?: FaqStatus
} & Partial<FaqSeoFields>,
): Promise<FaqItem> {
try {
const response = await api.post('/help-center/faqs', payload)
return response.data
} catch (error: any) {
throw errorMessage(error, 'Failed to create FAQ')
}
},
async updateFaq(
id: string,
payload: Partial<
Pick<FaqItem, 'question' | 'answer' | 'category' | 'status'> & FaqSeoFields
>,
): Promise<FaqItem> {
try {
const response = await api.put(`/help-center/faqs/${id}`, payload)
return response.data
} catch (error: any) {
throw errorMessage(error, 'Failed to update FAQ')
}
},
async deleteFaq(id: string): Promise<void> {
try {
await api.delete(`/help-center/faqs/${id}`)
} catch (error: any) {
throw errorMessage(error, 'Failed to delete FAQ')
}
},
async setStatus(ids: string[], status: FaqStatus): Promise<number> {
try {
let updated = 0
for (const batch of chunk(ids, MAX_BULK_IDS)) {
const response = await api.post('/help-center/faqs/bulk-status', { faq_ids: batch, status })
updated += response.data.updated
}
return updated
} catch (error: any) {
throw errorMessage(error, 'Failed to update FAQ status')
}
},
async bulkDelete(ids: string[]): Promise<number> {
try {
let deleted = 0
for (const batch of chunk(ids, MAX_BULK_IDS)) {
const response = await api.post('/help-center/faqs/bulk-delete', { faq_ids: batch })
deleted += response.data.deleted
}
return deleted
} catch (error: any) {
throw errorMessage(error, 'Failed to delete FAQs')
}
},
async getGenerateEstimate(includePages = true): Promise<GenerateEstimate> {
try {
const response = await api.get('/help-center/generate/estimate', {
params: { include_pages: includePages },
})
return response.data
} catch (error: any) {
throw errorMessage(error, 'Failed to load generation estimate')
}
},
async startGeneration(knowledgeIds?: number[]): Promise<FaqGenerationJob> {
try {
const response = await api.post(
'/help-center/generate',
knowledgeIds?.length ? { knowledge_ids: knowledgeIds } : {},
)
return response.data
} catch (error: any) {
throw errorMessage(error, 'Failed to start generation')
}
},
async importFaq(
url: string,
mode: FaqImportMode = 'qa',
preserveUrls = false,
): Promise<FaqGenerationJob> {
try {
const response = await api.post('/help-center/import', {
url,
mode,
preserve_urls: preserveUrls,
})
return response.data
} catch (error: any) {
throw errorMessage(error, 'Failed to start import')
}
},
async importPdf(file: File): Promise<FaqGenerationJob> {
try {
const formData = new FormData()
formData.append('file', file)
const response = await api.post('/help-center/import/pdf', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})
return response.data
} catch (error: any) {
throw errorMessage(error, 'Failed to start PDF import')
}
},
async getJob(active = true): Promise<FaqGenerationJob | null> {
try {
const response = await api.get('/help-center/jobs', { params: { active } })
return response.data
} catch (error: any) {
throw errorMessage(error, 'Failed to load job status')
}
},
async getSettings(): Promise<HelpCenterSettings> {
try {
const response = await api.get('/help-center/settings')
return response.data
} catch (error: any) {
throw errorMessage(error, 'Failed to load help center settings')
}
},
async updateSettings(payload: HelpCenterSettingsUpdate): Promise<HelpCenterSettings> {
try {
const response = await api.put('/help-center/settings', payload)
return response.data
} catch (error: any) {
throw errorMessage(error, 'Failed to save help center settings')
}
},
async uploadLogo(file: File): Promise<HelpCenterSettings> {
try {
const formData = new FormData()
formData.append('file', file)
const response = await api.post('/help-center/logo', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})
return response.data
} catch (error: any) {
throw errorMessage(error, 'Failed to upload logo')
}
},
async removeLogo(): Promise<HelpCenterSettings> {
try {
const response = await api.delete('/help-center/logo')
return response.data
} catch (error: any) {
throw errorMessage(error, 'Failed to remove logo')
}
},
async uploadFavicon(file: File): Promise<HelpCenterSettings> {
try {
const formData = new FormData()
formData.append('file', file)
const response = await api.post('/help-center/favicon', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})
return response.data
} catch (error: any) {
throw errorMessage(error, 'Failed to upload favicon')
}
},
async removeFavicon(): Promise<HelpCenterSettings> {
try {
const response = await api.delete('/help-center/favicon')
return response.data
} catch (error: any) {
throw errorMessage(error, 'Failed to remove favicon')
}
},
async setDomain(domain: string): Promise<HelpCenterDomain> {
try {
const response = await api.post('/help-center/domain', { domain })
return response.data
} catch (error: any) {
throw errorMessage(error, 'Failed to set custom domain')
}
},
async removeDomain(): Promise<HelpCenterDomain> {
try {
const response = await api.delete('/help-center/domain')
return response.data
} catch (error: any) {
throw errorMessage(error, 'Failed to remove custom domain')
}
},
async verifyDomain(): Promise<HelpCenterDomain> {
try {
const response = await api.post('/help-center/domain/verify')
return response.data
} catch (error: any) {
throw errorMessage(error, 'Failed to verify domain')
}
},
async getDomainStatus(): Promise<HelpCenterDomain> {
try {
const response = await api.get('/help-center/domain/status')
return response.data
} catch (error: any) {
throw errorMessage(error, 'Failed to load domain status')
}
},
}