feat(aihr): add M1 broadcast targeting and mandatory reads
This commit is contained in:
@@ -12,6 +12,10 @@
|
||||
<view v-if="loading" class="panel employee-card loading-card" role="status" aria-live="polite">消息加载中…</view>
|
||||
<view v-else-if="item" class="panel employee-card message-detail">
|
||||
<text class="message-title">{{ item.title }}</text>
|
||||
<view class="detail-tags">
|
||||
<text v-if="item.requiredRead" class="detail-tag required">必读消息</text>
|
||||
<text v-if="item.targeted" class="detail-tag related">与我有关{{ item.targetReason ? ` · ${item.targetReason}` : '' }}</text>
|
||||
</view>
|
||||
<text class="message-time">发布时间:{{ displayTime(item.publishedAt) }}</text>
|
||||
<text class="message-content">{{ item.content }}</text>
|
||||
<view class="detail-actions">
|
||||
@@ -115,6 +119,10 @@ onLoad((options) => {
|
||||
.status-message { display: block; margin-top: 16px; padding: 12px; border-radius: 12px; background: #fff0f1; color: #a9000d; line-height: 1.55; }
|
||||
.loading-card, .message-detail { display: grid; gap: 12px; margin-top: 18px; padding: 20px; }
|
||||
.message-title { overflow-wrap: anywhere; color: var(--ink); font-size: 21px; font-weight: 900; line-height: 1.45; }
|
||||
.detail-tags { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.detail-tag { padding: 4px 8px; border-radius: 999px; font-size: 12px; font-weight: 800; }
|
||||
.detail-tag.required { background: #fff0f1; color: #b20d19; }
|
||||
.detail-tag.related { background: #fff7e8; color: #a76500; }
|
||||
.message-time { color: var(--muted); font-size: 12px; }
|
||||
.message-content { overflow-wrap: anywhere; color: var(--ink); font-size: 15px; line-height: 1.8; white-space: pre-wrap; }
|
||||
.detail-actions { display: grid; gap: 8px; margin-top: 8px; padding-top: 16px; border-top: 1px solid var(--employee-line, #ebedf0); }
|
||||
|
||||
@@ -16,6 +16,17 @@
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<view class="scope-tabs" role="tablist" aria-label="消息范围筛选">
|
||||
<button
|
||||
v-for="option in scopeOptions"
|
||||
:key="option.value"
|
||||
class="scope-tab"
|
||||
:class="{ active: scope === option.value }"
|
||||
:aria-selected="scope === option.value"
|
||||
role="tab"
|
||||
@click="changeScope(option.value)"
|
||||
>{{ option.label }}</button>
|
||||
</view>
|
||||
<text v-if="message && !loadFailed" class="status-message" role="status" aria-live="polite">{{ message }}</text>
|
||||
<view v-if="loading" class="panel employee-card loading-card" role="status" aria-live="polite">消息加载中…</view>
|
||||
<view v-else-if="loadFailed" class="panel employee-card empty-card" role="alert" aria-live="assertive">
|
||||
@@ -30,7 +41,11 @@
|
||||
<view v-else class="message-list">
|
||||
<button v-for="item in items" :key="item.id" class="message-card employee-card" @click="openMessage(item.id)">
|
||||
<view class="message-head">
|
||||
<text class="message-state" :class="{ read: item.read }">{{ item.read ? '已读' : '未读' }}</text>
|
||||
<view class="message-tags">
|
||||
<text class="message-state" :class="{ read: item.read }">{{ item.read ? '已读' : '未读' }}</text>
|
||||
<text v-if="item.requiredRead" class="message-tag required">必读</text>
|
||||
<text v-if="item.targeted" class="message-tag related">与我有关</text>
|
||||
</view>
|
||||
<text class="message-time">{{ displayTime(item.publishedAt) }}</text>
|
||||
</view>
|
||||
<text class="message-title">{{ item.title }}</text>
|
||||
@@ -47,7 +62,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { onHide, onShow, onUnload } from '@dcloudio/uni-app';
|
||||
import type { BroadcastMessageSummary } from '@/services/broadcast';
|
||||
import type { BroadcastMessageScope, BroadcastMessageSummary } from '@/services/broadcast';
|
||||
import { listBroadcastMessages } from '@/services/broadcast';
|
||||
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
@@ -64,6 +79,12 @@ const items = ref<BroadcastMessageSummary[]>([]);
|
||||
const total = ref(0);
|
||||
const pageNum = ref(1);
|
||||
const moreAvailable = ref(false);
|
||||
const scope = ref<BroadcastMessageScope>('ALL');
|
||||
const scopeOptions: Array<{ value: BroadcastMessageScope; label: string }> = [
|
||||
{ value: 'ALL', label: '全部' },
|
||||
{ value: 'RELATED', label: '与我有关' },
|
||||
{ value: 'REQUIRED_UNREAD', label: '必读未读' }
|
||||
];
|
||||
const requests = createPageRequestScope();
|
||||
const hasMore = computed(() => moreAvailable.value && items.value.length < total.value);
|
||||
|
||||
@@ -112,7 +133,7 @@ const load = async () => {
|
||||
const request = requests.begin('broadcast-list');
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await listBroadcastMessages(1, pageSize);
|
||||
const result = await listBroadcastMessages(1, pageSize, scope.value);
|
||||
if (!requests.isCurrent(request)) return;
|
||||
applyFirstPage(result);
|
||||
} catch (error) {
|
||||
@@ -131,7 +152,7 @@ const loadMore = async () => {
|
||||
loadingMore.value = true;
|
||||
message.value = '';
|
||||
try {
|
||||
const result = await listBroadcastMessages(nextPageNum, pageSize);
|
||||
const result = await listBroadcastMessages(nextPageNum, pageSize, scope.value);
|
||||
if (!requests.isCurrent(request)) return;
|
||||
appendPage(result, nextPageNum);
|
||||
} catch (error) {
|
||||
@@ -144,6 +165,12 @@ const loadMore = async () => {
|
||||
|
||||
const openMessage = (id: number) => uni.navigateTo({ url: `/pages/user/broadcast/detail?id=${id}` });
|
||||
|
||||
const changeScope = (nextScope: BroadcastMessageScope) => {
|
||||
if (scope.value === nextScope || loading.value || loadingMore.value) return;
|
||||
scope.value = nextScope;
|
||||
void load();
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
requests.activate();
|
||||
resetPageScroll();
|
||||
@@ -166,10 +193,17 @@ onUnload(() => {
|
||||
.status-message { display: block; margin-top: 16px; padding: 12px; border-radius: 12px; background: #fff0f1; color: #a9000d; }
|
||||
.empty-title { font-size: 17px; font-weight: 900; }
|
||||
.message-list { display: grid; gap: 12px; margin-top: 18px; }
|
||||
.scope-tabs { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 16px; padding: 4px; border-radius: 14px; background: #f3f4f6; }
|
||||
.scope-tab { min-height: 36px; border-radius: 10px; background: transparent; color: var(--muted); font-size: 13px; font-weight: 800; }
|
||||
.scope-tab.active { background: #fff; color: var(--brand); box-shadow: 0 1px 4px rgba(20, 28, 42, .12); }
|
||||
.message-card { display: grid; width: 100%; gap: 10px; padding: 16px; color: var(--ink); text-align: left; }
|
||||
.message-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.message-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
|
||||
.message-state { padding: 3px 8px; border-radius: 999px; background: #fff0f1; color: var(--brand); font-size: 12px; font-weight: 900; }
|
||||
.message-state.read { background: #f2f3f5; color: var(--muted); }
|
||||
.message-tag { padding: 3px 7px; border-radius: 999px; font-size: 11px; font-weight: 800; }
|
||||
.message-tag.required { background: #fff0f1; color: #b20d19; }
|
||||
.message-tag.related { background: #fff7e8; color: #a76500; }
|
||||
.message-time { color: var(--muted); font-size: 12px; }
|
||||
.message-title { overflow-wrap: anywhere; font-size: 17px; font-weight: 900; line-height: 1.5; }
|
||||
.load-more { width: 100%; min-height: 42px; border: 1px solid #f0a6ac; border-radius: 999px; background: #fff; color: var(--brand); font-size: 14px; font-weight: 800; }
|
||||
|
||||
@@ -3,6 +3,8 @@ import { apiRequest } from './api';
|
||||
export const broadcastQuestionContextStorageKey = 'aihr_broadcast_question_context';
|
||||
export const broadcastQuestionContextTtlMs = 15 * 60 * 1000;
|
||||
|
||||
export type BroadcastMessageScope = 'ALL' | 'RELATED' | 'REQUIRED_UNREAD';
|
||||
|
||||
export interface BroadcastQuestionContext {
|
||||
messageId: number;
|
||||
accountKey: string;
|
||||
@@ -14,6 +16,9 @@ export interface BroadcastMessageSummary {
|
||||
title: string;
|
||||
read: boolean;
|
||||
publishedAt: string;
|
||||
requiredRead: boolean;
|
||||
targeted: boolean;
|
||||
targetReason?: string;
|
||||
}
|
||||
|
||||
export interface BroadcastMessage extends BroadcastMessageSummary {
|
||||
@@ -36,9 +41,9 @@ export const getBroadcastUnreadCount = async () => {
|
||||
return Math.max(0, Number(result?.unreadCount) || 0);
|
||||
};
|
||||
|
||||
export const listBroadcastMessages = (pageNum = 1, pageSize = 20) =>
|
||||
export const listBroadcastMessages = (pageNum = 1, pageSize = 20, scope: BroadcastMessageScope = 'ALL') =>
|
||||
apiRequest<BroadcastMessagePage>({
|
||||
url: `/api/aihr/broadcast/messages?pageNum=${pageNum}&pageSize=${pageSize}`,
|
||||
url: `/api/aihr/broadcast/messages?pageNum=${pageNum}&pageSize=${pageSize}&scope=${scope}`,
|
||||
method: 'GET',
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ test('频道支持失败重试、分页和请求失效保护', async () => {
|
||||
assert.match(list, /@click="load">重新加载/);
|
||||
assert.match(list, /v-if="hasMore" class="load-more"/);
|
||||
assert.match(list, /const loadMore = async \(\)/);
|
||||
assert.match(list, /listBroadcastMessages\(nextPageNum, pageSize\)/);
|
||||
assert.match(list, /listBroadcastMessages\(nextPageNum, pageSize, scope\.value\)/);
|
||||
assert.match(list, /createPageRequestScope/);
|
||||
assert.match(list, /const clearLoading = \(\) => \{[\s\S]*loadingMore\.value = false;/);
|
||||
assert.match(list, /onHide\(\(\) => \{[\s\S]*clearLoading\(\);[\s\S]*requests\.invalidate\(\);/);
|
||||
|
||||
Reference in New Issue
Block a user