feat: Batch 1 - basic pages (scanCode/privacyPolicy/resetPwd/notice/menuEdit/addGroup)

Agent Loop: 2 agents, both passed on iteration 1
6 new pages + 6 routes added
This commit is contained in:
2026-06-15 21:04:21 +08:00
parent 9b68a2d275
commit f585c1c505
9 changed files with 1158 additions and 0 deletions

View File

@@ -65,3 +65,6 @@ export function createBridge(): IBridge {
return browserProvider return browserProvider
} }
} }
/** 当前环境的 Bridge 单例,页面直接使用 */
export const bridge: IBridge = createBridge()

View File

@@ -23,6 +23,17 @@ import {
CellGroup, CellGroup,
Popup, Popup,
Uploader, Uploader,
PullRefresh,
List,
Tabs,
Tab,
Divider,
Loading,
Icon,
Grid,
GridItem,
Empty,
Search,
} from 'vant' } from 'vant'
// ── Vant 样式(组件样式按需引入) ── // ── Vant 样式(组件样式按需引入) ──
@@ -62,6 +73,17 @@ const vantComponents = [
CellGroup, CellGroup,
Popup, Popup,
Uploader, Uploader,
PullRefresh,
List,
Tabs,
Tab,
Divider,
Loading,
Icon,
Grid,
GridItem,
Empty,
Search,
] ]
for (const component of vantComponents) { for (const component of vantComponents) {

View File

@@ -63,6 +63,54 @@ const routes: RouteRecordRaw[] = [
title: '我的', title: '我的',
}, },
}, },
{
path: '/mine/scanCode',
name: 'ScanCode',
component: () => import('@/views/mine/scanCode.vue'),
meta: {
title: '扫一扫',
},
},
{
path: '/mine/privacyPolicy',
name: 'PrivacyPolicy',
component: () => import('@/views/mine/privacyPolicy.vue'),
meta: {
title: '隐私政策',
},
},
{
path: '/mine/resetPwd',
name: 'ResetPwd',
component: () => import('@/views/mine/resetPwd.vue'),
meta: {
title: '修改密码',
},
},
{
path: '/noticeList',
name: 'noticeList',
component: () => import('@/views/notice/index.vue'),
meta: {
title: '消息通知',
},
},
{
path: '/menuEdit',
name: 'menuEdit',
component: () => import('@/views/menuEdit/index.vue'),
meta: {
title: '工作台设置',
},
},
{
path: '/addGroup/:title?',
name: 'addGroup',
component: () => import('@/views/menuEdit/addGroup.vue'),
meta: {
title: '分组管理',
},
},
// 404 兜底 // 404 兜底
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',

View File

@@ -0,0 +1,163 @@
<script setup lang="ts">
/**
* 添加/编辑分组
*
* 使用 van-form + van-field 收集分组名称,
* 支持添加和编辑两种模式。
*/
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { showToast } from 'vant'
const route = useRoute()
const router = useRouter()
/** 页面标题(添加/编辑) */
const title = computed(() => (route.params.title as string) || '添加')
const pageTitle = computed(() => `${title.value}分组`)
/** 分组 ID编辑模式时存在 */
const groupId = ref<string>((route.query.groupId as string) || '')
/** 分组名称 */
const menuTitle = ref<string>((route.query.name as string) || '')
/** 是否正在提交 */
const isSubmitting = ref(false)
/**
* 提交表单
*/
function onSubmit() {
const trimmedTitle = menuTitle.value.trim()
if (!trimmedTitle) {
showToast('分组名称不能为空')
return
}
if (trimmedTitle.length > 10) {
showToast('分组名称最多10个字符')
return
}
if (isSubmitting.value) return
isSubmitting.value = true
setTimeout(() => {
isSubmitting.value = false
if (groupId.value) {
showToast('编辑成功')
} else {
showToast('添加成功')
}
router.back()
}, 300)
}
/**
* 删除分组(仅编辑模式)
*/
function onDelete() {
// showConfirmDialog would be used here with real API
showToast('删除功能待接入')
}
</script>
<template>
<div class="add-group-page">
<!-- 顶部导航栏 -->
<van-nav-bar :title="pageTitle" left-text="返回" left-arrow fixed placeholder @click-left="router.back()" />
<div class="add-group-content">
<!-- 分组名称输入 -->
<div class="form-section">
<van-field
v-model="menuTitle"
maxlength="10"
placeholder="请输入分组名称"
label="分组名称"
/>
</div>
<!-- 操作按钮 -->
<div class="action-section" v-if="title === '添加'">
<van-button
round
block
type="primary"
:loading="isSubmitting"
loading-text="保存中..."
@click="onSubmit"
>
保存
</van-button>
</div>
<div class="action-section-edit" v-if="title === '编辑'">
<van-button
round
block
type="danger"
@click="onDelete"
>
删除
</van-button>
<van-button
round
block
type="primary"
:loading="isSubmitting"
loading-text="保存中..."
@click="onSubmit"
>
保存
</van-button>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.add-group-page {
min-height: 100vh;
background: #f4f7f8;
:deep(.van-nav-bar) {
background: var(--color-primary);
--van-nav-bar-title-text-color: #fff;
--van-nav-bar-text-color: #fff;
--van-nav-bar-icon-color: #fff;
}
}
.add-group-content {
width: 100%;
}
.form-section {
margin: 12px;
border-radius: 10px;
background: #fff;
overflow: hidden;
}
.action-section {
padding: 10px 16px;
}
.action-section-edit {
padding: 10px 16px;
display: flex;
gap: 12px;
.van-button {
flex: 1;
}
.van-button--danger {
flex: 0 0 30%;
}
.van-button--primary {
flex: 1;
}
}
</style>

