This commit is contained in:
2026-09-24 16:25:22 +08:00
commit 7428184f01
1198 changed files with 314515 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
<template>
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="通讯录" @right-click="onSearch">
<template v-slot:right>
<x-icon name="search-line" color="#6B7280" font-size="34rpx"></x-icon>
</template>
<view class="hr-body">
<!-- ============ 分组列表:指挥调度 / 协作医院 / 本队同事 ============ -->
<view v-for="(g, gi) in groups" :key="gi">
<hr-sec :title="g.title" :first="gi == 0"></hr-sec>
<!-- 卡片内每行:左侧头像/机构图标 + 姓名与号码 + 呼叫按钮 -->
<hr-card padding="4rpx 26rpx">
<view v-for="(c, ci) in g.items" :key="ci"
:class="ci == g.items.length - 1 ? 'hr-lrow hr-lrow-last' : 'hr-lrow'" @click="onCall(c)">
<view class="ct-blk" :style="blkStyle(c)">
<!-- 机构(医院 / 协作单位)用图标,个人用姓名首字 -->
<x-icon v-if="!c.useAvatar" name="hospital-line" color="#2563EB"
font-size="30rpx"></x-icon>
<text v-else class="ct-blk-t">{{ blkText(c) }}</text>
</view>
<view class="hr-lmain">
<text class="hr-lmain-b">{{ c.name }}</text>
<text class="hr-lmain-s">{{ c.phone }}</text>
</view>
<x-icon name="phone-fill" color="#2563EB" font-size="32rpx"></x-icon>
</view>
</hr-card>
</view>
<view style="height:14px"></view>
</view>
</hr-page>
</template>
<script lang="uts" setup>
import { contactGroups } from '@/mock/index.uts'
import { HrContact, HrContactGroup } from '@/mock/types.uts'
/**
* s28 通讯录
* 与设计稿一致:按「指挥调度 / 协作医院 / 本队同事」三段分组,
* 每组 = 小节标题 + 卡片列表行(头像 + 姓名 + 号码 + 呼叫)。
* 数据全部来自 mock/index.uts,换接口只改取数处。
*/
const groups : HrContactGroup[] = contactGroups
/** 头像文字:优先用数据里的字母头,缺省回落到姓名首字 */
function blkText(c : HrContact) : string {
if (c.avatar != '') { return c.avatar }
if (c.name.length > 0) { return c.name.substring(0, 1) }
return '医'
}
/**
* 头像底色。uvue 不支持复合类选择器(.ct-blk.ct-blk-gn),
* 因此主题色用内联样式下发;机构块固定浅蓝底。
*/
function blkStyle(c : HrContact) : string {
if (!c.useAvatar) { return 'background-color:#EFF6FF;' }
if (c.theme == 'gn') { return 'background-color:#059669;' }
if (c.theme == 'ag') { return 'background-color:#D97706;' }
if (c.theme == 'rd') { return 'background-color:#DC2626;' }
if (c.theme == 'gy') { return 'background-color:#94A3B8;' }
return 'background-color:#2563EB;'
}
/** 只保留数字字符(0-9 */
function digitsOnly(s : string) : string {
const table = '0123456789'
let out = ''
for (let i = 0; i < s.length; i++) {
const ch = s.charAt(i)
if (table.indexOf(ch) > -1) { out += ch }
}
return out
}
/**
* 从「值班室 8801 · 手机 138****2201」这类文案里取出可拨号码。
* 规则:按「·」分段,取「标签 号码」中的号码部分,
* 纯数字(不含 * 掩码)、长度≥4 才算号码;「工号 1248」这类直接跳过。
*/
function extractPhone(text : string) : string {
const parts = text.split('·')
for (let i = 0; i < parts.length; i++) {
const seg = parts[i].trim()
const idx = seg.indexOf(' ')
const label = idx >= 0 ? seg.substring(0, idx) : ''
const raw = idx >= 0 ? seg.substring(idx + 1) : seg
if (label.indexOf('工号') > -1) { continue }
const digits = digitsOnly(raw)
const compact = raw.replaceAll(' ', '').replaceAll('-', '')
if (digits.length >= 4 && digits.length == compact.length) { return digits }
}
return ''
}
// ⚠ UTS 不提升函数声明:extractPhone / digitsOnly 必须写在 onCall 之前
function onCall(c : HrContact) : void {
const digits = extractPhone(c.phone)
if (digits == '') {
uni.showToast({ title: '暂无可用号码', icon: 'none' })
return
}
uni.showModal({
title: c.name,
content: `呼叫 ${digits}`,
confirmText: '呼叫',
success: (res) => {
if (!res.confirm) { return }
// #ifdef APP
uni.makePhoneCall({ phoneNumber: digits })
// #endif
// #ifndef APP
uni.showToast({ title: '已复制号码 ' + digits, icon: 'none' })
uni.setClipboardData({ data: digits })
// #endif
}
})
}
function onSearch() : void {
uni.showToast({ title: '搜索联系人 / 医院', icon: 'none' })
}
</script>
<style>
/* 头像 / 机构图标块:设计稿 .avt.sm / .lic30~32px */
.ct-blk {
width: 64rpx;
height: 64rpx;
border-radius: 18rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.ct-blk-t {
font-size: 25rpx;
font-weight: bold;
color: #FFFFFF;
}
</style>
+191
View File
@@ -0,0 +1,191 @@
<template>
<view class="hr-page" style="background-color:#F3F4F6">
<hr-statusbar bg="#FFFFFF"></hr-statusbar>
<view class="ms-nav">
<text class="ms-nav-t">消息</text>
<view class="ms-nav-a" @click="onReadAll">
<x-icon name="check-double-line" color="#6B7280" font-size="16"></x-icon>
<text class="ms-nav-a-t">全部已读</text>
</view>
</view>
<view class="hr-flow">
<scroll-view class="hr-scroll" :scroll-y="true" :show-scrollbar="false">
<view class="hr-body">
<!-- 消息分类筛选(组件库 xRadioButton 分段切换) -->
<view style="margin-bottom:8rpx">
<x-radio-button v-model="chipId" :list="chipTabs" height="64rpx" space="4rpx" round="22rpx"
text-style="font-weight:bold;" bg-color="#E5E7EB" active-color="#2563EB"
active-font-color="#FFFFFF" font-color="#6B7280" font-size="22rpx"></x-radio-button>
</view>
<view v-for="(g, gi) in groups" :key="gi">
<hr-sec :title="g.title" :more="g.count > 0 ? g.count.toString() + ' 条' : ''"></hr-sec>
<hr-card :variant="g.danger ? 'danger' : 'list'">
<hr-lrow v-for="(m, i) in g.items" :key="i" :icon="m.icon" :theme="m.theme" :title="m.title"
:sub="m.sub" :value="m.action" :value-theme="m.theme == 'red' ? 'red' : 'pri'"
:value-bold="true" :is-last="i == g.items.length - 1" @click="onMsgClick(m, gi, i)">
<template v-slot:right>
<view v-if="isUnread(gi, i, m.unread)" class="hr-badge">
<text class="hr-badge-t">{{ m.unread }}</text>
</view>
</template>
</hr-lrow>
</hr-card>
</view>
<hr-empty v-if="groups.length == 0" icon="✉" title="暂无消息"
sub="库存预警、借用审批与系统通知会汇总到这里"></hr-empty>
<view style="height:28rpx"></view>
</view>
</scroll-view>
<!-- 加载中覆盖层(组件库 xLoading -->
<view v-if="loading" class="hr-loading-mask">
<x-loading label="加载中…" :icon-size="'44rpx'" :text-size="'26rpx'" :vertical="true" color="#2563EB"
text-color="#6B7280"></x-loading>
</view>
</view>
<hr-tabbar :active="3"></hr-tabbar>
</view>
</template>
<script lang="uts" setup>
import { ref, computed } from 'vue'
import { keeperMsgGroups } from '@/mock/index.uts'
import { HrMsg, HrMsgGroup } from '@/mock/types.uts'
import { navTo } from '@/utils/nav.uts'
import { mockRequest } from '@/utils/request.uts'
import { markMessagesRead } from '@/utils/unread.uts'
import { RADIO_BUTTON } from '@/uni_modules/tmx-ui/interface.uts'
/**
* s33 消息中心 · 物资端(仓管 Tab「消息」)
* 库存预警自动置顶;借用审批「去审批」直达设备借出台账。
* 角标 = 未读数,阅读后消失:整页查看清 tabbar/入口角标,点开单条清该条角标。
*/
/**
* 消息分类筛选(组件库 xRadioButton 分段切换,替代原 hr-chip 横向滚动)
* ⚠ RADIO_BUTTON 的 id 是 string:统一用下标字符串,选中值即筛选序号。
*/
const chipTabs : RADIO_BUTTON[] = [
{ id: '0', title: '全部 9' } as RADIO_BUTTON,
{ id: '1', title: '库存预警 4' } as RADIO_BUTTON,
{ id: '2', title: '借用审批 3' } as RADIO_BUTTON,
{ id: '3', title: '系统 2' } as RADIO_BUTTON
]
const chipId = ref('0')
/** 由字符串 id 换算分类序号,供分组筛选使用 */
const chip = computed(() : number => {
const i = parseInt(chipId.value)
return isNaN(i) ? 0 : i
})
/** 已读的条目(key = 分组下标-条目下标) */
const readKeys = ref<string[]>([])
const loading = ref(true)
onLoad(() => {
mockRequest(() => {
loading.value = false
})
})
onPageShow(() => {
markMessagesRead()
})
function keyOf(gi : number, i : number) : string {
return gi.toString() + '-' + i.toString()
}
function isRead(gi : number, i : number) : boolean {
return readKeys.value.indexOf(keyOf(gi, i)) > -1
}
function isUnread(gi : number, i : number, n : number) : boolean {
if (n <= 0) { return false }
return !isRead(gi, i)
}
function markRead(gi : number, i : number) : void {
const k = keyOf(gi, i)
if (readKeys.value.indexOf(k) > -1) { return }
const arr = readKeys.value
arr.push(k)
readKeys.value = arr
}
const groups = computed(() : HrMsgGroup[] => {
if (chip.value == 1) {
return [keeperMsgGroups[0]]
}
if (chip.value == 2) {
return [keeperMsgGroups[1]]
}
if (chip.value == 3) {
return [keeperMsgGroups[2]]
}
return keeperMsgGroups
})
function onMsgClick(m : HrMsg, gi : number, i : number) : void {
markRead(gi, i)
if (m.action == '去审批 ') {
navTo('/pages/warehouse/devices')
return
}
uni.showModal({ title: m.title, content: m.sub, showCancel: false })
}
function onReadAll() : void {
// 全部标记已读 → 所有条目角标一并消失
const arr : string[] = []
for (let gi = 0; gi < groups.value.length; gi++) {
const g = groups.value[gi]
for (let i = 0; i < g.items.length; i++) {
arr.push(keyOf(gi, i))
}
}
readKeys.value = arr
markMessagesRead()
uni.showToast({ title: '已全部标记为已读', icon: 'success' })
}
</script>
<style>
.ms-nav {
height: 96rpx;
display: flex;
flex-direction: row;
align-items: center;
padding-left: 32rpx;
padding-right: 32rpx;
background-color: #FFFFFF;
border-bottom: 2rpx solid #F3F4F6;
}
.ms-nav-t {
font-size: 33.6rpx;
font-weight: bold;
color: #1F2937;
}
.ms-nav-a {
margin-left: auto;
display: flex;
flex-direction: row;
align-items: center;
}
.ms-nav-a-t {
font-size: 29.2rpx;
color: #6B7280;
margin-left: 8rpx;
}
</style>
+88
View File
@@ -0,0 +1,88 @@
<template>
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="消息中心" :xloading="true" @right-click="onReadAll">
<template v-slot:right>
<x-icon name="check-double-line" color="#6B7280" font-size="17"></x-icon>
</template>
<view class="hr-body">
<!-- 消息分类筛选(组件库 xRadioButton 分段切换) -->
<view style="margin-bottom:8rpx">
<x-radio-button v-model="chipId" :list="chipTabs" height="64rpx" space="4rpx" round="22rpx"
text-style="font-weight:bold;" bg-color="#E5E7EB" active-color="#2563EB"
active-font-color="#FFFFFF" font-color="#6B7280" font-size="22rpx"></x-radio-button>
</view>
<view v-for="(g, gi) in groups" :key="gi">
<hr-sec :title="g.title" :more="g.count > 0 ? g.count.toString() + ' 条' : ''"></hr-sec>
<hr-card :variant="g.danger ? 'danger' : 'list'">
<hr-lrow v-for="(m, i) in g.items" :key="i" :icon="m.icon" :theme="m.theme" :title="m.title"
:sub="m.sub" :value="m.action" :value-theme="m.theme == 'red' ? 'red' : 'pri'"
:value-bold="true" :is-last="i == g.items.length - 1" @click="onMsgClick(m)"></hr-lrow>
</hr-card>
</view>
<view style="height:14px"></view>
</view>
</hr-page>
</template>
<script lang="uts" setup>
import { ref, computed } from 'vue'
import { rescuerMsgGroups } from '@/mock/index.uts'
import { HrMsg, HrMsgGroup } from '@/mock/types.uts'
import { navTo } from '@/utils/nav.uts'
import { markMessagesRead } from '@/utils/unread.uts'
import { RADIO_BUTTON } from '@/uni_modules/tmx-ui/interface.uts'
/**
* s27 消息中心 · 救护端
* P0 派单消息可直接「去接收」,跳转到事件中心查看进行中的派单。
* 查看后清未读 → 「我的」页消息中心入口角标消失。
*/
/**
* 消息分类筛选(组件库 xRadioButton 分段切换,替代原 hr-chip 横向滚动)
* ⚠ RADIO_BUTTON 的 id 是 string:统一用下标字符串,选中值即筛选序号。
*/
const chipTabs : RADIO_BUTTON[] = [
{ id: '0', title: '全部 5' } as RADIO_BUTTON,
{ id: '1', title: '派单通知 2' } as RADIO_BUTTON,
{ id: '2', title: '系统消息 3' } as RADIO_BUTTON
]
const chipId = ref('0')
/** 由字符串 id 换算分类序号,供 groups 筛选使用 */
const chip = computed(() : number => {
const i = parseInt(chipId.value)
return isNaN(i) ? 0 : i
})
onPageShow(() => {
markMessagesRead()
})
const groups = computed(() : HrMsgGroup[] => {
if (chip.value == 1) { return [rescuerMsgGroups[0]] }
if (chip.value == 2) { return [rescuerMsgGroups[1]] }
return rescuerMsgGroups
})
function onMsgClick(m : HrMsg) : void {
if (m.action == '去接收 ') {
navTo('/pages/event/index')
return
}
if (m.title == 'BLS 证书即将到期提醒') {
navTo('/pages/study/certificates')
return
}
if (m.title.indexOf('新课题') > -1) {
navTo('/pages/course/library')
return
}
uni.showModal({ title: m.title, content: m.sub, showCancel: false })
}
function onReadAll() : void {
uni.showToast({ title: '已全部标记为已读', icon: 'success' })
}
</script>