Files
airz-uniapp/services/content.uts
T

145 lines
11 KiB
Plaintext
Raw Normal View History

import { API_BASE_URL, requestJson } from '@/services/api.uts'
export type ContentSection = { id: string, title: string, body: string }
export type NewsArticle = { id: string, title: string, summary: string, category: string, date: string, source: string, readingTime: string, image: string, sections: ContentSection[] }
export type FinanceLesson = { id: string, title: string, desc: string, duration: string, tag: string, image: string, intro: string, topics: string[], chapters: ContentSection[], chapterCount: number, lessonVersion: string }
export type ArticlePage = { items: NewsArticle[], total: number, hasMore: boolean }
export type LessonPage = { items: FinanceLesson[], total: number, hasMore: boolean }
export type LessonState = { lessonId: string, lessonVersion: string, completedChapterIds: string[] }
export type ContentHome = { lessons: FinanceLesson[], articles: NewsArticle[] }
export type ContentSearchResult = { id: string, type: string, title: string, summary: string, category: string, meta: string, image: string }
export type SearchPage = { items: ContentSearchResult[], total: number, hasMore: boolean }
function imageUrl(value: string): string {
// 随包图片保留本地路径,后台上传的封面从接口站点读取。
if (value.startsWith('/uploads/')) return API_BASE_URL.replace('/api/v1', '') + value
return value
}
function contentObject(value: any | null): UTSJSONObject {
if (value == null || typeof value != 'object' || Array.isArray(value)) throw new Error('内容响应格式不完整,请重试')
return value as UTSJSONObject
}
function contentText(data: UTSJSONObject, key: string, empty: boolean = false): string {
const value = data.getString(key)
if (value == null || (!empty && value.trim().length == 0)) throw new Error('内容响应格式不完整,请重试')
return value
}
function validContentId(id: string): boolean { return /^[1-9][0-9]{0,9}$/.test(id) && parseFloat(id) <= 2147483647 }
function requireContentId(id: string): void { if (!validContentId(id)) throw new Error('内容编号无效,请重新选择') }
function uniqueStrings(value: any | null): string[] {
if (value == null || !Array.isArray(value)) throw new Error('内容响应格式不完整,请重试')
const result: string[] = []
;(value as any[]).forEach((item: any) => {
if (typeof item != 'string' || result.includes(item as string)) throw new Error('内容响应格式不完整,请重试')
result.push(item as string)
})
return result
}
function sections(value: any | null, lesson: boolean): ContentSection[] {
if (value == null || !Array.isArray(value) || (value as any[]).length > 30) throw new Error('内容章节格式不完整,请重试')
const ids: string[] = []
return (value as any[]).map((value: any): ContentSection => {
const item = contentObject(value); const id = lesson ? contentText(item, 'id') : ''
if (lesson && (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(id) || ids.includes(id))) throw new Error('课程章节编号无效,请重试')
ids.push(id)
return { id, title: contentText(item, 'title'), body: contentText(item, 'body') }
})
}
export function contentArticleFrom(data: UTSJSONObject): NewsArticle {
contentObject(data); const id = contentText(data, 'id'); requireContentId(id)
return { id, title: contentText(data, 'title'), summary: contentText(data, 'summary'), category: contentText(data, 'category'),
date: contentText(data, 'date'), source: contentText(data, 'source'), readingTime: contentText(data, 'readingTime'), image: imageUrl(contentText(data, 'image', true)),
sections: sections(data['sections'], false) }
}
export function contentLessonFrom(data: UTSJSONObject): FinanceLesson {
contentObject(data); const id = contentText(data, 'id'); requireContentId(id)
const chapterCount = data.getNumber('chapterCount'); const lessonVersion = contentText(data, 'lessonVersion')
const topics = uniqueStrings(data['topics']); const chapters = sections(data['chapters'], true)
if (chapterCount == null || !Number.isInteger(chapterCount) || chapterCount < 1 || chapterCount > 30
|| !/^[a-f0-9]{64}$/.test(lessonVersion) || topics.length == 0 || topics.some((topic: string): boolean => !['credit','business','documents'].includes(topic))
|| (chapters.length > 0 && chapters.length != chapterCount)) throw new Error('课程响应格式不完整,请重试')
return { id, title: contentText(data, 'title'), desc: contentText(data, 'desc'), duration: contentText(data, 'duration'), tag: contentText(data, 'tag'),
image: imageUrl(contentText(data, 'image', true)), intro: contentText(data, 'intro'), topics, chapters, chapterCount, lessonVersion }
}
function pageItems(data: UTSJSONObject, page: number): UTSJSONObject[] {
const total = data.getNumber('total'); const more = data.getBoolean('hasMore'); const items = data['items']
if (data.getNumber('page') != page || data.getNumber('pageSize') != 20 || total == null || !Number.isInteger(total) || total < 0
|| more == null || more != (page * 20 < total) || items == null || !Array.isArray(items)
|| (items as any[]).length > 20 || (items as any[]).length > total) throw new Error('内容分页响应不完整,请重试')
return (items as any[]).map((item: any): UTSJSONObject => contentObject(item))
}
export function validateContentAppend(previous: string[], incoming: string[]): void {
const seen = previous.slice()
incoming.forEach((key: string) => {
if (seen.includes(key)) throw new Error('内容列表已变化,请刷新后重试')
seen.push(key)
})
}
export async function fetchArticles(category: string, page: number = 1): Promise<ArticlePage> {
const data = await requestJson('/content/articles', 'GET', { category, page, pageSize: 20 })
const items = pageItems(data, page).map((item: UTSJSONObject): NewsArticle => contentArticleFrom(item))
validateContentAppend([], items.map((item: NewsArticle): string => item.id))
if (category.length > 0 && items.some((item: NewsArticle): boolean => item.category != category)) throw new Error('资讯分类响应不符,请重试')
return { items, total: data.getNumber('total')!, hasMore: data.getBoolean('hasMore')! }
}
export async function fetchArticle(id: string): Promise<NewsArticle> {
requireContentId(id)
const result = contentArticleFrom(await requestJson('/content/articles/' + id, 'GET', {}))
if (result.id != id || result.sections.length == 0) throw new Error('资讯详情响应不符,请重试')
return result
}
export async function fetchLessons(topic: string, page: number = 1): Promise<LessonPage> {
const data = await requestJson('/content/lessons', 'GET', { topic, page, pageSize: 20 })
const items = pageItems(data, page).map((item: UTSJSONObject): FinanceLesson => contentLessonFrom(item))
validateContentAppend([], items.map((item: FinanceLesson): string => item.id))
if (topic.length > 0 && items.some((item: FinanceLesson): boolean => !item.topics.includes(topic))) throw new Error('课程场景响应不符,请重试')
return { items, total: data.getNumber('total')!, hasMore: data.getBoolean('hasMore')! }
}
export async function fetchLesson(id: string): Promise<FinanceLesson> {
requireContentId(id)
const result = contentLessonFrom(await requestJson('/content/lessons/' + id, 'GET', {}))
if (result.id != id || result.chapters.length != result.chapterCount) throw new Error('课程详情响应不符,请重试')
return result
}
function stateFrom(data: UTSJSONObject, id: string): LessonState {
const lessonId = contentText(data, 'lessonId'); const lessonVersion = contentText(data, 'lessonVersion')
const completedChapterIds = uniqueStrings(data['completedChapterIds'])
if (lessonId != id || !/^[a-f0-9]{64}$/.test(lessonVersion) || completedChapterIds.length > 30
|| completedChapterIds.some((chapter: string): boolean => !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(chapter))) throw new Error('课程进度响应不符,请刷新后重试')
return { lessonId, lessonVersion, completedChapterIds }
}
export function validateLessonState(lesson: FinanceLesson, state: LessonState): void {
if (state.lessonId != lesson.id || state.lessonVersion != lesson.lessonVersion
|| state.completedChapterIds.some((id: string): boolean => !lesson.chapters.some((chapter: ContentSection): boolean => chapter.id == id)))
throw new Error('课程或进度已更新,请刷新后继续学习')
}
export async function fetchLessonProgress(id: string): Promise<LessonState> {
requireContentId(id)
return stateFrom(await requestJson('/content/lessons/' + id + '/progress', 'GET', {}), id)
}
export async function completeLessonChapter(id: string, chapterId: string, lessonVersion: string): Promise<LessonState> {
requireContentId(id)
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(chapterId) || !/^[a-f0-9]{64}$/.test(lessonVersion)) throw new Error('课程章节或版本无效,请刷新')
const state = stateFrom(await requestJson('/content/lessons/' + id + '/progress', 'PUT', { chapterId, lessonVersion, completed: true }), id)
if (state.lessonVersion != lessonVersion || !state.completedChapterIds.includes(chapterId)) throw new Error('课程进度保存响应不符,请刷新后重试')
return state
}
export async function fetchContentHome(): Promise<ContentHome> {
const data = await requestJson('/content/home', 'GET', {})
const lessons = data['lessons']; const articles = data['articles']
if (lessons == null || !Array.isArray(lessons) || articles == null || !Array.isArray(articles)) throw new Error('推荐内容响应不完整,请重试')
return { lessons: (lessons as any[]).map((item: any): FinanceLesson => contentLessonFrom(contentObject(item))), articles: (articles as any[]).map((item: any): NewsArticle => contentArticleFrom(contentObject(item))) }
}
export async function searchContent(keyword: string, type: string, page: number = 1): Promise<SearchPage> {
const data = await requestJson('/content/search', 'GET', { keyword, type, page, pageSize: 20 })
const items = pageItems(data, page).map((item: UTSJSONObject): ContentSearchResult => {
const id = contentText(item, 'id'); const kind = contentText(item, 'type'); requireContentId(id)
if (!['news','lesson'].includes(kind) || (type != 'all' && kind != type)) throw new Error('搜索分类响应不符,请重试')
return { id, type: kind, title: contentText(item, 'title'), summary: contentText(item, 'summary'), category: contentText(item, 'category'), meta: contentText(item, 'meta'), image: imageUrl(contentText(item, 'image', true)) }
})
validateContentAppend([], items.map((item: ContentSearchResult): string => item.type + ':' + item.id))
return { items, total: data.getNumber('total')!, hasMore: data.getBoolean('hasMore')! }
}
export function emptyArticle(): NewsArticle { return { id: '', title: '', summary: '', category: '', date: '', source: '', readingTime: '', image: '', sections: [] } }
export function emptyLesson(): FinanceLesson { return { id: '', title: '', desc: '', duration: '', tag: '', image: '', intro: '', topics: [], chapters: [], chapterCount: 0, lessonVersion: '' } }