feat(auth): 后台登录、角色权限与会话访问策略
管理员与客服分角色登录,权限由后端判定;前端补齐登录、人员与组织管理页面。
This commit is contained in:
1 parent
5e0615f36d
commit
e24d3e2a65
123 files changed
+25293
No files matched your search
@@ -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')
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in new issue
Block a user