feat: 移动端双Token队列拦截器 + 通知列表 + 登录页 + TabBar红点 + v1.0.1
This commit is contained in:
@ -4,22 +4,150 @@
|
||||
<text class="title">消息通知</text>
|
||||
<text class="subtitle">任务流转和系统通知</text>
|
||||
</view>
|
||||
<view class="empty">
|
||||
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loading" class="center">加载中...</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else-if="notifications.length === 0" class="empty">
|
||||
<text class="empty-icon">🔔</text>
|
||||
<text class="empty-text">暂无新消息</text>
|
||||
</view>
|
||||
|
||||
<!-- 通知列表 -->
|
||||
<view v-else class="list">
|
||||
<view
|
||||
v-for="item in notifications"
|
||||
:key="item.id"
|
||||
:class="['card', item.is_read ? '' : 'card-unread']"
|
||||
@tap="handleCardTap(item)"
|
||||
>
|
||||
<view class="card-left">
|
||||
<view v-if="!item.is_read" class="unread-dot" />
|
||||
<text :class="['type-icon', item.is_read ? 'type-icon-read' : '']">
|
||||
{{ typeIcon(item.type) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="card-top">
|
||||
<text :class="['card-title', item.is_read ? '' : 'card-title-bold']">
|
||||
{{ typeTitle(item.type) }}
|
||||
</text>
|
||||
<text class="card-time">{{ formatTime(item.created_at) }}</text>
|
||||
</view>
|
||||
<text class="card-content">{{ item.content }}</text>
|
||||
</view>
|
||||
<text class="card-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import { getNotifications, markNotificationRead } from "../../api/notification";
|
||||
|
||||
const TYPE_CONFIG = {
|
||||
TRANSFER: { icon: "🟢", title: "新任务派发" },
|
||||
REJECT: { icon: "🔴", title: "品质驳回提醒" },
|
||||
};
|
||||
|
||||
const notifications = ref([]);
|
||||
const loading = ref(true);
|
||||
let currentUser = null;
|
||||
|
||||
onShow(() => {
|
||||
loadUser();
|
||||
setTimeout(() => {
|
||||
if (!currentUser) loadUser();
|
||||
fetchNotifications();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
function loadUser() {
|
||||
try {
|
||||
let user = uni.getStorageSync("user");
|
||||
if (typeof user === "string" && user) {
|
||||
try { user = JSON.parse(user); } catch (e) { user = null; }
|
||||
}
|
||||
if (user && typeof user === "object") currentUser = user;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function fetchNotifications() {
|
||||
const userId = currentUser?.username || currentUser?.id || "";
|
||||
if (!userId) { loading.value = false; return; }
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getNotifications(userId);
|
||||
notifications.value = res.notifications || [];
|
||||
} catch {
|
||||
notifications.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function typeIcon(type) { return (TYPE_CONFIG[type] || { icon: "📌" }).icon; }
|
||||
function typeTitle(type) { return (TYPE_CONFIG[type] || { title: "系统通知" }).title; }
|
||||
|
||||
function formatTime(t) {
|
||||
if (!t) return "";
|
||||
const d = new Date(t);
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
async function handleCardTap(item) {
|
||||
if (!item.is_read) {
|
||||
try { await markNotificationRead(item.id); item.is_read = true; } catch {}
|
||||
}
|
||||
if (item.task_id) {
|
||||
uni.navigateTo({ url: `/pages/scan/detail?taskId=${item.task_id}` });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; padding: 16px; padding-bottom: 80px; }
|
||||
.header { margin-bottom: 24px; }
|
||||
.page { min-height: 100vh; padding: 16px; padding-bottom: 80px; background: #f3f4f6; }
|
||||
.header { margin-bottom: 20px; }
|
||||
.title { font-size: 20px; font-weight: 700; color: #1f2937; display: block; }
|
||||
.subtitle { font-size: 13px; color: #9ca3af; margin-top: 4px; display: block; }
|
||||
|
||||
.center { text-align: center; padding: 80px 0; color: #9ca3af; font-size: 14px; }
|
||||
|
||||
.empty { display: flex; flex-direction: column; align-items: center; padding-top: 80px; }
|
||||
.empty-icon { font-size: 64px; margin-bottom: 12px; }
|
||||
.empty-text { font-size: 14px; color: #9ca3af; }
|
||||
|
||||
.list { display: flex; flex-direction: column; gap: 10px; }
|
||||
.card {
|
||||
display: flex; align-items: flex-start; gap: 10px;
|
||||
background: #fff; border-radius: 12px; padding: 14px 12px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||||
position: relative; transition: all 0.2s;
|
||||
}
|
||||
.card:active { transform: scale(0.98); }
|
||||
.card-unread {
|
||||
box-shadow: 0 1px 6px rgba(37,99,235,0.1);
|
||||
border-left: 3px solid #2563eb;
|
||||
}
|
||||
|
||||
.card-left { display: flex; flex-direction: column; align-items: center; gap: 4px; width: 28px; flex-shrink: 0; }
|
||||
.unread-dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
background: #ef4444; box-shadow: 0 0 0 3px rgba(239,68,68,0.15);
|
||||
}
|
||||
.type-icon { font-size: 20px; line-height: 1; }
|
||||
.type-icon-read { opacity: 0.5; }
|
||||
|
||||
.card-body { flex: 1; min-width: 0; }
|
||||
.card-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
|
||||
.card-title { font-size: 15px; font-weight: 600; color: #374151; }
|
||||
.card-title-bold { color: #1f2937; font-weight: 700; }
|
||||
.card-time { font-size: 11px; color: #9ca3af; flex-shrink: 0; }
|
||||
.card-content { font-size: 13px; color: #6b7280; line-height: 1.5; display: block; word-break: break-all; }
|
||||
|
||||
.card-arrow { font-size: 20px; color: #d1d5db; margin-top: 6px; flex-shrink: 0; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user