7 changed files with 1030 additions and 49 deletions
@ -0,0 +1,114 @@ |
|||
<template> |
|||
<div class="footer-bar"> |
|||
<div |
|||
v-for="item in tabs" |
|||
:key="item.key" |
|||
class="footer-item" |
|||
:class="{ 'footer-item--active': item.key === active }" |
|||
@click="onTabClick(item)" |
|||
> |
|||
<div v-if="item.key === 'message'" class="footer-icon-wrap"> |
|||
<svg viewBox="0 0 24 24" fill="currentColor" v-html="item.path"></svg> |
|||
<span v-if="messageUnread" class="footer-badge">{{ messageUnread }}</span> |
|||
</div> |
|||
<svg v-else viewBox="0 0 24 24" fill="currentColor" v-html="item.path"></svg> |
|||
<span>{{ item.label }}</span> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup name="FooterBar"> |
|||
import { useRouter } from 'vue-router'; |
|||
import { messageUnread } from '@/store/messageState'; |
|||
|
|||
const props = defineProps({ |
|||
active: { |
|||
type: String, |
|||
default: '', |
|||
}, |
|||
}); |
|||
|
|||
const router = useRouter(); |
|||
|
|||
const tabs = [ |
|||
{ key: 'home', label: '首页', route: '/enforcement/enforcementHome', |
|||
path: '<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>' }, |
|||
{ key: 'query', label: '查询', route: '/enforcement/query', |
|||
path: '<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>' }, |
|||
{ key: 'message', label: '消息', route: '/enforcement/message', |
|||
path: '<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/>' }, |
|||
{ key: 'my', label: '我的', route: '/enforcement/my', |
|||
path: '<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>' }, |
|||
]; |
|||
|
|||
const onTabClick = (item) => { |
|||
if (item.key === props.active) return; |
|||
if (router.currentRoute.value.path !== item.route) { |
|||
router.replace(item.route); |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.footer-bar { |
|||
flex-shrink: 0; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-around; |
|||
background: #fff; |
|||
padding: 8px 0; |
|||
padding-bottom: calc(8px + env(safe-area-inset-bottom)); |
|||
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.06); |
|||
} |
|||
|
|||
.footer-item { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
gap: 3px; |
|||
color: #999; |
|||
font-size: 11px; |
|||
cursor: pointer; |
|||
user-select: none; |
|||
|
|||
svg { |
|||
width: 24px; |
|||
height: 24px; |
|||
} |
|||
|
|||
&--active { |
|||
color: #2b7de9; |
|||
} |
|||
|
|||
&:active { |
|||
opacity: 0.7; |
|||
} |
|||
} |
|||
|
|||
.footer-icon-wrap { |
|||
position: relative; |
|||
width: 24px; |
|||
height: 24px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.footer-badge { |
|||
position: absolute; |
|||
top: -4px; |
|||
right: -10px; |
|||
min-width: 16px; |
|||
height: 16px; |
|||
line-height: 16px; |
|||
padding: 0 4px; |
|||
background: #f5222d; |
|||
color: #fff; |
|||
font-size: 10px; |
|||
text-align: center; |
|||
border-radius: 8px; |
|||
border: 2px solid #fff; |
|||
box-sizing: content-box; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,22 @@ |
|||
import { ref, computed } from 'vue'; |
|||
|
|||
/** |
|||
* 消息未读数(跨 tabbar 页面共享) |
|||
* - 模块级单例,四个 tabbar 页面(首页/查询/消息/我的)共享同一份 |
|||
* - 消息中心页面负责写入;其他页面仅读取用于 footer-bar 角标展示 |
|||
*/ |
|||
export const messageUnread = ref(0); |
|||
|
|||
/** 格式化为角标展示值(0 显示空) */ |
|||
export const messageUnreadBadge = computed(() => (messageUnread.value > 0 ? messageUnread.value : '')); |
|||
|
|||
/** 直接设置未读总数 */ |
|||
export const setMessageUnread = (count) => { |
|||
messageUnread.value = Number(count) || 0; |
|||
}; |
|||
|
|||
// 占位 default 导出:src/store 目录会被 globalStore.js 通过 import.meta.glob
|
|||
// 以 { import: 'default' } 的方式扫描,缺少 default 会触发
|
|||
// "does not provide an export named 'default'" 错误。
|
|||
// 此处导出一个不带 $id 的对象,globalStore 会自动跳过注册,不影响 Pinia。
|
|||
export default {}; |
|||
@ -0,0 +1,474 @@ |
|||
<template> |
|||
<div class="message-center app_content"> |
|||
<!-- 顶部蓝色背景 --> |
|||
<div class="header-bg"> |
|||
<!-- 城市天际线装饰 --> |
|||
<div class="cityscape"></div> |
|||
|
|||
<!-- 标题区 --> |
|||
<div class="title-area"> |
|||
<div class="bell-icon"> |
|||
<svg viewBox="0 0 24 24" fill="currentColor"> |
|||
<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/> |
|||
</svg> |
|||
</div> |
|||
<div class="title-group"> |
|||
<div class="title-cn">消息中心</div> |
|||
<div class="title-en">Message Center</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Tab 切换 + 列表 --> |
|||
<div class="content-card"> |
|||
<van-tabs |
|||
v-model:active="activeTab" |
|||
title-active-color="#1a1a2e" |
|||
title-inactive-color="#999" |
|||
color="#2b7de9" |
|||
line-width="40px" |
|||
line-height="3px" |
|||
:border="false" |
|||
> |
|||
<van-tab title="消息" name="message"> |
|||
<div class="list-wrap"> |
|||
<div |
|||
v-for="item in messageList" |
|||
:key="item.id" |
|||
class="msg-item" |
|||
@click="onItemClick(item)" |
|||
> |
|||
<van-badge |
|||
:content="item.unread" |
|||
:show-zero="false" |
|||
:max="99" |
|||
> |
|||
<div class="msg-icon" :class="item.iconClass"> |
|||
<component :is="item.iconComp"/> |
|||
</div> |
|||
</van-badge> |
|||
<div class="msg-info"> |
|||
<div class="msg-row"> |
|||
<div class="msg-title">{{ item.title }}</div> |
|||
<div class="msg-time">{{ item.time }}</div> |
|||
</div> |
|||
<div class="msg-desc">{{ item.desc }}</div> |
|||
</div> |
|||
</div> |
|||
<div v-if="!messageList.length" class="empty-tip">暂无消息</div> |
|||
</div> |
|||
</van-tab> |
|||
<van-tab title="待办" name="todo"> |
|||
<div class="list-wrap"> |
|||
<div |
|||
v-for="item in todoList" |
|||
:key="item.id" |
|||
class="msg-item" |
|||
@click="onItemClick(item)" |
|||
> |
|||
<van-badge |
|||
:content="item.unread" |
|||
:show-zero="false" |
|||
:max="99" |
|||
> |
|||
<div class="msg-icon" :class="item.iconClass"> |
|||
<component :is="item.iconComp"/> |
|||
</div> |
|||
</van-badge> |
|||
<div class="msg-info"> |
|||
<div class="msg-row"> |
|||
<div class="msg-title">{{ item.title }}</div> |
|||
<div class="msg-time">{{ item.time }}</div> |
|||
</div> |
|||
<div class="msg-desc">{{ item.desc }}</div> |
|||
</div> |
|||
</div> |
|||
<div v-if="!todoList.length" class="empty-tip">暂无待办</div> |
|||
</div> |
|||
</van-tab> |
|||
</van-tabs> |
|||
</div> |
|||
|
|||
<!-- 底部导航栏 --> |
|||
<FooterBar active="message" /> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup name="messageCenter"> |
|||
import { h, watch } from 'vue'; |
|||
import { useRouter } from 'vue-router'; |
|||
import { setMessageUnread } from '@/store/messageState'; |
|||
|
|||
const router = useRouter(); |
|||
|
|||
// ========== Tab 状态 ========== |
|||
const activeTab = ref('todo'); // 默认显示待办(与设计图一致) |
|||
|
|||
// ========== 图标组件(SVG) ========== |
|||
const IconNotice = () => h('svg', { viewBox: '0 0 24 24', fill: 'currentColor' }, [ |
|||
h('path', { d: 'M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM7 9h10v2H7V9zm6 5H7v-2h6v2zm4-6H7V6h10v2z' }), |
|||
]); |
|||
|
|||
const IconTask = () => h('svg', { viewBox: '0 0 24 24', fill: 'currentColor' }, [ |
|||
h('path', { d: 'M19 3h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm2 15l-4-4 1.41-1.41L14 15.17l4.59-4.59L20 12l-6 6z' }), |
|||
]); |
|||
|
|||
const IconAppointment = () => h('svg', { viewBox: '0 0 24 24', fill: 'currentColor' }, [ |
|||
h('path', { d: 'M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z' }), |
|||
]); |
|||
|
|||
// ========== 列表数据 ========== |
|||
const messageList = ref([]); |
|||
|
|||
const todoList = ref([ |
|||
{ |
|||
id: 1, |
|||
title: '执法预告', |
|||
desc: '最新一条预告信息标题', |
|||
time: '2026-04-15 10:07', |
|||
unread: 3, |
|||
iconComp: IconNotice, |
|||
iconClass: 'icon-blue', |
|||
type: 'notice', |
|||
}, |
|||
{ |
|||
id: 2, |
|||
title: '执法任务', |
|||
desc: '最新一条任务信息标题', |
|||
time: '2026-04-14 10:00', |
|||
unread: 2, |
|||
iconComp: IconTask, |
|||
iconClass: 'icon-cyan', |
|||
type: 'task', |
|||
}, |
|||
{ |
|||
id: 3, |
|||
title: '执法预约', |
|||
desc: '最新一条预约信息标题', |
|||
time: '', |
|||
unread: 0, |
|||
iconComp: IconAppointment, |
|||
iconClass: 'icon-green', |
|||
type: 'appointment', |
|||
}, |
|||
]); |
|||
|
|||
// 未读总数(tabbar 角标) |
|||
const totalUnread = computed(() => { |
|||
const total = [...messageList.value, ...todoList.value] |
|||
.reduce((sum, it) => sum + (Number(it.unread) || 0), 0); |
|||
return total > 0 ? total : ''; |
|||
}); |
|||
|
|||
// 同步到全局共享 state,便于其他 tabbar 页面读取 |
|||
watch( |
|||
[messageList, todoList], |
|||
() => { |
|||
const total = [...messageList.value, ...todoList.value] |
|||
.reduce((sum, it) => sum + (Number(it.unread) || 0), 0); |
|||
setMessageUnread(total); |
|||
}, |
|||
{ immediate: true, deep: true } |
|||
); |
|||
|
|||
// ========== 事件 ========== |
|||
const onBack = () => { |
|||
router.back(); |
|||
}; |
|||
|
|||
const onItemClick = (item) => { |
|||
// TODO: 后续对接业务详情路由 |
|||
// eslint-disable-next-line no-console |
|||
console.log('点击消息项:', item); |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.message-center { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
background: #f0f6ff; |
|||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif; |
|||
overflow: hidden; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
/* ===== 顶部蓝色背景 ===== */ |
|||
.header-bg { |
|||
position: relative; |
|||
background: linear-gradient(180deg, #2b7de9 0%, #5ba3f5 60%, #a0cdf8 100%); |
|||
padding: 0 0 24px; |
|||
overflow: hidden; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.nav-bar { |
|||
height: 44px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
padding: 0 12px; |
|||
position: relative; |
|||
z-index: 2; |
|||
|
|||
.nav-back { |
|||
width: 32px; |
|||
height: 32px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
color: #fff; |
|||
|
|||
svg { |
|||
width: 24px; |
|||
height: 24px; |
|||
} |
|||
} |
|||
|
|||
.nav-title { |
|||
flex: 1; |
|||
text-align: center; |
|||
color: #fff; |
|||
font-size: 17px; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.nav-right { |
|||
width: 32px; |
|||
} |
|||
} |
|||
|
|||
.cityscape { |
|||
position: absolute; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
height: 100px; |
|||
opacity: 0.18; |
|||
pointer-events: none; |
|||
|
|||
&::before { |
|||
content: ''; |
|||
position: absolute; |
|||
bottom: 0; |
|||
left: 10%; |
|||
width: 8%; |
|||
height: 50px; |
|||
background: rgba(255, 255, 255, 0.5); |
|||
border-radius: 2px 2px 0 0; |
|||
box-shadow: |
|||
20px 15px 0 rgba(255, 255, 255, 0.4), |
|||
45px 30px 0 rgba(255, 255, 255, 0.35); |
|||
} |
|||
|
|||
&::after { |
|||
content: ''; |
|||
position: absolute; |
|||
bottom: 0; |
|||
right: 15%; |
|||
width: 6%; |
|||
height: 80px; |
|||
background: rgba(255, 255, 255, 0.4); |
|||
border-radius: 2px 2px 0 0; |
|||
box-shadow: |
|||
-30px 20px 0 rgba(255, 255, 255, 0.3), |
|||
-55px 35px 0 rgba(255, 255, 255, 0.25); |
|||
} |
|||
} |
|||
|
|||
.title-area { |
|||
position: relative; |
|||
z-index: 1; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 20px 20px 10px; |
|||
gap: 14px; |
|||
|
|||
.bell-icon { |
|||
width: 64px; |
|||
height: 64px; |
|||
background: rgba(255, 255, 255, 0.2); |
|||
border-radius: 50%; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
svg { |
|||
width: 38px; |
|||
height: 38px; |
|||
color: #fff; |
|||
} |
|||
} |
|||
|
|||
.title-group { |
|||
.title-cn { |
|||
font-size: 28px; |
|||
font-weight: 700; |
|||
color: #fff; |
|||
letter-spacing: 4px; |
|||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.title-en { |
|||
margin-top: 4px; |
|||
font-size: 16px; |
|||
color: rgba(255, 255, 255, 0.75); |
|||
letter-spacing: 2px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* ===== 内容卡片 ===== */ |
|||
.content-card { |
|||
flex: 1; |
|||
min-height: 0; |
|||
margin: -18px 0 0; |
|||
background: #fff; |
|||
border-radius: 18px 18px 0 0; |
|||
overflow: hidden; |
|||
display: flex; |
|||
flex-direction: column; |
|||
position: relative; |
|||
|
|||
:deep(.van-tabs) { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
} |
|||
|
|||
:deep(.van-tabs__wrap) { |
|||
height: 52px; |
|||
border-bottom: 1px solid #f0f2f5; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
:deep(.van-tab) { |
|||
font-size: 16px; |
|||
} |
|||
|
|||
:deep(.van-tab--active) { |
|||
font-weight: 700; |
|||
} |
|||
|
|||
:deep(.van-tabs__content), |
|||
:deep(.van-tabs__track), |
|||
:deep(.van-tab__panel-wrapper), |
|||
:deep(.van-tab__pane) { |
|||
flex: 1; |
|||
min-height: 0; |
|||
height: 100%; |
|||
} |
|||
|
|||
:deep(.van-tabs__content) { |
|||
overflow: hidden; |
|||
} |
|||
} |
|||
|
|||
.list-wrap { |
|||
padding: 12px 14px 16px; |
|||
height: 100%; |
|||
box-sizing: border-box; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
/* ===== 消息条目 ===== */ |
|||
.msg-item { |
|||
display: flex; |
|||
align-items: center; |
|||
background: #fff; |
|||
padding: 14px 14px; |
|||
border-radius: 12px; |
|||
margin-bottom: 12px; |
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); |
|||
transition: background 0.2s; |
|||
|
|||
&:active { |
|||
background: #f5f7fa; |
|||
} |
|||
} |
|||
|
|||
.msg-icon { |
|||
width: 48px; |
|||
height: 48px; |
|||
border-radius: 12px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
flex-shrink: 0; |
|||
|
|||
svg { |
|||
width: 26px; |
|||
height: 26px; |
|||
color: #fff; |
|||
} |
|||
|
|||
&.icon-blue { |
|||
background: linear-gradient(135deg, #60a5fa, #3b82f6); |
|||
box-shadow: 0 4px 10px rgba(59, 130, 246, 0.25); |
|||
} |
|||
|
|||
&.icon-cyan { |
|||
background: linear-gradient(135deg, #22d3ee, #06b6d4); |
|||
box-shadow: 0 4px 10px rgba(6, 182, 212, 0.25); |
|||
} |
|||
|
|||
&.icon-green { |
|||
background: linear-gradient(135deg, #2dd4bf, #14b8a6); |
|||
box-shadow: 0 4px 10px rgba(20, 184, 166, 0.25); |
|||
} |
|||
} |
|||
|
|||
.msg-info { |
|||
flex: 1; |
|||
margin-left: 14px; |
|||
min-width: 0; |
|||
} |
|||
|
|||
.msg-row { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
gap: 8px; |
|||
|
|||
.msg-title { |
|||
font-size: 16px; |
|||
font-weight: 600; |
|||
color: #1a1a2e; |
|||
} |
|||
|
|||
.msg-time { |
|||
font-size: 12px; |
|||
color: #9ca3af; |
|||
flex-shrink: 0; |
|||
} |
|||
} |
|||
|
|||
.msg-desc { |
|||
margin-top: 6px; |
|||
font-size: 13px; |
|||
color: #9ca3af; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.empty-tip { |
|||
text-align: center; |
|||
color: #bbb; |
|||
font-size: 14px; |
|||
padding: 60px 0; |
|||
} |
|||
|
|||
/* 调整 Vant badge 样式 */ |
|||
:deep(.van-badge) { |
|||
font-size: 11px; |
|||
min-width: 18px; |
|||
height: 18px; |
|||
line-height: 18px; |
|||
padding: 0 4px; |
|||
border: 2px solid #fff; |
|||
background: #f5222d; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,348 @@ |
|||
<template> |
|||
<div class="my-page app_content"> |
|||
<!-- 顶部蓝色背景(含城市天际线 + 用户信息) --> |
|||
<div class="header-bg"> |
|||
<div class="cityscape"></div> |
|||
|
|||
<div class="user-wrap"> |
|||
<div class="avatar"> |
|||
<img |
|||
:src="userInfo.avatar" |
|||
alt="avatar" |
|||
@error="onAvatarError" |
|||
/> |
|||
</div> |
|||
<div class="user-name">{{ userInfo.name }}</div> |
|||
<div class="user-phone">电话:{{ userInfo.phone }}</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 菜单列表 --> |
|||
<div class="menu-wrap"> |
|||
<div class="menu-card"> |
|||
<div |
|||
v-for="(item, idx) in menuList" |
|||
:key="item.key" |
|||
class="menu-item" |
|||
:class="{ 'menu-item--last': idx === menuList.length - 1 }" |
|||
@click="onMenuClick(item)" |
|||
> |
|||
<div class="menu-icon" :class="item.iconClass"> |
|||
<component :is="item.iconComp"/> |
|||
</div> |
|||
<div class="menu-label">{{ item.label }}</div> |
|||
<div class="menu-arrow"> |
|||
<svg viewBox="0 0 24 24" fill="currentColor"> |
|||
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6z"/> |
|||
</svg> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 设置单独一组卡片 --> |
|||
<div class="menu-card menu-card--split"> |
|||
<div class="menu-item menu-item--last" @click="onMenuClick(settingItem)"> |
|||
<div class="menu-icon" :class="settingItem.iconClass"> |
|||
<component :is="settingItem.iconComp"/> |
|||
</div> |
|||
<div class="menu-label">{{ settingItem.label }}</div> |
|||
<div class="menu-arrow"> |
|||
<svg viewBox="0 0 24 24" fill="currentColor"> |
|||
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6z"/> |
|||
</svg> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 底部导航栏 --> |
|||
<FooterBar active="my" /> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup name="enforcementMy"> |
|||
import { h } from 'vue'; |
|||
import { useRouter } from 'vue-router'; |
|||
import { showToast } from 'vant'; |
|||
|
|||
const router = useRouter(); |
|||
|
|||
// ========== 用户信息 ========== |
|||
const defaultAvatar = 'https://img.yzcdn.cn/vant/cat.jpeg'; |
|||
const userInfo = ref({ |
|||
name: '司小华', |
|||
phone: '13800138000', |
|||
avatar: defaultAvatar, |
|||
}); |
|||
|
|||
const onAvatarError = (e) => { |
|||
e.target.src = defaultAvatar; |
|||
}; |
|||
|
|||
// ========== 菜单图标(SVG) ========== |
|||
const IconNotice = () => h('svg', { viewBox: '0 0 24 24', fill: 'currentColor' }, [ |
|||
h('path', { d: 'M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM7 9h10v2H7V9zm6 5H7v-2h6v2zm4-6H7V6h10v2z' }), |
|||
]); |
|||
|
|||
const IconTask = () => h('svg', { viewBox: '0 0 24 24', fill: 'currentColor' }, [ |
|||
h('path', { d: 'M19 3h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm2 15l-4-4 1.41-1.41L14 15.17l4.59-4.59L20 12l-6 6z' }), |
|||
]); |
|||
|
|||
const IconAppointment = () => h('svg', { viewBox: '0 0 24 24', fill: 'currentColor' }, [ |
|||
h('path', { d: 'M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z' }), |
|||
]); |
|||
|
|||
const IconPlan = () => h('svg', { viewBox: '0 0 24 24', fill: 'currentColor' }, [ |
|||
h('path', { d: 'M3.5 18.49l6-6.01 4 4L22 6.92l-1.41-1.41-7.09 7.97-4-4L2 16.99z' }), |
|||
]); |
|||
|
|||
const IconSetting = () => h('svg', { viewBox: '0 0 24 24', fill: 'currentColor' }, [ |
|||
h('path', { d: 'M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 00.12-.61l-1.92-3.32a.488.488 0 00-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 00-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 00-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z' }), |
|||
]); |
|||
|
|||
// ========== 菜单配置 ========== |
|||
const menuList = ref([ |
|||
{ key: 'notice', label: '预告', iconComp: IconNotice, iconClass: 'icon-blue' }, |
|||
{ key: 'task', label: '任务', iconComp: IconTask, iconClass: 'icon-cyan' }, |
|||
{ key: 'appointment', label: '预约', iconComp: IconAppointment, iconClass: 'icon-teal' }, |
|||
{ key: 'plan', label: '计划', iconComp: IconPlan, iconClass: 'icon-green' }, |
|||
]); |
|||
|
|||
const settingItem = { |
|||
key: 'setting', |
|||
label: '设置', |
|||
iconComp: IconSetting, |
|||
iconClass: 'icon-purple', |
|||
}; |
|||
|
|||
// ========== 菜单点击 ========== |
|||
const onMenuClick = (item) => { |
|||
switch (item.key) { |
|||
case 'notice': |
|||
case 'task': |
|||
case 'appointment': |
|||
// 跳至消息中心待办 tab,并带类型过滤参数 |
|||
router.push({ |
|||
path: '/enforcement/message', |
|||
query: { tab: 'todo', type: item.key }, |
|||
}); |
|||
break; |
|||
case 'plan': |
|||
// TODO: 对接执法计划列表页 |
|||
showToast('执法计划功能开发中'); |
|||
break; |
|||
case 'setting': |
|||
// TODO: 对接设置页 |
|||
showToast('设置功能开发中'); |
|||
break; |
|||
default: |
|||
showToast('功能开发中'); |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.my-page { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
background: #f0f6ff; |
|||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
/* ===== 顶部蓝色背景 ===== */ |
|||
.header-bg { |
|||
position: relative; |
|||
background: linear-gradient(180deg, #2b7de9 0%, #5ba3f5 60%, #a0cdf8 100%); |
|||
padding: 24px 20px 70px; |
|||
overflow: hidden; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.cityscape { |
|||
position: absolute; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
height: 110px; |
|||
opacity: 0.2; |
|||
pointer-events: none; |
|||
|
|||
&::before { |
|||
content: ''; |
|||
position: absolute; |
|||
bottom: 0; |
|||
left: 6%; |
|||
width: 8%; |
|||
height: 60px; |
|||
background: rgba(255, 255, 255, 0.5); |
|||
border-radius: 2px 2px 0 0; |
|||
box-shadow: |
|||
22px 18px 0 rgba(255, 255, 255, 0.4), |
|||
48px 30px 0 rgba(255, 255, 255, 0.35), |
|||
75px 12px 0 rgba(255, 255, 255, 0.3); |
|||
} |
|||
|
|||
&::after { |
|||
content: ''; |
|||
position: absolute; |
|||
bottom: 0; |
|||
right: 10%; |
|||
width: 6%; |
|||
height: 80px; |
|||
background: rgba(255, 255, 255, 0.4); |
|||
border-radius: 2px 2px 0 0; |
|||
box-shadow: |
|||
-30px 20px 0 rgba(255, 255, 255, 0.35), |
|||
-55px 35px 0 rgba(255, 255, 255, 0.28), |
|||
-80px 18px 0 rgba(255, 255, 255, 0.22); |
|||
} |
|||
} |
|||
|
|||
.user-wrap { |
|||
position: relative; |
|||
z-index: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
gap: 10px; |
|||
|
|||
.avatar { |
|||
width: 84px; |
|||
height: 84px; |
|||
border-radius: 50%; |
|||
overflow: hidden; |
|||
border: 3px solid rgba(255, 255, 255, 0.9); |
|||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12); |
|||
background: #fff; |
|||
|
|||
img { |
|||
width: 100%; |
|||
height: 100%; |
|||
object-fit: cover; |
|||
} |
|||
} |
|||
|
|||
.user-name { |
|||
font-size: 22px; |
|||
font-weight: 700; |
|||
color: #fff; |
|||
letter-spacing: 2px; |
|||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); |
|||
} |
|||
|
|||
.user-phone { |
|||
font-size: 13px; |
|||
color: rgba(255, 255, 255, 0.9); |
|||
letter-spacing: 1px; |
|||
} |
|||
} |
|||
|
|||
/* ===== 菜单列表 ===== */ |
|||
.menu-wrap { |
|||
flex: 1; |
|||
overflow-y: auto; |
|||
padding: 0 16px 16px; |
|||
margin-top: -48px; |
|||
position: relative; |
|||
z-index: 2; |
|||
} |
|||
|
|||
.menu-card { |
|||
background: #fff; |
|||
border-radius: 14px; |
|||
overflow: hidden; |
|||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); |
|||
margin-bottom: 14px; |
|||
} |
|||
|
|||
.menu-card--split { |
|||
margin-top: 4px; |
|||
} |
|||
|
|||
.menu-item { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 14px 16px; |
|||
position: relative; |
|||
transition: background 0.2s; |
|||
|
|||
&::after { |
|||
content: ''; |
|||
position: absolute; |
|||
left: 60px; |
|||
right: 16px; |
|||
bottom: 0; |
|||
height: 1px; |
|||
background: #f0f2f5; |
|||
transform: scaleY(0.5); |
|||
} |
|||
|
|||
&--last::after { |
|||
display: none; |
|||
} |
|||
|
|||
&:active { |
|||
background: #f5f7fa; |
|||
} |
|||
} |
|||
|
|||
.menu-icon { |
|||
width: 34px; |
|||
height: 34px; |
|||
border-radius: 9px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
flex-shrink: 0; |
|||
margin-right: 14px; |
|||
|
|||
svg { |
|||
width: 20px; |
|||
height: 20px; |
|||
color: #fff; |
|||
} |
|||
|
|||
&.icon-blue { |
|||
background: linear-gradient(135deg, #60a5fa, #3b82f6); |
|||
} |
|||
|
|||
&.icon-cyan { |
|||
background: linear-gradient(135deg, #22d3ee, #06b6d4); |
|||
} |
|||
|
|||
&.icon-teal { |
|||
background: linear-gradient(135deg, #2dd4bf, #14b8a6); |
|||
} |
|||
|
|||
&.icon-green { |
|||
background: linear-gradient(135deg, #34d399, #10b981); |
|||
} |
|||
|
|||
&.icon-purple { |
|||
background: linear-gradient(135deg, #a78bfa, #8b5cf6); |
|||
} |
|||
} |
|||
|
|||
.menu-label { |
|||
flex: 1; |
|||
font-size: 15px; |
|||
color: #1a1a2e; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.menu-arrow { |
|||
width: 18px; |
|||
height: 18px; |
|||
color: #c0c4cc; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
svg { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,43 @@ |
|||
<template> |
|||
<div class="query-page app_content"> |
|||
<div class="page-header"> |
|||
<div class="page-title">查询</div> |
|||
</div> |
|||
<div class="page-body"> |
|||
<van-empty description="查询功能开发中" /> |
|||
</div> |
|||
|
|||
<!-- 底部导航栏 --> |
|||
<FooterBar active="query" /> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup name="enforcementQuery"> |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.query-page { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
background: #f0f6ff; |
|||
} |
|||
|
|||
.page-header { |
|||
height: 44px; |
|||
line-height: 44px; |
|||
text-align: center; |
|||
background: linear-gradient(180deg, #2b7de9 0%, #5ba3f5 100%); |
|||
color: #fff; |
|||
font-size: 17px; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.page-body { |
|||
flex: 1; |
|||
overflow-y: auto; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue