feat(mobile-uni): 彻底完成登录页、首页、学习中心与问答页UI/UX极简重构及单条消息追问功能
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<view class="page employee-page-shell detail-page">
|
||||
<view class="subpage-header">
|
||||
<button class="subpage-back-button" aria-label="返回银城大喇叭" @click="goBack"><uni-icons type="back" size="20" color="#25282d" /></button>
|
||||
<view class="subpage-heading">
|
||||
<text class="employee-kicker">公司消息</text>
|
||||
<text class="page-title">银城大喇叭</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text v-if="message" class="status-message" role="alert" aria-live="assertive">{{ message }}</text>
|
||||
<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>
|
||||
<text class="message-time">发布时间:{{ displayTime(item.publishedAt) }}</text>
|
||||
<text class="message-content">{{ item.content }}</text>
|
||||
<view class="detail-actions">
|
||||
<button class="ask-message-button" @click="askAboutMessage">问这条消息</button>
|
||||
<text class="ask-message-note">将在“问”中围绕本条消息继续咨询</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import type { BroadcastMessage } from '@/services/broadcast';
|
||||
import {
|
||||
clearBroadcastQuestionContext,
|
||||
getBroadcastMessage,
|
||||
markBroadcastMessageRead,
|
||||
readBroadcastQuestionContext,
|
||||
saveBroadcastQuestionContext
|
||||
} from '@/services/broadcast';
|
||||
import { ApiError } from '@/services/api';
|
||||
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { currentAccountKey } from '@/services/request-scope';
|
||||
|
||||
const id = ref(0);
|
||||
const item = ref<BroadcastMessage | null>(null);
|
||||
const loading = ref(false);
|
||||
const message = ref('');
|
||||
|
||||
const goBack = () => uni.navigateBack({ delta: 1, fail: () => uni.switchTab({ url: '/pages/user/today/index' }) });
|
||||
const displayTime = (value: string) => value ? value.replace('T', ' ').slice(0, 16) : '';
|
||||
const isBroadcastMessageUnavailable = (error: unknown) =>
|
||||
error instanceof ApiError && (error.statusCode === 404 || error.bizCode === 404);
|
||||
|
||||
const askAboutMessage = () => {
|
||||
if (!item.value || !ensureLogin()) return;
|
||||
const saved = saveBroadcastQuestionContext(item.value.id, currentAccountKey());
|
||||
if (!saved) {
|
||||
uni.showToast({ title: '暂时无法打开消息咨询,请稍后重试', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.switchTab({
|
||||
url: '/pages/user/sop/index',
|
||||
fail: () => uni.showToast({ title: '无法打开“问”', icon: 'none' })
|
||||
});
|
||||
};
|
||||
|
||||
const ensureLogin = () => {
|
||||
if (isLoggedIn()) return true;
|
||||
rememberLoginRedirect(`/pages/user/broadcast/detail?id=${id.value}`);
|
||||
uni.redirectTo({ url: '/pages/auth/login/index' });
|
||||
return false;
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
if (!Number.isSafeInteger(id.value) || id.value <= 0) {
|
||||
item.value = null;
|
||||
message.value = '消息不存在或链接无效';
|
||||
return;
|
||||
}
|
||||
if (!ensureLogin()) return;
|
||||
loading.value = true;
|
||||
message.value = '';
|
||||
try {
|
||||
const next = await getBroadcastMessage(id.value);
|
||||
item.value = next;
|
||||
try {
|
||||
await markBroadcastMessageRead(id.value);
|
||||
item.value = { ...next, read: true };
|
||||
} catch (error) {
|
||||
message.value = error instanceof Error ? `消息已打开,但标记已读失败:${error.message}` : '消息已打开,但标记已读失败';
|
||||
}
|
||||
} catch (error) {
|
||||
item.value = null;
|
||||
// If this is the same consultation handoff, do not leave a now-withdrawn
|
||||
// message bound when the employee returns to the existing Ask tab.
|
||||
if (isBroadcastMessageUnavailable(error)
|
||||
&& readBroadcastQuestionContext(currentAccountKey())?.messageId === id.value) {
|
||||
clearBroadcastQuestionContext();
|
||||
}
|
||||
message.value = error instanceof Error ? error.message : '消息读取失败';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onLoad((options) => {
|
||||
const nextId = Number(options?.id || 0);
|
||||
if (!Number.isSafeInteger(nextId) || nextId <= 0) {
|
||||
message.value = '消息不存在或链接无效';
|
||||
return;
|
||||
}
|
||||
id.value = nextId;
|
||||
void load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.detail-page { max-width: var(--mobile-shell-max-width); margin: 0 auto; padding-bottom: calc(28px + env(safe-area-inset-bottom)); }
|
||||
.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; }
|
||||
.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); }
|
||||
.ask-message-button { width: 100%; min-height: 46px; border-radius: 14px; background: var(--employee-primary, #e60012); color: #fff; font-size: 15px; font-weight: 800; }
|
||||
.ask-message-note { color: var(--muted); font-size: 12px; line-height: 1.5; text-align: center; }
|
||||
</style>
|
||||
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<view class="page employee-page-shell broadcast-page">
|
||||
<view class="subpage-header">
|
||||
<button class="subpage-back-button" aria-label="返回今日" @click="goBack"><uni-icons type="back" size="20" color="#25282d" /></button>
|
||||
<view class="subpage-heading">
|
||||
<text class="employee-kicker">公司消息</text>
|
||||
<text class="page-title">银城大喇叭</text>
|
||||
<text class="page-subtitle">查看公司发布的通知、制度和重要提醒。</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!loggedIn" class="panel employee-card login-panel">
|
||||
<text class="panel-title">登录后查看</text>
|
||||
<text class="muted">消息只会按当前登录账号读取。</text>
|
||||
<button class="primary-button" @click="goLogin">去登录</button>
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<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">
|
||||
<text class="empty-title">消息加载失败</text>
|
||||
<text class="muted">{{ message || '请检查网络后重试。' }}</text>
|
||||
<button class="primary-button" @click="load">重新加载</button>
|
||||
</view>
|
||||
<view v-else-if="!items.length" class="panel employee-card empty-card">
|
||||
<text class="empty-title">暂时没有消息</text>
|
||||
<text class="muted">新的公司通知会显示在这里。</text>
|
||||
</view>
|
||||
<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>
|
||||
<text class="message-time">{{ displayTime(item.publishedAt) }}</text>
|
||||
</view>
|
||||
<text class="message-title">{{ item.title }}</text>
|
||||
</button>
|
||||
<button v-if="hasMore" class="load-more" :disabled="loadingMore" @click="loadMore">
|
||||
{{ loadingMore ? '加载中…' : '加载更多' }}
|
||||
</button>
|
||||
<text v-else class="list-end">已加载全部 {{ items.length }} 条消息</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { onHide, onShow, onUnload } from '@dcloudio/uni-app';
|
||||
import type { BroadcastMessageSummary } from '@/services/broadcast';
|
||||
import { listBroadcastMessages } from '@/services/broadcast';
|
||||
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
import { createPageRequestScope } from '@/services/request-scope';
|
||||
|
||||
const path = '/pages/user/broadcast/index';
|
||||
const pageSize = 20;
|
||||
const loggedIn = ref(false);
|
||||
const loading = ref(false);
|
||||
const loadingMore = ref(false);
|
||||
const loadFailed = ref(false);
|
||||
const message = ref('');
|
||||
const items = ref<BroadcastMessageSummary[]>([]);
|
||||
const total = ref(0);
|
||||
const pageNum = ref(1);
|
||||
const moreAvailable = ref(false);
|
||||
const requests = createPageRequestScope();
|
||||
const hasMore = computed(() => moreAvailable.value && items.value.length < total.value);
|
||||
|
||||
const clearLoading = () => {
|
||||
loading.value = false;
|
||||
loadingMore.value = false;
|
||||
};
|
||||
|
||||
const goBack = () => uni.navigateBack({ delta: 1, fail: () => uni.switchTab({ url: '/pages/user/today/index' }) });
|
||||
const goLogin = () => {
|
||||
rememberLoginRedirect(path);
|
||||
uni.redirectTo({ url: '/pages/auth/login/index' });
|
||||
};
|
||||
|
||||
const displayTime = (value: string) => value ? value.replace('T', ' ').slice(0, 16) : '';
|
||||
|
||||
const pageRows = (rows: BroadcastMessageSummary[] | undefined) => Array.isArray(rows) ? rows : [];
|
||||
|
||||
const applyFirstPage = (result: Awaited<ReturnType<typeof listBroadcastMessages>>) => {
|
||||
const rows = pageRows(result.rows);
|
||||
items.value = rows;
|
||||
total.value = Math.max(rows.length, Number(result.total) || 0);
|
||||
pageNum.value = Math.max(1, Number(result.pageNum) || 1);
|
||||
moreAvailable.value = rows.length > 0 && rows.length < total.value;
|
||||
};
|
||||
|
||||
const appendPage = (result: Awaited<ReturnType<typeof listBroadcastMessages>>, fallbackPageNum: number) => {
|
||||
const rows = pageRows(result.rows);
|
||||
const seen = new Set(items.value.map((item) => item.id));
|
||||
items.value = [...items.value, ...rows.filter((item) => !seen.has(item.id))];
|
||||
total.value = Math.max(items.value.length, Number(result.total) || 0);
|
||||
pageNum.value = Math.max(fallbackPageNum, Number(result.pageNum) || fallbackPageNum);
|
||||
moreAvailable.value = rows.length > 0 && items.value.length < total.value;
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
loggedIn.value = isLoggedIn();
|
||||
items.value = [];
|
||||
total.value = 0;
|
||||
pageNum.value = 1;
|
||||
moreAvailable.value = false;
|
||||
loadFailed.value = false;
|
||||
message.value = '';
|
||||
clearLoading();
|
||||
if (!loggedIn.value) return;
|
||||
const request = requests.begin('broadcast-list');
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await listBroadcastMessages(1, pageSize);
|
||||
if (!requests.isCurrent(request)) return;
|
||||
applyFirstPage(result);
|
||||
} catch (error) {
|
||||
if (!requests.isCurrent(request)) return;
|
||||
loadFailed.value = true;
|
||||
message.value = error instanceof Error ? error.message : '消息加载失败';
|
||||
} finally {
|
||||
if (requests.isCurrent(request)) loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadMore = async () => {
|
||||
if (loading.value || loadingMore.value || !hasMore.value) return;
|
||||
const nextPageNum = pageNum.value + 1;
|
||||
const request = requests.begin('broadcast-list');
|
||||
loadingMore.value = true;
|
||||
message.value = '';
|
||||
try {
|
||||
const result = await listBroadcastMessages(nextPageNum, pageSize);
|
||||
if (!requests.isCurrent(request)) return;
|
||||
appendPage(result, nextPageNum);
|
||||
} catch (error) {
|
||||
if (!requests.isCurrent(request)) return;
|
||||
message.value = error instanceof Error ? error.message : '更多消息加载失败';
|
||||
} finally {
|
||||
if (requests.isCurrent(request)) loadingMore.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openMessage = (id: number) => uni.navigateTo({ url: `/pages/user/broadcast/detail?id=${id}` });
|
||||
|
||||
onShow(() => {
|
||||
requests.activate();
|
||||
resetPageScroll();
|
||||
void load();
|
||||
});
|
||||
|
||||
onHide(() => {
|
||||
clearLoading();
|
||||
requests.invalidate();
|
||||
});
|
||||
onUnload(() => {
|
||||
clearLoading();
|
||||
requests.invalidate();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.broadcast-page { max-width: var(--mobile-shell-max-width); margin: 0 auto; padding-bottom: calc(28px + env(safe-area-inset-bottom)); }
|
||||
.login-panel, .loading-card, .empty-card { display: grid; gap: 10px; margin-top: 18px; padding: 20px; }
|
||||
.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; }
|
||||
.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-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-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; }
|
||||
.load-more::after { border: 0; }
|
||||
.list-end { display: block; padding: 8px; color: var(--muted); font-size: 12px; text-align: center; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user