View File

@@ -0,0 +1,153 @@
<script setup lang="ts">
/**
* 工作台设置 / 菜单编辑器
*
* 展示分组列表支持拖拽排序van-grid 展示)。
* 点击"添加分组"跳转到 addGroup 页面。
*/
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
/** 分组列表 */
interface GroupItem {
id: number
name: string
moving: boolean
}
const drag = reactive({
list: [] as GroupItem[],
})
/**
* 模拟初始化数据
*/
onMounted(() => {
drag.list = [
{ id: 1, name: '常用功能', moving: false },
{ id: 2, name: '管网管理', moving: false },
{ id: 3, name: '巡检管理', moving: false },
{ id: 4, name: '报表统计', moving: false },
]
})
/**
* 跳转添加/编辑分组
*/
function toAddGroup(type: number, item?: GroupItem) {
if (type === 0) {
router.push({
name: 'addGroup',
params: { title: '添加' },
})
} else if (item) {
router.push({
name: 'addGroup',
params: { title: '编辑' },
query: {
name: item.name,
groupId: item.id,
},
})
}
}
</script>
<template>
<div class="menu-edit-page">
<!-- 顶部导航栏 -->
<van-nav-bar title="工作台设置" left-text="返回" left-arrow fixed placeholder @click-left="router.back()" />
<div class="menu-edit-content">
<!-- 添加分组按钮 -->
<div class="add-group-btn" @click="toAddGroup(0)">
添加分组
</div>
<!-- 分组列表 -->
<div class="group-list">
<div
class="group-item"
v-for="item in drag.list"
:key="item.id"
@click="toAddGroup(1, item)"
>
<div class="group-item-name">{{ item.name }}</div>
<div class="group-item-actions">
<van-icon name="arrow" />
</div>
</div>
</div>
<!-- 空状态 -->
<van-empty v-if="drag.list.length === 0" description="暂无分组" />
</div>
</div>
</template>
<style lang="scss" scoped>
.menu-edit-page {
min-height: 100vh;
background: #f4f7f8;
:deep(.van-nav-bar) {
background: var(--color-primary);
--van-nav-bar-title-text-color: #fff;
--van-nav-bar-text-color: #fff;
--van-nav-bar-icon-color: #fff;
}
}
.menu-edit-content {
width: 100%;
}
.add-group-btn {
width: 100%;
height: 40px;
text-align: center;
font-size: 15px;
color: #3d6ffb;
line-height: 40px;
background-color: #fff;
cursor: pointer;
&:active {
background: #f5f5f5;
}
}
.group-list {
padding: 0 12px;
}
.group-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 56px;
margin: 12px 0;
padding: 0 12px;
background: #fff;
border-radius: 10px;
color: #333;
font-size: 15px;
cursor: pointer;
&:active {
background: #f5f5f5;
}
&-name {
flex: 1;
}
&-actions {
display: flex;
align-items: center;
color: #c8c9cc;
}
}
</style>

View File

