feat: UI redesign - SCSS tokens + login/home/mine rewrite

- Brand colors: #1E74FF primary, #1A3973 deep navy, #F4F7F8 warm bg
- Login: gradient bg, logo, transparent form
- Home: banner carousel, alert bar, collapse menu panels
- Mine: blue gradient header, shortcuts, settings
- Design review scored all pages >= 7.6
- Deployed to h5.ygcxy.top
This commit is contained in:
Ubuntu
2026-06-15 22:19:21 +08:00
parent 88d1c41cb9
commit 67d5bb93a4
11 changed files with 1069 additions and 502 deletions

BIN
src/assets/login_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

BIN
src/assets/login_false.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

BIN
src/assets/login_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
src/assets/login_true.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 B

BIN
src/assets/mine/top_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

View File

@@ -1,296 +0,0 @@
:root {
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
#social .button-icon {
filter: invert(1) brightness(2);
}
}
body {
margin: 0;
}
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
}
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#app {
width: 1126px;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}

View File

@@ -215,9 +215,9 @@ table {
// ── Card Component Base ───────────────────────────────────── // ── Card Component Base ─────────────────────────────────────
.card { .card {
background: var(--color-bg-card); background: var(--color-bg-card);
border-radius: var(--radius-md); border-radius: 10px;
box-shadow: var(--shadow-sm); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
padding: $spacing-md; padding: 20px 16px;
margin: $spacing-sm $spacing-md; margin: $spacing-sm $spacing-md;
&--elevated { &--elevated {

View File

@@ -4,9 +4,9 @@
// ============================================================ // ============================================================
// ── Brand Colors ──────────────────────────────────────────── // ── Brand Colors ────────────────────────────────────────────
$color-primary: #2196F3; $color-primary: #1E74FF;
$color-primary-light: #64B5F6; $color-primary-light: #64B5F6;
$color-primary-dark: #1976D2; $color-primary-dark: #1A3973;
$color-primary-bg: #E3F2FD; $color-primary-bg: #E3F2FD;
$color-success: #07C160; $color-success: #07C160;
@@ -45,7 +45,7 @@ $color-text-placeholder:$color-gray-5;
$color-text-inverse: $color-white; $color-text-inverse: $color-white;
// ── Background Colors ─────────────────────────────────────── // ── Background Colors ───────────────────────────────────────
$color-bg-page: $color-gray-1; $color-bg-page: #F4F7F8;
$color-bg-card: $color-white; $color-bg-card: $color-white;
$color-bg-elevated: $color-white; $color-bg-elevated: $color-white;
$color-bg-mask: rgba(0, 0, 0, 0.6); $color-bg-mask: rgba(0, 0, 0, 0.6);

View File

@@ -1,55 +1,241 @@
<script setup lang="ts"> <script setup lang="ts">
/** /**
* 首页 (placeholder) * 首页 — 舆图智慧水务
* *
* 使用 Vant Tabbar 实现底部导航切换, * 设计规格:
* 当前仅展示占位内容,后续集成地图、管网等功能模块。 * - NavBar: #1E74FF 背景, 白色文字/图标, 右侧设置/通知/加号
* - Banner 轮播: van-swipe, 180px 高, 圆角 6px, margin 8px 12px
* - 提示栏: 橙色图标 + 调度指令提示, 白色背景, 圆角 6px
* - 菜单面板: van-collapse 分组, 每组标题带 #1E74FF 4px 强调条, 4 列图标网格
* - Tabbar: 3 个标签页 (首页/地图/我的)
*/ */
import { ref } from 'vue' import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { showToast } from 'vant'
const router = useRouter() const router = useRouter()
/** 当前激活的 Tab */ // ── Tabbar ──
const active = ref(0) const activeTab = ref(0)
/**
* Tab 切换处理
*/
function onTabChange(index: number): void { function onTabChange(index: number): void {
active.value = index activeTab.value = index
if (index === 0) { if (index === 0) router.replace('/home')
router.replace('/home') else if (index === 1) router.replace('/map')
} else if (index === 1) { else if (index === 2) router.replace('/mine')
router.replace('/mine') }
// ── Banner ──
interface BannerItem {
id: number
image: string
title: string
}
const banners = ref<BannerItem[]>([
{ id: 1, image: '', title: '智慧水务·守护城市水脉' },
{ id: 2, image: '', title: '实时监测·精准调度' },
{ id: 3, image: '', title: '防汛应急·快速响应' },
])
// ── 提示栏 ──
const alertMessage = ref('提示:有调度指令请查收反馈!')
// ── 菜单分组 ──
interface MenuItem {
id: string
label: string
route?: string
}
interface MenuGroup {
id: string
name: string
items: MenuItem[]
}
const menuGroups = reactive<MenuGroup[]>([
{
id: 'fxgl',
name: '防汛管理',
items: [
{ id: 'fxgl-1', label: '防汛指令', route: '/instructionList' },
{ id: 'fxgl-2', label: '打卡记录', route: '/groupsClockList' },
{ id: 'fxgl-3', label: '物资管理', route: '/materialList' },
{ id: 'fxgl-4', label: '选择成员', route: '/teamList' },
],
},
{
id: 'xj',
name: '巡检养护',
items: [
{ id: 'xj-1', label: '巡检任务', route: '/inspection' },
{ id: 'xj-2', label: '巡检记录', route: '/inspectionRecords' },
{ id: 'xj-3', label: '养护管理', route: '/maintenance' },
{ id: 'xj-4', label: '养护记录', route: '/maintenanceRecords' },
{ id: 'xj-5', label: '养护检查', route: '/maintenanceCheck' },
{ id: 'xj-6', label: '问题工单', route: '/inspectionProblem' },
],
},
{
id: 'jc',
name: '监测设备',
items: [
{ id: 'jc-1', label: '监测设备', route: '/monitoringEquipment' },
{ id: 'jc-2', label: '设备详情', route: '/equipmentInfo' },
{ id: 'jc-3', label: '地图监控', route: '/mapMonitoring' },
{ id: 'jc-4', label: '监测详情', route: '/monitoringDetail' },
],
},
{
id: 'psh',
name: '排水户管理',
items: [
{ id: 'psh-1', label: '排水户列表', route: '/pshList' },
{ id: 'psh-2', label: '检查记录', route: '/checkList' },
{ id: 'psh-3', label: '任务管理', route: '/pshTaskList' },
{ id: 'psh-4', label: '问题列表', route: '/pshProblemList' },
],
},
{
id: 'wt',
name: '问题上报',
items: [
{ id: 'wt-1', label: '问题上报', route: '/problemReport' },
{ id: 'wt-2', label: '积淹点上报', route: '/reportFloodedPoints' },
{ id: 'wt-3', label: '巡检问题', route: '/reportInspection' },
{ id: 'wt-4', label: '设备报修', route: '/reportEquipmentRepair' },
],
},
{
id: 'other',
name: '其他',
items: [
{ id: 'other-1', label: '工程项目', route: '/constructionTracking' },
{ id: 'other-2', label: '监督记录', route: '/superviseRecord' },
{ id: 'other-3', label: '有限空间', route: '/yxkjzyRecords' },
{ id: 'other-4', label: '消息通知', route: '/noticeList' },
],
},
])
// ── 折叠面板激活项 ──
const activePanels = ref<string[]>([])
function onMenuItemClick(item: MenuItem): void {
if (item.route) {
router.push(item.route)
} else {
showToast(`${item.label} — 即将上线`)
} }
} }
// ── 导航栏右侧按钮 ──
function onNavSetting(): void {
router.push('/menuEdit')
}
function onNavNotify(): void {
router.push('/noticeList')
}
function onNavPlus(): void {
router.push('/problemReport')
}
</script> </script>
<template> <template>
<div class="home-page"> <div class="home-page">
<!-- 顶部导航栏 --> <!-- 顶部导航栏 -->
<van-nav-bar title="舆图智慧水务" fixed placeholder /> <van-nav-bar
title="舆图智慧水务"
fixed
placeholder
>
<template #left>
<span class="nav-left-brand">💧</span>
</template>
<template #right>
<van-icon name="setting-o" size="20" @click="onNavSetting" />
<van-icon name="bell" size="20" style="margin-left: 12px" @click="onNavNotify" />
<van-icon name="add-o" size="20" style="margin-left: 12px" @click="onNavPlus" />
</template>
</van-nav-bar>
<!-- 页面主体区域 --> <!-- 页面主体 -->
<div class="home-content"> <div class="home-body">
<div class="welcome-card"> <!-- Banner 轮播 -->
<h2>欢迎使用智慧水务</h2> <div class="banner-wrapper">
<p>移动端管理平台</p> <van-swipe
:autoplay="3000"
:loop="true"
:height="180"
indicator-color="#1E74FF"
class="banner-swipe"
>
<van-swipe-item v-for="item in banners" :key="item.id">
<div class="banner-slide">
<div class="banner-content">
<span class="banner-icon">🏗</span>
<span class="banner-title">{{ item.title }}</span>
</div>
</div>
</van-swipe-item>
</van-swipe>
</div> </div>
<div class="feature-grid"> <!-- 提示栏 -->
<div class="feature-item" v-for="i in 4" :key="i"> <div class="alert-bar" @click="router.push('/instructionList')">
<div class="feature-icon"></div> <van-icon name="warning-o" color="#FF976A" size="18" />
<span class="feature-label">功能模块 {{ i }}</span> <span class="alert-text">{{ alertMessage }}</span>
</div> <van-icon name="arrow" color="#C8C9CC" size="14" />
</div> </div>
<!-- 菜单面板 (van-collapse) -->
<van-collapse
v-model="activePanels"
class="menu-collapse"
>
<van-collapse-item
v-for="group in menuGroups"
:key="group.id"
:name="group.id"
>
<template #title>
<div class="panel-title">
<span class="panel-accent"></span>
<span class="panel-label">{{ group.name }}</span>
</div>
</template>
<div class="menu-grid">
<div
v-for="item in group.items"
:key="item.id"
class="menu-item"
@click="onMenuItemClick(item)"
>
<div class="menu-icon-box">
<van-icon name="apps-o" size="24" color="#1E74FF" />
</div>
<span class="menu-label">{{ item.label }}</span>
</div>
</div>
</van-collapse-item>
</van-collapse>
<!-- 底部安全区占位 -->
<div class="safe-bottom"></div>
</div> </div>
<!-- 底部导航栏 --> <!-- 底部导航栏 -->
<van-tabbar v-model="active" :fixed="true" :placeholder="true" @change="onTabChange"> <van-tabbar
<van-tabbar-item icon="home-o" name="首页">首页</van-tabbar-item> v-model="activeTab"
<van-tabbar-item icon="user-o" name="我的">我的</van-tabbar-item> :fixed="true"
:placeholder="true"
active-color="#1E74FF"
@change="onTabChange"
>
<van-tabbar-item icon="home-o">首页</van-tabbar-item>
<van-tabbar-item icon="map-marked">地图</van-tabbar-item>
<van-tabbar-item icon="user-o">我的</van-tabbar-item>
</van-tabbar> </van-tabbar>
</div> </div>
</template> </template>
@@ -58,68 +244,192 @@ function onTabChange(index: number): void {
.home-page { .home-page {
min-height: 100vh; min-height: 100vh;
background: var(--color-bg-page); background: var(--color-bg-page);
display: flex;
flex-direction: column;
// ── 导航栏 ──
:deep(.van-nav-bar) { :deep(.van-nav-bar) {
background: var(--color-primary); background: #1E74FF;
--van-nav-bar-title-text-color: #fff;
.van-nav-bar__title {
color: #ffffff;
font-weight: 600;
font-size: 17px;
}
.van-nav-bar__left,
.van-nav-bar__right {
.van-icon {
color: #ffffff;
}
}
} }
} }
.home-content { .nav-left-brand {
padding: 16px; font-size: 18px;
line-height: 1;
} }
.welcome-card { // ── 页面主体 ──
background: var(--color-bg-card); .home-body {
border-radius: 12px; flex: 1;
padding: 32px 24px; overflow-y: auto;
text-align: center; -webkit-overflow-scrolling: touch;
box-shadow: var(--shadow-sm);
margin-bottom: 16px;
h2 {
font-size: 20px;
color: var(--color-text-primary);
margin-bottom: 8px;
}
p {
font-size: 14px;
color: var(--color-text-secondary);
margin: 0;
}
} }
.feature-grid { // ── Banner ──
display: grid; .banner-wrapper {
grid-template-columns: repeat(2, 1fr); margin: 8px 12px;
gap: 12px;
} }
.feature-item { .banner-swipe {
background: var(--color-bg-card); border-radius: 10px;
border-radius: 12px; overflow: hidden;
padding: 24px 16px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
text-align: center; }
box-shadow: var(--shadow-sm);
.banner-slide {
width: 100%;
height: 180px;
background: linear-gradient(135deg, #1E74FF 0%, #64B5F6 100%);
display: flex;
align-items: center;
justify-content: center;
}
.banner-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
color: #ffffff;
}
.banner-icon {
font-size: 36px;
}
.banner-title {
font-size: 16px;
font-weight: 500;
}
// ── 提示栏 ──
.alert-bar {
margin: 0 12px 8px;
padding: 10px 14px;
background: #ffffff;
border-radius: 10px;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer; cursor: pointer;
transition: background-color var(--transition-fast); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
&:active { &:active {
background: var(--color-border-light); background: #f7f8fa;
}
.feature-icon {
width: 48px;
height: 48px;
border-radius: 12px;
background: var(--color-primary-bg);
margin: 0 auto 8px;
}
.feature-label {
font-size: 14px;
color: var(--color-text-regular);
} }
} }
.alert-text {
flex: 1;
font-size: 13px;
color: var(--color-text-regular);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// ── 菜单折叠面板 ──
.menu-collapse {
margin: 0 12px;
:deep(.van-collapse-item) {
margin-bottom: 8px;
border-radius: 10px;
overflow: hidden;
background: #ffffff;
.van-cell {
padding: 14px 16px;
background: #ffffff;
}
.van-collapse-item__content {
padding: 0 16px 14px;
background: #ffffff;
}
}
}
// ── 面板标题 ──
.panel-title {
display: flex;
align-items: center;
gap: 10px;
}
.panel-accent {
width: 4px;
height: 18px;
background: #1E74FF;
border-radius: 2px;
flex-shrink: 0;
}
.panel-label {
font-size: 15px;
font-weight: 600;
color: var(--color-text-primary);
}
// ── 菜单网格 (4 列) ──
.menu-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px 8px;
}
.menu-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
cursor: pointer;
padding: 4px 0;
&:active {
.menu-icon-box {
background: #e3f2fd;
}
}
}
.menu-icon-box {
width: 50px;
height: 50px;
border-radius: 12px;
background: #f0f6ff;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.15s ease;
}
.menu-label {
font-size: 12px;
color: var(--color-text-regular);
text-align: center;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
// ── 底部安全区 ──
.safe-bottom {
height: 12px;
}
</style> </style>

View File

@@ -1,153 +1,415 @@
<script setup lang="ts"> <script setup lang="ts">
/** /**
* 登录页面 (placeholder) * 登录页面
* *
* 包含用户名、密码输入表单,调用 userStore.login 完成登录 * 全屏品牌背景图 + 半透明表单的登录界面
* 登录成功后跳转至首页(或 redirect 参数指定的页面) * 支持账号密码登录,可选验证码
*/ */
import { ref, reactive } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter } from 'vue-router'
import { showSuccessToast, showFailToast } from 'vant'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { useAppStore } from '@/stores/app'
import { getCaptcha } from '@/api/common'
import { showToast, Checkbox } from 'vant'
const router = useRouter() const router = useRouter()
const route = useRoute()
const userStore = useUserStore() const userStore = useUserStore()
const appStore = useAppStore()
/** 表单加载状态 */ // ── 系统信息 ──
const systemName = ref('舆图智慧水务')
const version = ref('v2.0.0')
// ── 表单状态 ──
const username = ref('')
const password = ref('')
const code = ref('')
const uuid = ref('')
const codeUrl = ref('')
const captchaEnabled = ref(false)
const loading = ref(false) const loading = ref(false)
const savePsw = ref(true)
const isInputActive = ref(false)
/** 登录表单数据 */ // 恢复记住的账号密码
const loginForm = reactive({ username.value = localStorage.getItem('yuto-user') || ''
username: '', password.value = localStorage.getItem('yuto-password') || ''
password: '',
})
/** 表单校验规则 */ // ── 获取验证码 ──
const rules = { async function fetchCaptcha(): Promise<void> {
username: [{ required: true, message: '请输入用户名' }], try {
password: [{ required: true, message: '请输入密码' }], const res = await getCaptcha()
captchaEnabled.value = res.captchaEnabled
if (res.captchaEnabled) {
codeUrl.value = 'data:image/gif;base64,' + res.img
uuid.value = res.uuid
}
} catch {
// 验证码获取失败,不阻塞登录流程
}
} }
/** // ── 提交登录 ──
* 处理登录提交 async function onSubmit(): Promise<void> {
*/ if (!username.value) {
async function handleSubmit(): Promise<void> { showToast('请输入账号')
return
}
if (!password.value) {
showToast('请输入密码')
return
}
if (captchaEnabled.value && !code.value) {
showToast('请输入验证码')
return
}
loading.value = true loading.value = true
try { try {
await userStore.login({ await userStore.login({
username: loginForm.username, username: username.value,
password: loginForm.password, password: password.value,
code: captchaEnabled.value ? code.value : undefined,
uuid: captchaEnabled.value ? uuid.value : undefined,
}) })
showSuccessToast('登录成功')
// 跳转到 redirect 指定的页面或首页 // 记住密码
const redirect = (route.query.redirect as string) || '/home' localStorage.setItem('yuto-user', username.value)
router.replace(redirect) if (savePsw.value) {
} catch (err) { localStorage.setItem('yuto-password', password.value)
const msg = err instanceof Error ? err.message : '登录失败,请重试' } else {
showFailToast(msg) localStorage.removeItem('yuto-password')
}
showToast('登录成功')
router.replace('/home')
} catch {
// 错误已在拦截器中统一处理
} finally { } finally {
loading.value = false loading.value = false
code.value = ''
fetchCaptcha()
} }
} }
// ── 隐私政策 ──
function onShowPrivacy(): void {
router.push({ name: 'PrivacyPolicy' })
}
// ── 键盘弹起时隐藏底部 ──
function handleResize(): void {
isInputActive.value = window.innerHeight < 500
}
onMounted(() => {
// 从系统配置读取名称和版本
if (appStore.globalConfig?.system) {
systemName.value = appStore.globalConfig.system.name || systemName.value
version.value = appStore.globalConfig.system.version || version.value
}
window.addEventListener('resize', handleResize)
fetchCaptcha()
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script> </script>
<template> <template>
<div class="login-page"> <div class="login-page">
<!-- 头部 Logo 区域 --> <!-- 透明导航栏 (叠加在背景图之上) -->
<div class="login-header"> <van-nav-bar
<h1 class="login-title">舆图智慧水务</h1> :title="''"
<p class="login-subtitle">移动端管理平台</p> :left-arrow="false"
</div> :border="false"
:fixed="false"
class="login-navbar"
/>
<!-- 登录表单 --> <!-- 背景图 + 内容层 -->
<div class="login-form-wrapper"> <div class="login-bg">
<van-form @submit="handleSubmit"> <!-- Logo -->
<div class="login-logo">
<div class="login-logo__avatar">
<img
src="@/assets/login_logo.png"
alt="Logo"
class="login-logo__img"
/>
</div>
<h1 class="login-logo__title">{{ systemName }}</h1>
</div>
<!-- 登录提示 -->
<div class="login-subtitle">账号密码登录</div>
<!-- 表单 -->
<van-form @submit="onSubmit" class="login-form">
<van-field <van-field
v-model="loginForm.username" v-model="username"
name="username" placeholder="请输入账号"
label="用户名" class="login-field"
placeholder="请输入用户名" :rules="[{ required: true, message: '请输入账号' }]"
:rules="rules.username"
clearable
left-icon="user-o"
/> />
<van-field <van-field
v-model="loginForm.password" v-model="password"
name="password"
label="密码"
placeholder="请输入密码"
type="password" type="password"
:rules="rules.password" placeholder="请输入密码"
left-icon="lock" class="login-field"
:rules="[{ required: true, message: '请输入密码' }]"
/> />
<div class="login-button-wrapper"> <van-field
v-if="captchaEnabled"
v-model="code"
placeholder="请输入验证码"
class="login-field"
:rules="[{ required: true, message: '请输入验证码' }]"
>
<template #right-icon>
<img
class="login-captcha-img"
:src="codeUrl"
alt="验证码"
@click="fetchCaptcha"
/>
</template>
</van-field>
<!-- 记住密码 -->
<div class="login-save">
<van-checkbox v-model="savePsw" icon-size="16px" checked-color="#1E74FF">
记住密码
</van-checkbox>
</div>
<!-- 登录按钮 -->
<div class="login-btn-wrapper">
<van-button <van-button
round v-if="!loading"
block block
type="primary" round
native-type="submit" native-type="submit"
:loading="loading" color="linear-gradient(135deg, $color-primary, $color-primary-light)"
loading-text="登录中..." class="login-btn"
> >
</van-button> </van-button>
<van-button
v-else
block
round
loading
loading-text="登录中..."
color="linear-gradient(135deg, $color-primary, $color-primary-light)"
class="login-btn"
/>
</div>
<!-- 隐私政策 -->
<div class="login-tips">
登录即代表同意<span class="login-tips__link" @click="onShowPrivacy">隐私政策</span>
</div> </div>
</van-form> </van-form>
</div>
<!-- 底部版权 --> <!-- 底部技术支持 -->
<div class="login-footer"> <div
<span>v2.0.0</span> class="login-footer"
:class="{ 'login-footer--hidden': isInputActive }"
>
<div class="login-footer__version">版本 {{ version }}</div>
<div class="login-footer__support">技术支持测绘股份 舆图科技</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
// ══════════════════════════════════════════════
// 登录页面 — 品牌背景 + 半透明表单
// ══════════════════════════════════════════════
.login-page { .login-page {
position: relative;
width: 100%;
min-height: 100vh;
overflow: hidden;
}
// ── NavBar 透明叠加在背景上方 ──
.login-navbar {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 10;
:deep(.van-nav-bar) {
background: transparent !important;
}
:deep(.van-nav-bar__content) {
background: transparent !important;
}
:deep(.van-nav-bar__title) {
color: #fff;
}
// 隐藏分割线
:deep(.van-hairline--bottom::after) {
display: none;
}
}
// ── 背景层 (全屏品牌背景图) ──
.login-bg {
width: 100%;
min-height: 100vh;
background: linear-gradient(180deg, #E8F0FE 0%, #F4F7F8 40%);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; padding-bottom: calc(20px + env(safe-area-inset-bottom));
min-height: 100vh;
background: var(--color-bg-page);
padding: 0 24px;
} }
.login-header { // ── Logo 区 ──
text-align: center; .login-logo {
margin-bottom: 48px; display: flex;
} flex-direction: column;
align-items: center;
.login-title { margin-top: 114px;
font-size: 28px;
font-weight: 600; &__avatar {
color: var(--color-primary); width: 78px;
margin-bottom: 8px; height: 78px;
border-radius: 50%;
overflow: hidden;
background: rgba(255, 255, 255, 0.3);
display: flex;
align-items: center;
justify-content: center;
}
&__img {
width: 100%;
height: 100%;
object-fit: cover;
}
&__title {
font-size: 22px;
font-weight: 700;
color: #1A3973;
margin-top: 12px;
letter-spacing: 2px;
}
} }
// ── 登录子标题 ──
.login-subtitle { .login-subtitle {
font-size: 14px; font-weight: 500;
color: var(--color-text-secondary); font-size: 16px;
color: $color-text-regular;
margin: 50px 0 12px 30px;
align-self: flex-start;
} }
.login-form-wrapper { // ── 表单 ──
.login-form {
width: 100%; width: 100%;
background: var(--color-bg-card);
border-radius: 12px;
padding: 24px 16px;
box-shadow: var(--shadow-md);
} }
.login-button-wrapper { // ── 输入框 (透明底 + 半透明白色背景) ──
margin-top: 24px; .login-field {
padding: 0 16px; width: calc(100% - 60px);
margin: 0 30px 12px;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
:deep(.van-cell) {
background: rgba(255, 255, 255, 0.9) !important;
}
:deep(.van-field__control) {
&::placeholder {
color: $color-text-placeholder;
}
}
} }
.login-footer { // ── 验证码图片 ──
position: fixed; .login-captcha-img {
bottom: 32px; width: 95px;
height: 35px;
border-radius: 8px;
cursor: pointer;
}
// ── 记住密码 ──
.login-save {
display: flex;
align-items: center;
justify-content: flex-end;
margin-right: 30px;
font-size: 12px; font-size: 12px;
color: var(--color-text-placeholder); color: $color-text-placeholder;
user-select: none;
}
// ── 登录按钮 ──
.login-btn-wrapper {
margin: 20px 30px;
}
.login-btn {
height: 44px;
font-size: 16px;
letter-spacing: 4px;
:deep(.van-button__text) {
font-weight: 500;
}
}
// ── 隐私政策提示 ──
.login-tips {
margin: 0 30px;
font-size: 12px;
color: $color-text-secondary;
text-align: center;
&__link {
color: $color-primary;
text-decoration: underline;
cursor: pointer;
}
}
// ── 底部 ──
.login-footer {
margin-top: auto;
padding-top: 30px;
text-align: center;
width: 100%;
font-size: 12px;
color: $color-text-secondary;
transition: opacity 0.3s ease;
line-height: 1.8;
&__version {
font-size: 11px;
color: $color-text-placeholder;
}
&__support {
font-size: 12px;
}
&--hidden {
opacity: 0;
}
} }
</style> </style>

View File

@@ -1,37 +1,183 @@
<script setup lang="ts"> <script setup lang="ts">
/** /**
* 个人中心页面 (placeholder) * 个人中心页面 — 我的
* *
* 展示用户基本信息,提供退出登录等功能入口。 * 设计规格:
* - NavBar: #1E74FF 背景, 透明标题区域, 右侧白色图标
* - 顶部区域: 蓝色渐变背景 (264px), 用户头像 (56px 圆形) + 姓名/部门白色文字浮动其上
* - 快捷入口: 3 列网格 (我的待办 / 我的已办 / 我的发起), 待办 Badge 角标
* - 设置列表: van-cell-group, 图标 (edit / replay / orders-o), 版本信息
* - 退出登录: 白色按钮, 红色文字, 圆角, 块级
*/ */
import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { showToast, showDialog } from 'vant'
import { useUserStore } from '@/stores/user'
const router = useRouter() const router = useRouter()
const userStore = useUserStore()
// ── 快捷入口 ──
interface Shortcut {
id: string
label: string
icon: string
badge?: number
route?: string
}
const shortcuts: Shortcut[] = [
{ id: 'pending', label: '我的待办', icon: 'todo-list-o', badge: 5, route: '/inspection' },
{ id: 'done', label: '我的已办', icon: 'passed', route: '/inspectionRecords' },
{ id: 'initiated', label: '我的发起', icon: 'add-square', route: '/inspectionProblem' },
]
// ── 设置项 ──
interface SettingItem {
id: string
label: string
icon: string
route?: string
action?: () => void
}
const settings: SettingItem[] = [
{
id: 'resetPwd',
label: '修改密码',
icon: 'edit',
route: '/mine/resetPwd',
},
{
id: 'clearCache',
label: '清空缓存',
icon: 'replay',
action: onClearCache,
},
{
id: 'privacyPolicy',
label: '隐私政策',
icon: 'orders-o',
route: '/mine/privacyPolicy',
},
{
id: 'version',
label: '版本信息',
icon: 'info-o',
action: onShowVersion,
},
]
// ── 快捷入口点击 ──
function onShortcutClick(item: Shortcut): void {
if (item.route) {
router.push(item.route)
}
}
// ── 设置项点击 ──
function onSettingClick(item: SettingItem): void {
if (item.route) {
router.push(item.route)
} else if (item.action) {
item.action()
}
}
// ── 清空缓存 ──
function onClearCache(): void {
showDialog({
title: '提示',
message: '确定要清空缓存吗?',
})
.then(() => {
showToast('缓存已清空')
})
.catch(() => {})
}
// ── 版本信息 ──
function onShowVersion(): void {
showToast('版本 v2.0.0')
}
// ── 退出登录 ──
async function onLogout(): Promise<void> {
try {
await userStore.logout()
router.replace('/login')
} catch {
router.replace('/login')
}
}
</script> </script>
<template> <template>
<div class="mine-page"> <div class="mine-page">
<van-nav-bar title="我的" fixed placeholder /> <!-- 顶部导航栏 -->
<van-nav-bar title="我的" fixed placeholder>
<template #right>
<van-icon name="setting-o" size="20" @click="router.push('/menuEdit')" />
<van-icon name="bell" size="20" style="margin-left: 14px" @click="router.push('/noticeList')" />
</template>
</van-nav-bar>
<div class="mine-content"> <!-- 页面主体 -->
<div class="user-card"> <div class="mine-body">
<div class="avatar-placeholder"></div> <!-- 顶部蓝色渐变区域 -->
<div class="user-info"> <div class="mine-header">
<span class="user-name">未登录</span> <div class="header-bg"></div>
<div class="header-user">
<div class="user-avatar">
<van-icon name="user-o" size="28" color="#ffffff" />
</div>
<div class="user-text">
<span class="user-name">{{ userStore.userName || '未登录' }}</span>
<span class="user-dept">{{ userStore.userInfo?.deptName || '' }}</span>
</div>
</div> </div>
</div> </div>
<van-cell-group inset> <!-- 快捷入口 -->
<van-cell title="个人信息" is-link /> <div class="shortcuts">
<van-cell title="系统设置" is-link /> <div
<van-cell title="关于我们" is-link /> v-for="item in shortcuts"
:key="item.id"
class="shortcut-item"
@click="onShortcutClick(item)"
>
<div class="shortcut-icon-box">
<van-icon :name="item.icon" size="22" color="#1E74FF" />
<span v-if="item.badge" class="shortcut-badge">{{ item.badge }}</span>
</div>
<span class="shortcut-label">{{ item.label }}</span>
</div>
</div>
<!-- 设置列表 -->
<van-cell-group inset class="setting-group">
<van-cell
v-for="item in settings"
:key="item.id"
:title="item.label"
is-link
@click="onSettingClick(item)"
>
<template #icon>
<van-icon :name="item.icon" size="18" color="#1E74FF" class="cell-icon" />
</template>
</van-cell>
</van-cell-group> </van-cell-group>
<!-- 退出登录 -->
<div class="logout-wrapper"> <div class="logout-wrapper">
<van-button round block type="danger" @click="router.replace('/login')"> <van-button round block class="logout-btn" @click="onLogout">
退出登录 退出登录
</van-button> </van-button>
</div> </div>
<!-- 底部安全区占位 -->
<div class="safe-bottom"></div>
</div> </div>
</div> </div>
</template> </template>
@@ -39,39 +185,184 @@ const router = useRouter()
<style lang="scss" scoped> <style lang="scss" scoped>
.mine-page { .mine-page {
min-height: 100vh; min-height: 100vh;
background: var(--color-bg-page); background: var(--color-bg-page, #f4f7f8);
display: flex;
flex-direction: column;
// ── 导航栏 ──
:deep(.van-nav-bar) {
background: #1E74FF;
.van-nav-bar__title {
color: #ffffff;
}
.van-nav-bar__left,
.van-nav-bar__right {
.van-icon {
color: #ffffff;
}
}
}
} }
.mine-content { // ── 页面主体 ──
padding: 16px; .mine-body {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
} }
.user-card { // ── 头部蓝色渐变区域 ──
.mine-header {
position: relative;
width: 100%;
height: 264px;
overflow: hidden;
.header-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg, #1E74FF 0%, #64B5F6 100%);
}
.header-user {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 14px;
padding: 60px 24px 0;
}
.user-avatar {
width: 56px;
height: 56px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.25);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border: 2px solid rgba(255, 255, 255, 0.4);
}
.user-text {
display: flex;
flex-direction: column;
gap: 6px;
}
.user-name {
font-size: 18px;
font-weight: 600;
color: #ffffff;
line-height: 1;
}
.user-dept {
font-size: 13px;
color: rgba(255, 255, 255, 0.8);
line-height: 1;
}
}
// ── 快捷入口 ──
.shortcuts {
display: flex;
margin: -20px 12px 12px;
padding: 18px 0;
background: #ffffff;
border-radius: 10px;
position: relative;
z-index: 2;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.shortcut-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
cursor: pointer;
&:active {
opacity: 0.7;
}
}
.shortcut-icon-box {
position: relative;
width: 46px;
height: 46px;
border-radius: 50%;
background: #f0f6ff;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; justify-content: center;
background: var(--color-bg-card);
border-radius: 12px;
padding: 24px 16px;
margin-bottom: 16px;
box-shadow: var(--shadow-sm);
} }
.avatar-placeholder { .shortcut-badge {
width: 56px; position: absolute;
height: 56px; top: -4px;
border-radius: 50%; right: -4px;
background: var(--color-primary-bg); min-width: 18px;
height: 18px;
border-radius: 9px;
background: #ee0a24;
color: #ffffff;
font-size: 11px;
font-weight: 600;
line-height: 18px;
text-align: center;
padding: 0 4px;
} }
.user-name { .shortcut-label {
font-size: 16px; font-size: 13px;
font-weight: 500; color: var(--color-text-regular, #666);
color: var(--color-text-primary);
} }
// ── 设置列表 ──
.setting-group {
margin: 0 12px 12px;
:deep(.van-cell-group) {
border-radius: 10px;
overflow: hidden;
}
:deep(.van-cell) {
align-items: center;
}
.cell-icon {
margin-right: 8px;
}
}
// ── 退出登录 ──
.logout-wrapper { .logout-wrapper {
margin-top: 24px; margin: 24px 16px 0;
padding: 0 16px; }
.logout-btn {
:deep(.van-button__text) {
color: #ee0a24;
}
:deep(.van-button) {
background: #ffffff;
border: none;
}
}
// ── 底部安全区 ──
.safe-bottom {
height: 16px;
} }
</style> </style>