feat: 接入内容服务端真实接口并优化页面交互
- 迁移内容数据到服务端接口,移除本地模拟数据 - 首页新增推荐课程加载与未读消息检查 - 资讯、课程、搜索页面支持分页加载与错误重试 - 登录页新增恢复课程、消息、帮助页的路由逻辑 - 评估分析页新增处理中状态动效与阶段提示 - API 环境切换到正式域名 airz.hqling.cloud
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
||||
// 小程序体验版连接远程测试服;切回本机联调时改为 development。
|
||||
export const APP_ENVIRONMENT: string = 'test' as string
|
||||
const DEVELOPMENT_API_URL = 'http://127.0.0.1/api/v1'
|
||||
const TEST_API_URL = 'https://aitest.ychysg.cn/api/v1'
|
||||
const TEST_API_URL = 'https://airz.hqling.cloud/api/v1'
|
||||
const PRODUCTION_API_URL = ''
|
||||
export const API_BASE_URL = APP_ENVIRONMENT == 'production' ? PRODUCTION_API_URL : (APP_ENVIRONMENT == 'test' ? TEST_API_URL : DEVELOPMENT_API_URL)
|
||||
export const API_DEMO_ENABLED = false
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
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 = { 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 sections(value: UTSJSONObject[]): ContentSection[] {
|
||||
return value.map((item: UTSJSONObject): ContentSection => ({ id: item.getString('id') ?? '', title: item.getString('title') ?? '', body: item.getString('body') ?? '' }))
|
||||
}
|
||||
function articleFrom(data: UTSJSONObject): NewsArticle {
|
||||
return { id: data.getString('id') ?? '', title: data.getString('title') ?? '', summary: data.getString('summary') ?? '', category: data.getString('category') ?? '',
|
||||
date: data.getString('date') ?? '', source: data.getString('source') ?? '', readingTime: data.getString('readingTime') ?? '', image: imageUrl(data.getString('image') ?? ''),
|
||||
sections: sections(data['sections'] as UTSJSONObject[]) }
|
||||
}
|
||||
function lessonFrom(data: UTSJSONObject): FinanceLesson {
|
||||
return { id: data.getString('id') ?? '', title: data.getString('title') ?? '', desc: data.getString('desc') ?? '', duration: data.getString('duration') ?? '', tag: data.getString('tag') ?? '',
|
||||
image: imageUrl(data.getString('image') ?? ''), intro: data.getString('intro') ?? '', topics: data['topics'] as string[], chapters: sections(data['chapters'] as UTSJSONObject[]),
|
||||
chapterCount: data.getNumber('chapterCount') ?? 0, lessonVersion: data.getString('lessonVersion') ?? '' }
|
||||
}
|
||||
export async function fetchArticles(category: string, page: number = 1): Promise<ArticlePage> {
|
||||
const data = await requestJson('/content/articles', 'GET', { category, page, pageSize: 20 })
|
||||
return { items: (data['items'] as UTSJSONObject[]).map((item: UTSJSONObject): NewsArticle => articleFrom(item)), total: data.getNumber('total') ?? 0, hasMore: data.getBoolean('hasMore') ?? false }
|
||||
}
|
||||
export async function fetchArticle(id: string): Promise<NewsArticle> { return articleFrom(await requestJson('/content/articles/' + encodeURIComponent(id), 'GET', {})) }
|
||||
export async function fetchLessons(topic: string, page: number = 1): Promise<LessonPage> {
|
||||
const data = await requestJson('/content/lessons', 'GET', { topic, page, pageSize: 20 })
|
||||
return { items: (data['items'] as UTSJSONObject[]).map((item: UTSJSONObject): FinanceLesson => lessonFrom(item)), total: data.getNumber('total') ?? 0, hasMore: data.getBoolean('hasMore') ?? false }
|
||||
}
|
||||
export async function fetchLesson(id: string): Promise<FinanceLesson> { return lessonFrom(await requestJson('/content/lessons/' + encodeURIComponent(id), 'GET', {})) }
|
||||
function stateFrom(data: UTSJSONObject): LessonState { return { lessonVersion: data.getString('lessonVersion') ?? '', completedChapterIds: data['completedChapterIds'] as string[] } }
|
||||
export async function fetchLessonProgress(id: string): Promise<LessonState> { return stateFrom(await requestJson('/content/lessons/' + id + '/progress', 'GET', {})) }
|
||||
export async function completeLessonChapter(id: string, chapterId: string, lessonVersion: string): Promise<LessonState> {
|
||||
return stateFrom(await requestJson('/content/lessons/' + id + '/progress', 'PUT', { chapterId, lessonVersion, completed: true }))
|
||||
}
|
||||
export async function fetchContentHome(): Promise<ContentHome> {
|
||||
const data = await requestJson('/content/home', 'GET', {})
|
||||
return { lessons: (data['lessons'] as UTSJSONObject[]).map((item: UTSJSONObject): FinanceLesson => lessonFrom(item)), articles: (data['articles'] as UTSJSONObject[]).map((item: UTSJSONObject): NewsArticle => articleFrom(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 = (data['items'] as UTSJSONObject[]).map((item: UTSJSONObject): ContentSearchResult => ({
|
||||
id: item.getString('id') ?? '', type: item.getString('type') ?? '', title: item.getString('title') ?? '', summary: item.getString('summary') ?? '',
|
||||
category: item.getString('category') ?? '', meta: item.getString('meta') ?? '', image: imageUrl(item.getString('image') ?? '') }))
|
||||
return { items, total: data.getNumber('total') ?? 0, hasMore: data.getBoolean('hasMore') ?? false }
|
||||
}
|
||||
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: '' } }
|
||||
@@ -0,0 +1,55 @@
|
||||
import { getSession, requestJson } from '@/services/api.uts'
|
||||
|
||||
export type UserMessage = { id: string, type: string, category: string, icon: string, title: string, content: string, time: string, isRead: boolean }
|
||||
export type MessagePage = { items: UserMessage[], snapshotId: string, nextBeforeId: string, unreadCount: number }
|
||||
export type MessageDetail = { item: UserMessage, targetUrl: string, targetLabel: string, targetUnavailable: boolean }
|
||||
|
||||
const validId = (value: string): boolean => /^[1-9][0-9]{0,9}$/.test(value)
|
||||
const count = (data: UTSJSONObject): number => {
|
||||
const value = data.getNumber('unreadCount')
|
||||
if (value == null || !Number.isInteger(value) || value < 0) throw new Error('未读数量格式无效')
|
||||
return value
|
||||
}
|
||||
function messageFrom(data: UTSJSONObject): UserMessage {
|
||||
const id = data.getString('id') ?? ''; const type = data.getString('type') ?? ''
|
||||
if (!validId(id) || !['system', 'service', 'activity'].includes(type)) throw new Error('消息格式无效')
|
||||
const categories: UTSJSONObject = { system: '系统通知', service: '服务通知', activity: '活动消息' }
|
||||
const icons: UTSJSONObject = { system: 'system-bell', service: 'approval', activity: 'gift' }
|
||||
const date = new Date((data.getString('createdAt') ?? '').replace(' ', 'T'))
|
||||
const now = new Date(); const pad = (n: number): string => n < 10 ? '0' + n.toString() : n.toString()
|
||||
let time = '未记录'
|
||||
if (!isNaN(date.getTime())) time = date.getFullYear() == now.getFullYear() && date.getMonth() == now.getMonth() && date.getDate() == now.getDate()
|
||||
? pad(date.getHours()) + ':' + pad(date.getMinutes()) : date.getFullYear().toString() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate())
|
||||
return { id, type, category: categories.getString(type) ?? '', icon: icons.getString(type) ?? '', title: data.getString('title') ?? '',
|
||||
content: data.getString('content') ?? '', time, isRead: data.getBoolean('isRead') ?? false }
|
||||
}
|
||||
export async function fetchUnreadMessages(): Promise<number> {
|
||||
if (getSession() == null) return 0
|
||||
return count(await requestJson('/messages/unread-count', 'GET', {}))
|
||||
}
|
||||
export async function fetchMessages(type: string, beforeId: string = '', snapshotId: string = ''): Promise<MessagePage> {
|
||||
const params: UTSJSONObject = { type, limit: 20 }
|
||||
if (beforeId.length > 0) { params['beforeId'] = beforeId; params['snapshotId'] = snapshotId }
|
||||
const data = await requestJson('/messages', 'GET', params)
|
||||
const items = data['items'] as UTSJSONObject[]
|
||||
return { items: items.map((item: UTSJSONObject): UserMessage => messageFrom(item)), snapshotId: data.getString('snapshotId') ?? '',
|
||||
nextBeforeId: data.getString('nextBeforeId') ?? '', unreadCount: count(data) }
|
||||
}
|
||||
export async function fetchMessage(id: string): Promise<MessageDetail> {
|
||||
if (!validId(id)) throw new Error('消息编号无效')
|
||||
const data = await requestJson('/messages/' + id, 'GET', {})
|
||||
const target = data['target'] as UTSJSONObject | null
|
||||
let targetUrl = ''; let targetLabel = ''
|
||||
if (target != null) {
|
||||
const assessmentId = target.getString('assessmentId') ?? ''; const revisionId = target.getString('revisionId') ?? ''
|
||||
const type = target.getString('type') ?? ''; const kind = target.getString('kind') ?? ''
|
||||
if (!validId(assessmentId) || !validId(revisionId) || !['personal', 'enterprise'].includes(type)) throw new Error('关联评估格式无效')
|
||||
// 只构造白名单路由,通知不能下发任意链接;历史失败也只打开原提交版本。
|
||||
if (kind == 'report') { targetUrl = '/packages/finance/report/report?assessmentId=' + assessmentId + '&revisionId=' + revisionId; targetLabel = '查看报告' }
|
||||
else if (kind == 'revision') { targetUrl = '/packages/finance/archive/archive?assessmentId=' + assessmentId + '&revisionId=' + revisionId + '&type=' + type; targetLabel = '查看评估' }
|
||||
else throw new Error('关联评估格式无效')
|
||||
}
|
||||
return { item: messageFrom(data['item'] as UTSJSONObject), targetUrl, targetLabel, targetUnavailable: data.getBoolean('targetUnavailable') ?? false }
|
||||
}
|
||||
export async function readMessage(id: string): Promise<number> { return count(await requestJson('/messages/' + id + '/read', 'POST', {})) }
|
||||
export async function readAllMessages(snapshotId: string): Promise<number> { return count(await requestJson('/messages/read-all', 'POST', { snapshotId })) }
|
||||
Reference in New Issue
Block a user