@@ -0,0 +1,225 @@
<script setup lang="ts">
/**
* 隐私政策页面
*
* 静态展示隐私政策内容,支持滚动阅读。
*/
import { useRouter } from 'vue-router'
const router = useRouter()
function goBack(): void {
router.back()
}
</script>
<template>
<div class="privacy-policy-page">
<van-nav-bar
title="隐私政策"
left-text="返回"
left-arrow
@click-left="goBack"
fixed
placeholder
/>
<div class="policy-content">
<div class="policy-text">
发布日期2025年01月15日<br />
生效日期2025年01月16日<br />
更新日期2025年01月16日<br />
本政策仅适用于"舆图智慧水务平台"产品
本隐私权政策旨在帮助您了解我们会收集哪些数据为什么收集这些数据会利用这些数据做什么以及我们如何保护这些数据了解这些内容对于您行使个人权利及保护您的个人信息至关重要请您在使用舆图智慧水务平台产品或服务前务必抽出时间认真阅读本政策当您开始下载访问或使用舆图智慧水务平台产品或服务即表示您已经同意本隐私政策并信赖我们对您的信息的处理方式
需要特别说明的是本政策不适用于其他第三方向您提供的服务也不适用于舆图智慧水务平台在提供服务时另行独立设置法律声明及隐私权政策的产品或服务当您通过舆图智慧水务平台产品或服务使用第三方产品或服务时您的信息应当适用第三方的隐私条款我们强烈建议您在接受服务特别是与个人信息相关的服务前阅读并确认理解相关协议
本政策包含以下内容
1 我们如何收集和使用您的个人信息
2 我们如何使用Cookie和同类技术
3 我们如何共享转让和公开披露个人信息
4 我们如何保存及保护您的个人信息
5 用户权利与义务
6 变更
7 未成年人信息的保护
8 适用法律与争议解决
感谢您对舆图智慧水务平台的使用与信任舆图智慧水务平台深知个人信息对您的重要性我们将持续致力于为您提供更加可靠的服务
1.我们如何收集和使用您的个人信息
个人信息"是指以电子或者其他方式记录的能够单独或者与其他信息结合识别特定自然人身份或者反映特定自然人活动情况的各种信息。在本隐私政策中包括姓名、手机号码、用户密码、身份信息、照片、面部识别特征、性别、年龄、职业信息、征信信息、是否有犯罪记录、位置信息、行程信息、银行卡号及绑定手机号、评价信息、日志信息、设备信息、IP地址、通话记录、通讯地址、邮编、联系方式。您理解并同意其他无法通过其单独或者与其他信息结合识别您的个人身份或活动情况的信息不属于您的个人信息。
1.1我们收集个人信息的方式
1在您开启设备定位功能并使用舆图智慧水务平台提供的基于位置的服务时我们需要收集您的设备信息、位置信息、网络身份标识信息。我们会使用各种方式进行定位:包括通过IP地址、GPS以及能够提供相天信息的其他传感器等比如说可能会为舆图智慧水务平台提供附近设备、Wi-Fi接入点和基站的信息。我们会对舆图智慧水务平台用户的位置信息进行定位计算。您可以通过系统授权关闭定位服务或系统权限停止舆图智慧水务平台对您的位置信息的收集但您可能将无法获得相关服务或功能或者无法达到相关服务拟达到的效果。
2当您使用搜索服务时我们会收集您的搜索查询的内容、点击的链接、您的IP地址、设备信息、浏览器的类型和使用的语言、操作系统版本、网络运营商的信息、访问服务时间。我们会基于前述信息、结合我们收集的位置信息以及您的收藏数据对您的偏好习惯、位置作特征分析以便为您提供更合适您和您的设备的定制化服务。
3当您使用导航服务时我们会收集您的位置信息、地址信息及行踪轨迹。
4当您使用的客户端软件频繁向我们进行报错或显示错误日志信息时为了检查软件错误给您提供更好的服务我们会在您的设备上记录、收集您的配置信息、异常数据等用于诊断并完善产品及服务的文件。
5当您与舆图智慧水务平台联系时为了方便与您联系或帮助您解决问题我们可能会记录您的联系方式、身份信息并结合需要记录您的通话内容。
6为开展数据分析和更好地改善舆图智慧水务平台的产品和服务我们会收集您的日志信息包括检索内容、IP地址、设备信息包括设备型号、设备识别码、操作系统、系统语言、分辨率、电信运营商、SIM卡归属地以为您提供更加个性化、便捷的服务我们会使用浏览器网络存储机制和应用程序数据缓存在您的设备上收集信息并进行本地存储。
7您通过舆图智慧水务平台使用第三方服务时直接向第三方提供的个人信息或授权舆图智慧水务平台共享给第三方的个人信息将适用该第三方的隐私保护政策。当您查看第三方创建的网页或者使用第三方开发的应用程序时这些第三方可能会放置他们自己的cookie或像素标签这些cookie和像素标签不受我们的控制且它们的使用不受本政策的约束。我们会尽商业上的合理努力去要求这些主体对您的个人信息采取保护措施但我们无法保证这些主体一定会按照我们的要求采取保护措施。请您在使用前仔细阅读他们的隐私保护政策以判断是否继续使用该第三方服务。
8您通过第三方平台使用舆图智慧水务平台服务时舆图智慧水务平台会间接向第三方收集您的手机号、位置信息以及设备号以识别您的身份、确定您的位置并为您提供更好的服务。舆图智慧水务平台向第三方收集上述个人信息前将对个人信息来源的合法性进行确认。
9请您理解我们向您提供的功能和服务是不断更新和发展的如果某一功能或服务未在前述说明中且收集了您的信息我们会通过页面提示、交互流程、网站公告、另行签署协议等方式另行向您说明信息收集的内容、范围和目的以征得您的同意。
如您拒绝此类信息收集及使用,需要在设备系统中进行设置或关闭提供服务的软件,舆图智慧水务平台无法自动或手动设置关闭相关服务。
10舆图智慧水务平台可能会调用您的一些设备权限以下分别是
Android系统调用权限对应的业务功能、我们调用权限的目的以及调用权限前向您询问的情况。使用Android系统的不同设备中权限显示方式及关闭方式可能有所不同为了保护您的隐私请您仔细阅读并了解我们对于设备权限的调取情况。
1. Android系统权限调用情况
1位置权限
对应的业务功能:地图定位、巡查打卡定位
调用权限的目的:为您提供点位附近水务设施以及附属设备信息,方便行业人员使用作业
何时询问授权:首次安装时弹窗询问;如未获取,将在点击具体业务功能时询问
用户可否关闭权限:是
2电话权限
对应的业务功能:拨打电话
调用权限的目的:为您提供获取作业人员电话并快速拨打进行沟通
何时询问授权:首次安装时弹窗询问;如未获取,将在点击具体业务功能时询问
用户可否关闭权限:是
3相册权限
对应的业务功能:拍照上传
调用权限的目的:为您提供巡查打卡上传作业照片
何时询问授权:首次安装时弹窗询问;如未获取,将在点击具体业务功能时询问
用户可否关闭权限:是
我们会在您在某一场景首次使用某一设备权限时通过弹窗的方式向您请求开启相应权限,并授权我们为提供特定服务而收集、使用相应的个人信息。如涉及重要或敏感的设备权限时,我们会在您使用相应服务时,另行弹窗征得您的明示同意后开启相应设备权限。
权限开启后,您可以随时在设备的系统设置功能中选择打开或关闭相应的权限,从而允许或拒绝我们收集和使用您的个人信息。但请您注意,相应权限关闭后会影响到对应产品功能的正常使用。
我们承诺不会在您未授权同意的场景中调用设备权限或将通过调用设备权限收集的个人信息用于您未授权同意的场景。
1.2 我们使用个人信息的方式
1我们将按照在1.1条款中注明的方式在服务中使用您的个人信息,如我们需要基于本政策未载明的其他用途或非特定目的收集或使用您的个人信息,我们会事先征求您的同意。
2我们将根据您的授权在您使用舆图智慧水务平台产品及服务期间使用上述信息。
3我们可能使用您的信息用于安全防范、诈骗预防及非法监测等以预防、发现、调查危害公共安全侵犯个人合法权益、违反法规或协议规则的行为以保护您、我们或我们的关联公司、合作伙伴的合法权益及社会公益。
4如我们停止运营舆图智慧水务平台产品或服务我们将及时停止您仅授权于该服务使用的个人信息并将以逐一送达或公告的形式通知您并对我们所持有的与已关停业务相关的个人信息进行删除或匿名化处理。
5舆图智慧水务平台在提供服务过程中会调用您的一些设备权限您可以在设备的设置功能中选择关闭部分或全部权限从而拒绝舆图智慧水务平台收集与使用相应的个人信息。在不同设备中权限显示方式及关闭方式有所不同请您参照设备或系统开发说明与指引。您的设备系统可能会在必要信息授权的同时开放其他权限舆图智慧水务平台自身无法通过手动或自动设置关闭授权。
1.3 去标识化
在收集到您的个人信息后,我们可通过技术手段及时对数据进行去标识化处理,去标识化处理的信息将无法识别主体。在不透露您个人信息的前提下,舆图智慧水务平台有权对用户数据进行挖掘、分析和利用(包括商业化利用)。
1.4 例外
1以下情形中收集您的个人信息无需经过您的授权同意
a.与国家安全、国防安全有关的;
b.与公共安全、公共卫生、重大公共利益有关的;
c.与犯罪侦查、起诉、审判和判决执行等有关的;
d.出于维护您或其他个人的生命、财产等重大合法权益但又很难得到您同意的;
e.所收集的个人信息是您自行向社会公众公开的;
f.从合法公开披露的信息中收集个人信息的,如合法的新闻报道、政府信息公开等渠道;
g.根据您的要求实现产品功能及服务所必需的;
h.法律法规规定的其他情形。
2.我们如何使用Cookie和同类技术
1当您使用舆图智慧水务平台产品或服务时为使您获得更轻松的访问体验我们可能会使用各种技术来收集和存储信息在此过程中可能会向您的设备发送一个或多个Cookie或匿名标识符。这么做是为了了解您的使用习惯使您省去重复输入注册信息等步骤或帮助判断您的账户安全。
2当您使用舆图智慧水务平台产品或服务时我们还可能会利用Cookie和同类技术收取您的信息用于了解您的偏好进行咨询或数据分析改善产品服务及用户体验及时发现并防范安全风险为用户和合作伙伴提供更好的服务。
3我们不会将Cookie用于本政策所述目的之外的任何用途您可根据自己的偏好留存或删除Cookie。您可清除软件内保存的所有Cookie当您手动清除后您的相关信息即已删除。
3.我们如何共享、转让、公开披露您的个人信息
3.1共享
1舆图智慧水务平台会以高度的勤勉义务对待您的信息不会与舆图智慧水务平台及其关联公司以外的任何公司、组织和个人分享您的个人信息。但以下情况除外
a.获得您的明确授权或同意;
b.根据适用的法律法规、法律程序的要求或强制性的政府要求或司法裁定舆图智慧水务平台必须提供;
c.在法律要求或允许的范围内,为了保护舆图智慧水务平台、舆图智慧水务平台用户或社会公众的利益:财产或安全免遭损害而有必要提供;
d.由于我们的某些服务将由授权合作伙伴提供,舆图智慧水务平台可能会向合作伙伴等第三方提供您的个人信息。我们的合作伙伴无权将共享的个人信息用于其他任何用途,并将遵守本隐私权政策保护您的个人信息。
2我们的授权合作伙伴可能包括以下类型
a.软件服务提供商、智能设备提供商和系统服务提供商。
舆图智慧水务平台与第三方软件、设备系统结合为您提供基于位置的服务时,可能会基于您对系统定位的授权及设定,收取您的位置信息及设备信息并提供给系统,特殊情境下会包含您另行填写的个人信息。如果您拒绝此类信息收集及使用,您需要在设备系统中进行设置或关闭提供服务的软件,舆图智慧水务平台无法通过自动或手动设置关闭相关服务。
b.供应商、服务提供商和其他合作伙伴。
我们可能将信息发送给支持我们业务的供应商、服务提供商和其他合作伙伴,这些支持包括提供技术基础设施服务、提供产品内或产品链接后的功能型服务,分析我们服务的使用方式、服务的有效性、提供客户服务和调查。
(3)共享要求:
在共享信息前,我们会与共享个人信息的第三方签署严格的数据安全和保密协定,要求他们按照我们的说明、本隐私权政策采取相关的保密和安全措施来处理上述信息。
3.2转让与公开披露
1转让。我们不会将您的个人信息转让给除舆图智慧水务平台及其关联公司外的公司、组织和个人但以下情况除外
a.事先获得您的明确授权或同意;
b.根据法律法规、法律程序的要求或强制性的政府要求或司法裁定舆图智慧水务平台必须提供;
c.符合您签署的其它法律文件(如本条款)的约定而使用;
d.在涉及合并、收购或破产清算时,如涉及到个人信息转让,我们会要求新的持有您个人信息的公司、组织继续受此隐私权政策的约束,否则我们会要求该公司、组织重新向您征求授权同意。
2公开披露
我们仅会在以下情况下,且采取符合业界标准的安全防护措施的前提下,才会公开披露您的个人信息:1.根据您的需求,在您明确同意的披露方式下披露您所指定的个人信息:2、根据法律、法规的要求、强制性的行政执法或司法要求所必须提供您个人信息的情况下我们可能会依据所要求的个人信息类型和披露方式公开披露您的个人信息。在符合法律法规的前提下当我们收到上述披露信息的请求时我们会要求必须出具与之相应的法律文件如传票或调查函。我们坚信对于要求我们提供的信息应该在法律允许的范围内尽可能保持透明。我们对所有的请求都进行了慎重的审查以确保其具备合法依据且仅限于执法部门因特定调查目的且有合法权利获取的数据。
4.我们如何存储及保护您的个人信息
4.1信息存储的地域
1我们会按照法律法规规定将境内收集的用户个人信息存储于中国境内除外如需跨境传输我们将会单独征得您的授权同意。
2目前我们不会跨境传输或存储您的个人信息。将来如您因境外使用舆图智慧水务平台产品或服务需跨境传输或存储的我们会向您告知信息出境的目的、接收方、安全保证措施和安全风险并征得您的同意。
4.2信息存储的方式和期限
我们仅在本《隐私政策》所述目的所必需期间和法律法规要求的时限内保留您的信息。我们在中华人民共和国境内运营中收集和产生的信息,存储在中国境内,并在本政策所述目的所需的最短期限内保留您的个人信息。超出保存期限后,我们会对您的个人信息进行删除;除非法律法规另有规定,舆图智慧水务平台将按如下期间保存您的信息:
1您使用舆图智慧水务平台产品或服务期间我们将持续为您保存除非您自主删除这些信息
2当您删除个人信息后我们将及时采取措施将您的个人信息从业务功能系统删除使其保持不可被检索、访问的状态
3为了维护您和舆图智慧水务平台的合法权益我们有权在您删除账户之日起12个月内继续在单独的系统中保存您的信息用于证据保存。证据保存期限到期后我们将及时对该等保存的个人信息进行匿名化处理。
4.3安全措施
1舆图智慧水务平台会采取适当的符合业界标准的安全措施和技术手段存储和保护您的个人信息以防止其丢失、被误用、受到未授权访问或泄漏、被篡改或毁坏。
2我们会部署访问控制机制仅允许有必要知晓这些信息的舆图智慧水务平台和其关联公司的员工在采取合理的措施验证身份之后访问或修改这些信息。同时我们会严格要求他们履行保密及安全义务。
4.4特别提示
1互联网并非绝对安全的环境电子邮件、即时通讯、社交软件、交易平台等与其他用户的交流方式无法确定是否完全加密请您在进行交互使用时注意保护您个人信息的安全。
2请您理解由于计算机及互联网技术的飞速发展及同步速度的限制可能存在或出现各种恶意或非恶意的攻击手段。虽然舆图智慧水务平台持续致力于提升和加强安全措施以保护您的信息免遭意外或破坏但仍无法始终保证信息的百分之百安全。
3您使用产品或服务时所用的系统和通讯网络或硬件设备等舆图智慧水务平台均无法控制请您了解并注意保护您的个人信息安全。
4您的个人信息经匿名化处理后将形成可以使用及流通的数据舆图智慧水务平台对此类数据的保存及处理无需另行通知并征得您的同意。
4.5安全事件
1如不幸发生个人信息安全事件后我们将按照法律法规的要求及时向您告知安全事件的基本情况和可能的影响、我们已采取或将要采取的处理措施、您可自主防范和降低的风险的建议、对您的补救措施等。
2我们会及时处置系统漏洞、网络攻击、病毒侵袭及网络侵入等安全风险。在发生危害网络安全的事件时我们会按照网络安全事件应急预案及时采取相应的补救措施并按照规定向有关主管部门报告。
5.用户权利
舆图智慧水务平台非常尊重您对个人信息的关注,:为您提供了管理个人信息的方法。您有权利查询、更正、管理、删除自己的信息并保护自己的隐私和安全。
5.1查询权
除法律法规规定的例外情况,无论您何时使用我们的服务,我们都会力求让您顺利访问自己的个人信息。
5.2更正权
当您需要更新您的个人信息时,或发现我们处理您的个人信息有错误时,您有权作出更正或更新。您可以自行在舆图智慧水务平台产品内进行更正,或通过反馈与报错等将您的更正申请提交给我们。
5.3删除权
1您可以通过您设备的应用管理清除您的信息。当您删除个人信息后我们将及时采取措施将您的个人信息从业务功能系统删除使其保持不可被检索、访问的状态。
2发生以下情形您可以向我们提出删除个人信息的请求
a如果我们处理个人信息的行为违反法律法规
b如果收集、使用您的个人信息却未征得您的同意
c如果我们处理个人信息的行为违反了与您的约定
d如果我们终止产品运营及服务。
5.4撤销权
1如您希望撤回对于思明智慧水务授予的各项权限的授权您可通过您设备的权限设置路径撤回相关同意。
2当您撤回同意或授权后我们无法继续为您提供撤回同意或授权所对应的服务也将不再处理您相应的个人信息。
5.5获取个人信息副本
如您想获取我们所收集的关于您个人基本资料、行程信息的副本您可以通过技术服务渠道与我们取得联系我们将在15个工作日内回复您的获取请求。
5.6管理问题
1如果您无法通过上述方式访问、更正或删除您的个人信息或者您就舆图智慧水务平台收集、使用您信息有任何疑问或者问题您都可以通过舆图智慧水务平台技术服务渠道与我们取得联系。为保障安全我们可能需要您提供书面请求或以其他方式证明您的身份。
2我们将尽合理商业努力满足您对于个人信息的查询、更正、管理、删除、获取信息副本的要求。但对于无端重复、需要过多技术手段、给他人合法权益带来风险或者非常不切实际的请求我们可能会予以拒绝。
5.7例外
按照法律法规要求,以下情况中,我们将无法响应您的请求:
a.与国家安全、国防安全有关的;
b.与公共安全、公共卫生、重大公共利益有关的;
c.与犯罪侦查、起诉和审判等有关的;
d.有充分证据表明您存在主观恶意或滥用权利的;
e.响应您的请求将导致您或其他个人、组织的合法权益受到严重损害的。
6.变更
6.1更新与调整
本政策将根据业务模式的调整或更新不定期进行修订,该等修订构成本隐私权政策的一部分。
6.2告知方式
1未经您的明确同意我们不会削减您按照本隐私权政策所享有的权利。我们会通过合理的方式发布隐私权政策所做的变更。若您继续使用我们的产品或服务即表示您同意修订后的隐私权政策条款。
2我们可能会适时对本隐私保护指引进行修订。当隐私保护指引的条款发生变更时我们会在版本更新时以公告弹窗的方式向您提示变更后的隐私保护指引并向您说明生效日期。
7.未成年人保护
我们非常重视对未成年人个人信息的保护。我们会根据相关法律法规的规定,要求不满十四周岁的未成年人在使用思明智慧水务的服务或向我们提供个人信息前,应当事先取得自己的监护人(比如自己的父母)的授权同意;已满十四周岁未满十八周岁的未成年人,可以事先取得自己的监护人的授权同意或自行授权同意。
我们仅在法律法规允许、未成年人的监护人明确同意或者有必要保护未成年人的情况下使用、对外提供或公开披露该信息。
若您是未成年人的监护人,请您关注您监护的未成年人是否是在取得您的授权同意之后使用舆图智慧水务平台的服务。如果您对您所监护的未成年人的个人信息有疑问,请通过隐私政策中的联系方式与我们联系。
8.使用法律与争议解决
8.1适用法律
本政策的执行、解释及争议的解决均适用中华人民共和国法律,且不考虑任何冲突法。
8.2争议解决
您和舆图智慧水务平台就本政策内容或其执行发生任何争议,双方应友好协商解决;如双方无法协商解决争议时,双方同意由本协议签订地有管辖权的人民法院管辖,本协议的签订地为厦门市思明区。
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.privacy-policy-page {
min-height: 100vh;
background: #f4f7f8;
}
.policy-content {
width: 100%;
height: calc(100vh - 46px);
overflow: hidden;
}
.policy-text {
margin: 0 18px;
padding: 10px;
height: 100%;
overflow-y: scroll;
line-height: 1.5;
font-size: 14px;
background-color: #fff;
color: var(--color-text-primary, #333);
&::-webkit-scrollbar {
display: none;
}
}
</style>

143
src/views/mine/resetPwd.vue Normal file
View File

@@ -0,0 +1,143 @@
<script setup lang="ts">
/**
* 重置密码页面
*
* 提供旧密码验证 + 新密码设置的表单,
* 调用 resetPassword API 完成密码重置。
*/
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { showSuccessToast, showFailToast } from 'vant'
import { resetPassword } from '@/api'
const router = useRouter()
/** 表单加载状态 */
const loading = ref(false)
/** 密码表单数据 */
const pwdForm = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: '',
})
/** 表单校验规则 */
const rules = {
oldPassword: [{ required: true, message: '请输入旧密码' }],
newPassword: [
{ required: true, message: '请输入新密码' },
{ min: 6, message: '新密码长度不能少于6位' },
],
confirmPassword: [
{ required: true, message: '请确认新密码' },
],
}
/**
* 返回上一页
*/
function goBack(): void {
router.back()
}
/**
* 自定义校验:确认密码是否一致
*/
function validateConfirm(): boolean {
if (pwdForm.newPassword !== pwdForm.confirmPassword) {
showFailToast('两次输入的新密码不一致')
return false
}
return true
}
/**
* 处理重置密码提交
*/
async function handleSubmit(): Promise<void> {
if (!validateConfirm()) return
loading.value = true
try {
await resetPassword({
oldPassword: pwdForm.oldPassword,
newPassword: pwdForm.newPassword,
})
showSuccessToast('修改成功')
router.back()
} catch (err) {
const msg = err instanceof Error ? err.message : '修改失败,请重试'
showFailToast(msg)
} finally {
loading.value = false
}
}
</script>
<template>
<div class="reset-pwd-page">
<van-nav-bar
title="修改密码"
left-text="返回"
left-arrow
@click-left="goBack"
fixed
placeholder
/>
<div class="reset-pwd-content">
<van-form @submit="handleSubmit">
<van-cell-group inset>
<van-field
v-model="pwdForm.oldPassword"
name="oldPassword"
type="password"
label="旧密码"
placeholder="请输入旧密码"
:rules="rules.oldPassword"
/>
<van-field
v-model="pwdForm.newPassword"
name="newPassword"
type="password"
label="新密码"
placeholder="请输入新密码"
:rules="rules.newPassword"
/>
<van-field
v-model="pwdForm.confirmPassword"
name="confirmPassword"
type="password"
label="确认新密码"
placeholder="请确认新密码"
:rules="rules.confirmPassword"
/>
</van-cell-group>
<div style="margin: 20px 16px 0 16px;">
<van-button
round
block
type="primary"
native-type="submit"
:loading="loading"
loading-text="修改中..."
>
</van-button>
</div>
</van-form>
</div>
</div>
</template>
<style lang="scss" scoped>
.reset-pwd-page {
min-height: 100vh;
background: var(--color-bg-page, #f4f7f8);
}
.reset-pwd-content {
padding-top: 20px;
}
</style>

180
src/views/mine/scanCode.vue Normal file
View File

@@ -0,0 +1,180 @@
<script setup lang="ts">
/**
* 扫码页面
*
* 使用 Bridge 层 scanCode 能力进行二维码/条形码扫描,
* 扫描成功后跳转到结果 URL 或展示结果文本。
*/
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { showToast } from 'vant'
import { bridge } from '@/bridge'
const router = useRouter()
/** 扫码结果文本 */
const scanResult = ref('')
/** 是否正在扫码 */
const scanning = ref(false)
/**
* 返回上一页
*/
function goBack(): void {
router.back()
}
/**
* 开始扫码
*/
async function startScan(): Promise<void> {
scanning.value = true
scanResult.value = ''
try {
const res = await bridge.scanCode({ type: 'all' })
scanResult.value = res.result
// 如果扫描结果是一个链接,直接跳转
if (res.result && /^https?:\/\//i.test(res.result)) {
window.location.href = res.result
}
} catch (err) {
const msg = err instanceof Error ? err.message : '扫码失败'
showToast(msg)
} finally {
scanning.value = false
}
}
onMounted(() => {
startScan()
})
onUnmounted(() => {
// 清理
})
</script>
<template>
<div class="scan-code-page">
<van-nav-bar
title="扫一扫"
left-text="返回"
left-arrow
@click-left="goBack"
fixed
placeholder
/>
<div class="scan-content">
<div class="scan-area">
<div class="scan-frame">
<div v-if="scanning" class="scan-line"></div>
<p class="scan-hint">将二维码/条形码放入框内即可自动扫描</p>
</div>
</div>
<div v-if="scanResult" class="scan-result">
<p class="result-label">扫描结果</p>
<p class="result-text">{{ scanResult }}</p>
</div>
<div class="rescan-wrapper">
<van-button
v-if="!scanning"
round
block
type="primary"
@click="startScan"
>
重新扫描
</van-button>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.scan-code-page {
min-height: 100vh;
background: #000;
}
.scan-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 16px;
min-height: calc(100vh - 46px);
}
.scan-area {
width: 100%;
max-width: 300px;
margin-top: 40px;
}
.scan-frame {
position: relative;
width: 100%;
aspect-ratio: 1;
border: 2px solid var(--color-primary, #1989fa);
border-radius: 8px;
display: flex;
align-items: flex-end;
justify-content: center;
overflow: hidden;
}
.scan-line {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: var(--color-primary, #1989fa);
animation: scan 2s linear infinite;
}
@keyframes scan {
0% { top: 0; }
100% { top: 100%; }
}
.scan-hint {
color: rgba(255, 255, 255, 0.6);
font-size: 13px;
padding: 12px;
text-align: center;
}
.scan-result {
margin-top: 24px;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
width: 100%;
max-width: 300px;
.result-label {
color: rgba(255, 255, 255, 0.5);
font-size: 12px;
margin-bottom: 4px;
}
.result-text {
color: #fff;
font-size: 14px;
word-break: break-all;
}
}
.rescan-wrapper {
margin-top: 24px;
width: 100%;
max-width: 300px;
}
</style>

221
src/views/notice/index.vue Normal file
View File

@@ -0,0 +1,221 @@
<script setup lang="ts">
/**
* 消息通知页面
*
* 使用 Vant PullRefresh + List 实现下拉刷新和无限滚动,
* Tabs 切换"未读消息"和"全部消息"。
*/
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { showToast } from 'vant'
const router = useRouter()
/** 当前激活的 Tab */
const activeName = ref('未读消息')
/** 列表数据 */
const list = ref<any[]>([])
/** 是否加载完成 */
const finished = ref(false)
/** 是否正在加载 */
const isLoading = ref(false)
/** 分页参数 */
const pageParams = reactive({
pageNum: 1,
pageSize: 10,
})
/**
* 生成模拟消息数据
*/
function generateMockData(pageNum: number, pageSize: number) {
const start = (pageNum - 1) * pageSize
const total = 25
const items: any[] = []
const end = Math.min(start + pageSize, total)
for (let i = start; i < end; i++) {
items.push({
id: i + 1,
title: `通知消息标题 ${i + 1}`,
message: `这是第 ${i + 1} 条消息的详细内容,用于展示通知列表的效果。`,
createTime: `2025-06-${String(10 + Math.floor(i / 3)).padStart(2, '0')} 14:30:00`,
})
}
return { items, isLastPage: end >= total }
}
/**
* 获取列表数据
*/
function getList() {
isLoading.value = true
pageParams.pageNum = 1
setTimeout(() => {
const { items, isLastPage } = generateMockData(pageParams.pageNum, pageParams.pageSize)
list.value = items
finished.value = isLastPage
isLoading.value = false
}, 500)
}
/**
* Tab 切换
*/
function onChange(name: string | number) {
activeName.value = String(name)
finished.value = false
getList()
}
/**
* 无限滚动加载更多
*/
function onLoad() {
if (finished.value) return
isLoading.value = true
pageParams.pageNum += 1
setTimeout(() => {
const { items, isLastPage } = generateMockData(pageParams.pageNum, pageParams.pageSize)
list.value = list.value.concat(items)
finished.value = isLastPage
isLoading.value = false
}, 500)
}
/**
* 下拉刷新
*/
function onRefresh() {
finished.value = false
pageParams.pageNum = 1
setTimeout(() => {
const { items, isLastPage } = generateMockData(pageParams.pageNum, pageParams.pageSize)
list.value = items
finished.value = isLastPage
showToast('刷新成功')
}, 800)
}
/**
* 点击消息项
*/
function toDetail(item: any) {
showToast(`查看: ${item.title}`)
}
/** 初始加载 */
getList()
</script>
<template>
<div class="notice-page">
<!-- 顶部导航栏 -->
<van-nav-bar title="消息通知" left-text="返回" left-arrow fixed placeholder @click-left="router.back()" />
<!-- Tab 切换 -->
<van-tabs v-model:active="activeName" @change="onChange" sticky>
<van-tab title="未读消息" name="未读消息" />
<van-tab title="全部消息" name="全部消息" />
</van-tabs>
<!-- 下拉刷新 + 列表 -->
<van-pull-refresh v-model="isLoading" @refresh="onRefresh">
<van-list
v-model:loading="isLoading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<div
class="notice-item"
v-for="item in list"
:key="item.id"
@click="toDetail(item)"
>
<div class="notice-item-header">
<span class="notice-item-title">{{ item.title }}</span>
</div>
<van-divider />
<div class="notice-item-body">
<span>{{ item.message }}</span>
</div>
<van-divider />
<div class="notice-item-footer">
<span class="notice-item-time-label">通知时间</span>
<span class="notice-item-time">{{ item.createTime }}</span>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
</template>
<style lang="scss" scoped>
.notice-page {
min-height: 100vh;
background: #f4f7f8;
:deep(.van-nav-bar) {
background: var(--color-primary);
--van-nav-bar-title-text-color: #fff;
--van-nav-bar-text-color: #fff;
--van-nav-bar-icon-color: #fff;
}
}
.notice-item {
padding: 10px 12px;
margin: 12px 8px;
background: #fff;
border-radius: 10px;
cursor: pointer;
&:active {
background: #f5f5f5;
}
&-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
&-title {
flex: 1;
font-size: 17px;
color: #333438;
font-weight: 600;
}
&-body {
padding: 0 4px;
font-size: 14px;
color: #535c66;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
&-footer {
display: flex;
justify-content: space-between;
padding: 0 4px;
}
&-time-label {
font-size: 12px;
color: #999;
}
&-time {
font-size: 12px;
color: #666;
}
}
</style>