- 以 D:\SmartSenior 生产工程为源,纳入 AppScope、entry、cjagent、hvigor 及根配置 - 移除 build-profile.json5 的 signingConfigs(p12/p7b/cer 路径与加密口令)及 products.signingConfig 引用 - 不纳入本地与开发产物:local.properties、.idea、.hvigor、.codegenie、demo、backup_a2a_native、SESSION_LOG.md、build_err.txt、build/、oh_modules - .gitignore 追加 .codegenie 与密钥文件通配(*.p12/*.p7b/*.cer/*.jks/*.keystore/*.pem)
300 lines
10 KiB
Plaintext
300 lines
10 KiB
Plaintext
// 消息通知页(ArkTS 严格类型,无 any)
|
|
// 数据来源:/auth/notifications/(用户通知)+ /alerts/(预警),合并时间线
|
|
import { promptAction } from '@kit.ArkUI';
|
|
import { HdsNavigation, HdsNavigationTitleMode, ScrollEffectType } from '@kit.UIDesignKit';
|
|
import { hdsMaterial } from '@hms.hds.hdsMaterial';
|
|
import { BusinessError } from '@kit.BasicServicesKit';
|
|
import { Page, normalizeError } from '../../common/ApiConstants';
|
|
import { api, EmptyBody } from '../../utils/Request';
|
|
import { FontScaleLevel, FONT_SCALE_MAP } from '../../utils/FontScale';
|
|
import { AuthStore } from '../../utils/AuthStore';
|
|
|
|
interface UserNotice {
|
|
id: number;
|
|
title: string;
|
|
content: string;
|
|
is_read: boolean;
|
|
created_at: string;
|
|
}
|
|
|
|
interface AlertItem {
|
|
id: number;
|
|
title: string;
|
|
description: string;
|
|
created_at: string;
|
|
}
|
|
|
|
// 合并后的信息流条目(source 区分类型)
|
|
interface FeedItem {
|
|
key: string;
|
|
source: string; // 'user' | 'alert'
|
|
id: number;
|
|
title: string;
|
|
content: string;
|
|
isRead: boolean;
|
|
createdAt: string;
|
|
}
|
|
|
|
function formatDateTime(iso: string): string {
|
|
const d = new Date(iso);
|
|
if (Number.isNaN(d.getTime())) {
|
|
return '--';
|
|
}
|
|
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
const dd = String(d.getDate()).padStart(2, '0');
|
|
const hh = String(d.getHours()).padStart(2, '0');
|
|
const mi = String(d.getMinutes()).padStart(2, '0');
|
|
return `${mm}/${dd} ${hh}:${mi}`;
|
|
}
|
|
|
|
@Entry
|
|
@Component
|
|
struct Notifications {
|
|
@StorageProp('fontScale') fontScaleLevel: FontScaleLevel = 'system';
|
|
@State feed: Array<FeedItem> = [];
|
|
@State loading: boolean = true;
|
|
@State markingAll: boolean = false;
|
|
|
|
private scroller: Scroller = new Scroller();
|
|
|
|
private scaleFor(): number {
|
|
return FONT_SCALE_MAP[this.fontScaleLevel] ?? 1.0;
|
|
}
|
|
|
|
aboutToAppear(): void {
|
|
this.loadData();
|
|
}
|
|
|
|
private async loadData(): Promise<void> {
|
|
this.loading = true;
|
|
try {
|
|
// 护理预警仅限护理/管理角色:消防、销售不拉取 /alerts/
|
|
const state = await AuthStore.getState();
|
|
const role = state.role;
|
|
const includeAlerts = role === null || role === 'admin' || role === 'nurse' || role === 'staff_nurse';
|
|
const responses: Array<Page<UserNotice> | Page<AlertItem> | null> = await Promise.all([
|
|
api.get<Page<UserNotice>>('/auth/notifications/', { page_size: 30 }).catch((): Page<UserNotice> | null => null),
|
|
includeAlerts
|
|
? api.get<Page<AlertItem>>('/alerts/', { page_size: 30, ordering: '-created_at' }).catch((): Page<AlertItem> | null => null)
|
|
: Promise.resolve(null),
|
|
]);
|
|
const noticeRes = responses[0] as Page<UserNotice> | null;
|
|
const alertRes = responses[1] as Page<AlertItem> | null;
|
|
const items: Array<FeedItem> = [];
|
|
if (noticeRes) {
|
|
noticeRes.results.forEach((n: UserNotice) => {
|
|
items.push({
|
|
key: `user-${n.id}`,
|
|
source: 'user',
|
|
id: n.id,
|
|
title: n.title,
|
|
content: n.content,
|
|
isRead: n.is_read,
|
|
createdAt: n.created_at,
|
|
});
|
|
});
|
|
}
|
|
if (alertRes) {
|
|
alertRes.results.forEach((a: AlertItem) => {
|
|
items.push({
|
|
key: `alert-${a.id}`,
|
|
source: 'alert',
|
|
id: a.id,
|
|
title: a.title,
|
|
content: a.description,
|
|
isRead: true,
|
|
createdAt: a.created_at,
|
|
});
|
|
});
|
|
}
|
|
items.sort((a: FeedItem, b: FeedItem): number => {
|
|
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
});
|
|
this.feed = items;
|
|
} catch (err) {
|
|
promptAction.showToast({ message: normalizeError(err as BusinessError | Error) });
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
private async markRead(item: FeedItem): Promise<void> {
|
|
if (item.source !== 'user' || item.isRead) {
|
|
return;
|
|
}
|
|
try {
|
|
await api.post(`/auth/notifications/${item.id}/mark-read/`, new EmptyBody());
|
|
this.feed = this.feed.map((f: FeedItem): FeedItem => {
|
|
if (f.key === item.key) {
|
|
return { key: f.key, source: f.source, id: f.id, title: f.title, content: f.content, isRead: true, createdAt: f.createdAt } as FeedItem;
|
|
}
|
|
return f;
|
|
});
|
|
} catch (err) {
|
|
console.error('[Notifications] mark read failed:', normalizeError(err as BusinessError | Error));
|
|
}
|
|
}
|
|
|
|
private async markAllRead(): Promise<void> {
|
|
if (this.markingAll) {
|
|
return;
|
|
}
|
|
this.markingAll = true;
|
|
try {
|
|
await api.post('/auth/notifications/mark-all-read/', new EmptyBody());
|
|
this.feed = this.feed.map((f: FeedItem): FeedItem =>
|
|
({ key: f.key, source: f.source, id: f.id, title: f.title, content: f.content, isRead: true, createdAt: f.createdAt }) as FeedItem);
|
|
promptAction.showToast({ message: '已全部标记为已读' });
|
|
} catch (err) {
|
|
promptAction.showToast({ message: normalizeError(err as BusinessError | Error) });
|
|
} finally {
|
|
this.markingAll = false;
|
|
}
|
|
}
|
|
|
|
private hasUnread(): boolean {
|
|
return this.feed.some((f: FeedItem) => f.source === 'user' && !f.isRead);
|
|
}
|
|
|
|
@Builder
|
|
FeedCard(key: string) {
|
|
Column({ space: 8 }) {
|
|
Row({ space: 8 }) {
|
|
if (!this.feedItem(key).isRead) {
|
|
Circle()
|
|
.width(8)
|
|
.height(8)
|
|
.fill($r('app.color.accent_red'))
|
|
}
|
|
Text(this.feedItem(key).source === 'user' ? '通知' : '预警')
|
|
.fontSize(Math.round(11 * this.scaleFor()))
|
|
.fontColor(this.feedItem(key).source === 'user' ? $r('app.color.accent_blue') : '#FF9500')
|
|
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
|
|
.borderRadius(6)
|
|
.border({ width: 1, color: this.feedItem(key).source === 'user' ? $r('app.color.accent_blue') : '#FF9500' })
|
|
Blank()
|
|
Text(formatDateTime(this.feedItem(key).createdAt))
|
|
.fontSize(Math.round(12 * this.scaleFor()))
|
|
.fontColor($r('app.color.inactive_tab'))
|
|
}
|
|
.width('100%')
|
|
|
|
Text(this.feedItem(key).title)
|
|
.fontSize(Math.round(15 * this.scaleFor()))
|
|
.fontWeight(FontWeight.Medium)
|
|
.fontColor($r('app.color.text_primary'))
|
|
.maxLines(1)
|
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
.width('100%')
|
|
|
|
if (this.feedItem(key).content && this.feedItem(key).content.length > 0) {
|
|
Text(this.feedItem(key).content)
|
|
.fontSize(Math.round(13 * this.scaleFor()))
|
|
.fontColor($r('app.color.text_secondary'))
|
|
.maxLines(2)
|
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
.width('100%')
|
|
}
|
|
}
|
|
.width('100%')
|
|
.padding(16)
|
|
.backgroundColor($r('app.color.background_card'))
|
|
.borderRadius(14)
|
|
.alignItems(HorizontalAlign.Start)
|
|
.onClick(() => {
|
|
this.markRead(this.feedItem(key));
|
|
})
|
|
}
|
|
|
|
// 按 key 从 @State feed 读取(保证 markAllRead 后重渲红点)
|
|
private feedItem(key: string): FeedItem {
|
|
const found = this.feed.find((f: FeedItem) => f.key === key);
|
|
if (found) {
|
|
return found;
|
|
}
|
|
return { key: key, source: 'user', id: 0, title: '', content: '', isRead: true, createdAt: '' };
|
|
}
|
|
|
|
build() {
|
|
HdsNavigation() {
|
|
Column() {
|
|
if (this.loading) {
|
|
Text('加载中...')
|
|
.fontSize(Math.round(14 * this.scaleFor()))
|
|
.fontColor($r('app.color.inactive_tab'))
|
|
.margin({ top: 60 })
|
|
} else if (this.feed.length === 0) {
|
|
Column({ space: 8 }) {
|
|
Text('🔔')
|
|
.fontSize(Math.round(32 * this.scaleFor()))
|
|
Text('暂无消息通知')
|
|
.fontSize(Math.round(15 * this.scaleFor()))
|
|
.fontColor($r('app.color.text_secondary'))
|
|
}
|
|
.width('100%')
|
|
.margin({ top: 80 })
|
|
.alignItems(HorizontalAlign.Center)
|
|
} else {
|
|
Scroll(this.scroller) {
|
|
Column({ space: 10 }) {
|
|
if (this.hasUnread()) {
|
|
Row() {
|
|
Text(`共 ${this.feed.length} 条 · 有新通知`)
|
|
.fontSize(Math.round(13 * this.scaleFor()))
|
|
.fontColor($r('app.color.text_secondary'))
|
|
Blank()
|
|
Text(this.markingAll ? '处理中...' : '全部已读')
|
|
.fontSize(Math.round(14 * this.scaleFor()))
|
|
.fontColor($r('app.color.accent_blue'))
|
|
.onClick(() => {
|
|
this.markAllRead();
|
|
})
|
|
}
|
|
.width('100%')
|
|
.padding({ left: 4, right: 4, top: 8, bottom: 8 })
|
|
}
|
|
ForEach(this.feed, (item: FeedItem) => {
|
|
this.FeedCard(item.key)
|
|
}, (item: FeedItem) => item.key)
|
|
}
|
|
.width('100%')
|
|
.padding({ left: 16, right: 16, top: 108, bottom: 24 })
|
|
}
|
|
.width('100%')
|
|
.layoutWeight(1)
|
|
.scrollBar(BarState.Auto)
|
|
.edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
|
|
}
|
|
}
|
|
.width('100%')
|
|
.height('100%')
|
|
}
|
|
.mode(NavigationMode.Stack)
|
|
.titleBar({
|
|
content: {
|
|
title: {
|
|
mainTitle: '消息通知',
|
|
},
|
|
},
|
|
style: {
|
|
scrollEffectOpts: {
|
|
enableScrollEffect: false,
|
|
scrollEffectType: ScrollEffectType.GRADIENT_BLUR,
|
|
},
|
|
systemMaterialEffect: {
|
|
materialType: hdsMaterial.MaterialType.ADAPTIVE,
|
|
materialLevel: hdsMaterial.MaterialLevel.EXQUISITE,
|
|
},
|
|
},
|
|
avoidLayoutSafeArea: false,
|
|
enableComponentSafeArea: false,
|
|
})
|
|
.bindToScrollable([this.scroller])
|
|
.hideBackButton(false)
|
|
.titleMode(HdsNavigationTitleMode.MINI)
|
|
.ignoreLayoutSafeArea([LayoutSafeAreaType.SYSTEM], [LayoutSafeAreaEdge.TOP, LayoutSafeAreaEdge.BOTTOM])
|
|
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
|
|
.backgroundColor($r('app.color.background'))
|
|
}
|
|
}
|