[uni-app]小兔鲜-05登录+个人中心

news2024/10/1 15:52:15

登录

微信授权登录: 通过wx.login()获取code登录凭证, 通过特定类型按钮获取用户手机号, 实现授权登录

import type { LoginResult } from '@/types/member'
import { http } from '@/utils/http'

type LoginParams = {
    code: string
    encryptedData: string
    iv: string
}

// 微信登录
export const postLoginWxMin = (data: LoginParams) => {
    return http<LoginResult>({
        method: 'POST',
        url: '/login/wxMin',
        data,
    })
}

// 模拟微信登录
export const postLoginWxMinSimpleAPI = (phoneNumber: string) => {
    return http<LoginResult>({
        method: 'POST',
        url: '/login/wxMin/simple',
        data: {
            phoneNumber,
        },
    })
}
// 用户信息类型文件

/** 小程序登录 登录用户信息 */
export type LoginResult = {
   /** 用户ID */
    id: number
    /** 头像  */
    avatar: string
    /** 账户名  */
    account: string
    /** 昵称 */
    nickname?: string
    /** 手机号 */
    mobile: string
    /** 登录凭证 */
    token: string
}
<script setup lang="ts">
import { postLoginWxMin } from '@/services/login'
import { onLoad } from '@dcloudio/uni-app'

// 获取code
let code = ''
onLoad(async () => {
  const res = await wx.login()
  code = res.code
})

// 微信登录
const onGetPhoneNumber: UniHelper.ButtonOnGetphonenumber = async (ev) => {
  const encryptedData = ev.detail!.encryptedData!
  const iv = ev.detail!.iv!
  const res = await postLoginWxMin({
    encryptedData,
    iv,
    code,
  })
  console.log(res)
}

</script>

<template>
  <view class="viewport">
    <view class="logo">
      <image
        src="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/logo_icon.png"
      ></image>
    </view>
    <view class="login">
      <!-- 小程序端授权登录 -->
      <button class="button phone" open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber">
        <text class="icon icon-phone"></text>
        手机号快捷登录
      </button>
       
      <view class="tips">登录/注册即视为你同意《服务条款》和《小兔鲜儿隐私协议》</view>
    </view>
  </view>
</template>
  1. 使用企业小程序appid, 并且把微信号添加到开发者列表中, 才能获取到用户手机号
  2. ! 非空断言排除的是前面的空值, 有需要的话可以使用多次

模拟登录: 获取手机号功能针对非个人开发者, 且认证完成的小程序开发

<sript setup lang="ts">
import { useMemberStore } from '@/stores'
import { postLoginWxMinSimpleAPI } from '@/services/login'

// 模拟微信登录
const onGetPhoneNumberSimple = async () => {
  const res = await postLoginWxMinSimpleAPI('15553266208')
  console.log(res)
}
</script>

<template>
  <view class="viewport">
    <view class="logo">
      <image
        src="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/logo_icon.png"
      ></image>
    </view>
    <view class="login">
      <!-- 小程序端授权登录 -->
      <button class="button phone">
        <text class="icon icon-phone"></text>
        手机号快捷登录
      </button>
       
      <view class="extra">
        <view class="caption">
          <text>其他登录方式</text>
        </view>
        <view class="options">
          <!-- 通用模拟登录 -->
          <button @tap="onGetPhoneNumberSimple">
            <text class="icon icon-phone">模拟快捷登录</text>
          </button>
        </view>
      </view>
      <view class="tips">登录/注册即视为你同意《服务条款》和《小兔鲜儿隐私协议》</view>
    </view>
  </view>
</template>

保存登录信息

<sript setup lang="ts">
import { useMemberStore } from '@/stores'
import { postLoginWxMinSimpleAPI } from '@/services/login'

// 模拟微信登录
const onGetPhoneNumberSimple = async () => {
  const res = await postLoginWxMinSimpleAPI('15553266208')
  // 保存用户信息
  const memberStore = useMemberStore()
  memberStore.setProfile(res.result)
  // 提示
  uni.showToast({
    icon: 'none',
    title: '登录成功',
  })
  // 跳转
  setTimeout(() => {
    uni.navigateBack()
  }, 500)
}
</script>
import { defineStore } from 'pinia'
import { ref } from 'vue'

// 定义 Store
export const useMemberStore = defineStore(
  'member',
  () => {
    // 会员信息
    const profile = ref<LoginResult>()

    // 保存会员信息,登录时使用
    const setProfile = (val: LoginResult) => {
      profile.value = val
    }

    // 清理会员信息,退出时使用
    const clearProfile = () => {
      profile.value = undefined
    }

    // 记得 return
    return {
      profile,
      setProfile,
      clearProfile,
    }
  },
  // TODO: 持久化
    persist: {
      storage: {
        getItem(key) {
          return uni.getStorageSync(key)
        },
        setItem(key, value) {
          uni.setStorageSync(key, value)
        },
      },
    },
  },
)

我的页面

创建我的页面, 渲染用户信息

"pages": [
		{
			"path": "pages/my/my",
			"style": {
				"navigationStyle": "custom", // 隐藏默认导航
				"navigationBarTextStyle": "white",
				"navigationBarTitleText": "我的"
			}
		}
]
<script setup lang="ts">
import { useMemberStore } from '@/stores'
import { ref } from 'vue'
import { useGuessList } from '@/composables/index'

// 获取屏幕边界到安全区域距离
const { safeAreaInsets } = uni.getSystemInfoSync()
// 订单选项
const orderTypes = [
  { type: 1, text: '待付款', icon: 'icon-currency' },
  { type: 2, text: '待发货', icon: 'icon-gift' },
  { type: 3, text: '待收货', icon: 'icon-check' },
  { type: 4, text: '待评价', icon: 'icon-comment' },
]
// 获取会员信息
const memberStore = useMemberStore()
</script>

<template>
  <scroll-view class="viewport" scroll-y enable-back-to-top @scrolltolower="onScorlltolower">
    <!-- 个人资料 -->
    <view class="profile" :style="{ paddingTop: safeAreaInsets!.top + 'px' }">
      <!-- 情况1:已登录 -->
      <view class="overview" v-if="memberStore.profile">
        <navigator url="/pagesMember/profile/profile" hover-class="none">
          <image class="avatar" mode="aspectFill" :src="memberStore.profile.avatar"></image>
        </navigator>
        <view class="meta">
          <view class="nickname">
            {{ memberStore.profile.nickname || memberStore.profile.account }}
          </view>
          <navigator class="extra" url="/pagesMember/profile/profile" hover-class="none">
            <text class="update">更新头像昵称</text>
          </navigator>
        </view>
      </view>
      <!-- 情况2:未登录 -->
      <view class="overview" v-else>
        <navigator url="/pages/login/login" hover-class="none">
          <image
            class="avatar gray"
            mode="aspectFill"
            src="http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-04-06/db628d42-88a7-46e7-abb8-659448c33081.png"
          ></image>
        </navigator>
        <view class="meta">
          <navigator url="/pages/login/login" hover-class="none" class="nickname">
            未登录
          </navigator>
          <view class="extra">
            <text class="tips">点击登录账号</text>
          </view>
        </view>
      </view>
      <navigator class="settings" url="/pagesMember/settings/settings" hover-class="none">
        设置
      </navigator>
    </view>
    <!-- 我的订单 -->
    <view class="orders">
      <view class="title">
        我的订单
        <navigator class="navigator" url="/pagesOrder/list/list?type=0" hover-class="none">
          查看全部订单<text class="icon-right"></text>
        </navigator>
      </view>
      <view class="section">
        <!-- 订单 -->
        <navigator
          v-for="item in orderTypes"
          :key="item.type"
          :class="item.icon"
          :url="`/pagesOrder/list/list?type=${item.type}`"
          class="navigator"
          hover-class="none"
        >
          {{ item.text }}
        </navigator>
        <!-- 客服 -->
        <button class="contact icon-handset" open-type="contact">售后</button>
      </view>
    </view>
  </scroll-view>
</template>

猜你喜欢分页加载

<script setup lang="ts">
import { useGuessList } from '@/composables/index'

// 调用组合式函数,实现猜你喜欢分页加载
const { guessRef, onScorlltolower } = useGuessList()
</script>

<template>
  <scroll-view class="viewport" scroll-y enable-back-to-top @scrolltolower="onScorlltolower">
    <!-- 个人资料 -->
    <view class="profile" :style="{ paddingTop: safeAreaInsets!.top + 'px' }">
       ... ...
    </view>
    <!-- 猜你喜欢 -->
    <view class="guess">
      <XtxGuess ref="guessRef" />
    </view>
  </scroll-view>
</template>
import type { XtxGuessInstance } from '@/types/component'
import { ref } from 'vue'

/**
 * 猜你喜欢组合式函数
 */
export const useGuessList = () => {
    // 猜你喜欢的组件实例
    const guessRef = ref<XtxGuessInstance>()

    // 滚动触底事件
    const onScorlltolower = () => {
        guessRef.value.getMore()
    }

    // 返回ref 和 事件处理函数
    return {
        guessRef,
        onScorlltolower,
    }
}
  1. 组合式函数是vue3组合式API支持的语法, 进一步封装vue组件中的函数
  2. 组合式函数一般以use开头进行命名

效果展示

设置页面

新建分包页面, 配置分包预下载

分包: 将小程序的核心页面和次要页面进行分割,打包成多个小程序包,减少小程序的加载时间 ,提高用户体验

预下载: 在进入小程序的指定页面时,由框架自动提前下载分包资源. 提升进入分包页面的启动速度

<template>
  <view class="viewport">
    <!-- 列表1 -->
    <view class="list" v-if="true">
      <navigator url="/pagesMember/address/address" hover-class="none" class="item arrow">
        我的收货地址
      </navigator>
    </view>
    <!-- 列表2 -->
    <view class="list">
      <button hover-class="none" class="item arrow" open-type="openSetting">授权管理</button>
      <button hover-class="none" class="item arrow" open-type="feedback">问题反馈</button>
      <button hover-class="none" class="item arrow" open-type="contact">联系我们</button>
    </view>
    <!-- 列表3 -->
    <view class="list">
      <navigator hover-class="none" class="item arrow" url=" ">关于小兔鲜儿</navigator>
    </view>
    <!-- 操作按钮 -->
    <view class="action">
      <view class="button">退出登录</view>
    </view>
  </view>
</template>
  1. 存放: 主包页面放在pages中管理, 分包单独创建文件夹管理
  2. 新建: 编辑器都有快捷新建分包页面的功能

配置分包预下载规则

{
  // 主包页面配置
	"pages": [
     ...
	],
	// 分包页面配置
	"subPackages": [
		{
			// 子包的根目录
			"root": "pagesMember",
			// 页面路径和窗口表现
			"pages": [
				{
					"path": "settings/settings",
					"style": {
						"navigationBarTitleText": "设置"
					}
				},
			]
		},
	],
	// 分包预下载规则
	"preloadRule": {
		// 进入页面时预下载的分包
		"pages/my/my": {
			// 网络
			"network": "all",
			// 预下载的分包
			"packages": [
				"pagesMember"
			]
		}
	}
}

退出登录

<script setup lang="ts">
import { useMemberStore } from '@/stores'
// 退出登录
const memberStore = useMemberStore()
const onLogout = () => {
  // 确认框
  uni.showModal({
    content: '是否退出登录?',
    success: (res) => {
      if (res.confirm) {
        // 清理用户信息
        memberStore.clearProfile()
        // 页面跳转
        uni.navigateBack()
      }
    },
  })
}
</script>

<template>
  <view class="viewport">
    <!-- 列表1 -->
    <view class="list" v-if="memberStore.profile">
      <navigator url="/pagesMember/address/address" hover-class="none" class="item arrow">
        我的收货地址
      </navigator>
    </view>
    <!-- 列表2 -->
    <view class="list">
      <button hover-class="none" class="item arrow" open-type="openSetting">授权管理</button>
      <button hover-class="none" class="item arrow" open-type="feedback">问题反馈</button>
      <button hover-class="none" class="item arrow" open-type="contact">联系我们</button>
    </view>
    <!-- 列表3 -->
    <view class="list">
      <navigator hover-class="none" class="item arrow" url=" ">关于小兔鲜儿</navigator>
    </view>
    <!-- 操作按钮 -->
    <view class="action">
      <view class="button" @tap="onLogout" v-if="memberStore.profile">退出登录</view>
    </view>
  </view>
</template>

实现效果

个人信息

准备分包页面, 初始化渲染

<script setup lang="ts">
import { getMemberProfileAPI } from '@/services/profile'
import type { ProfileDetail } from '@/types/member'
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'

// 获取屏幕边界到安全区域距离
const { safeAreaInsets } = uni.getSystemInfoSync()

// 获取个人信息
const profile = ref({} as ProfileDetail)
const getMemberProfileData = async () => {
  const res = await getMemberProfileAPI()
  profile.value = res.result
}

onLoad(() => {
  getMemberProfileData()
})
</script>

<template>
  <view class="viewport">
    <!-- 导航栏 -->
    <view class="navbar" :style="{ paddingTop: safeAreaInsets?.top + 'px' }">
      <navigator open-type="navigateBack" class="back icon-left" hover-class="none"></navigator>
      <view class="title">个人信息</view>
    </view>
    <!-- 头像 -->
    <view class="avatar">
      <view class="avatar-content" @tap="onAvatarChange">
        <image class="image" :src="profile?.avatar" mode="aspectFill" />
        <text class="text">点击修改头像</text>
      </view>
    </view>
    <!-- 表单 -->
    <view class="form">
      <!-- 表单内容 -->
      <view class="form-content">
        <view class="form-item">
          <text class="label">账号</text>
          <text class="account">{{ profile?.account }}</text>
        </view>
        <view class="form-item">
          <text class="label">昵称</text>
          <input class="input" type="text" placeholder="请填写昵称" v-model="profile!.nickname" />
        </view>
        <view class="form-item">
          <text class="label">性别</text>
          <radio-group @change="onGenderChange">
            <label class="radio">
              <radio value="男" color="#27ba9b" :checked="profile?.gender === '男'" />
              男
            </label>
            <label class="radio">
              <radio value="女" color="#27ba9b" :checked="profile?.gender === '女'" />
              女
            </label>
          </radio-group>
        </view>
        <view class="form-item">
          <text class="label">生日</text>
          <picker
            class="picker"
            mode="date"
            start="1900-01-01"
            :end="new Date()"
            @change="onBirthdayChange"
            :value="profile?.birthday"
          >
            <view v-if="profile?.birthday">{{ profile?.birthday }}</view>
            <view class="placeholder" v-else>请选择日期</view>
          </picker>
        </view>
        <view class="form-item">
          <text class="label">城市</text>
          <picker
            class="picker"
            mode="region"
            :value="profile?.fullLocation?.split(' ')"
            @change="onFullLocationChange"
          >
            <view v-if="profile?.fullLocation">{{ profile?.fullLocation }}</view>
            <view class="placeholder" v-else>请选择城市</view>
          </picker>
        </view>
        <view class="form-item">
          <text class="label">职业</text>
          <input class="input" type="text" placeholder="请填写职业" v-model="profile.profession" />
        </view>
      </view>
      <!-- 提交按钮 -->
      <button class="form-button" @tap="onSubmit">保 存</button>
    </view>
  </view>
</template>
import type { ProfileDetail, ProfileParams } from '@/types/member'
import { http } from '@/utils/http'

/**
 * 获取个人信息
 */
export const getMemberProfileAPI = () => {
    return http<ProfileDetail>({
        method: 'GET',
        url: '/member/profile',
    })
}
// 用户信息类型文件

/** 封装通用信息 */
type BaseProfile = {
    /** 用户ID */
    id: number
    /** 头像  */
    avatar: string
    /** 账户名  */
    account: string
    /** 昵称 */
    nickname?: string
}

/** 小程序登录 登录用户信息 */
export type LoginResult = BaseProfile & {
    /** 手机号 */
    mobile: string
    /** 登录凭证 */
    token: string
}

/** 个人信息 用户详情信息 */
export type ProfileDetail = BaseProfile & {
    /** 性别 */
    gender?: Gender
    /** 生日 */
    birthday?: string
    /** 省市区 */
    fullLocation?: string
    /** 职业 */
    profession?: string
}

/** 性别 */
export type Gender = '女' | '男'
  1. 使用联合类型 & 进一步封装类型声明

头像上传

<script setup lang="ts">
import { getMemberProfileAPI, putMemberProfileAPI } from '@/services/profile'
import type { ProfileDetail, Gender } from '@/types/member'
import { ref } from 'vue'
import { useMemberStore } from '@/stores'

// 点击头像
const memberStore = useMemberStore()
const onAvatarChange = () => {
  // 调用拍照/选择图片
  // #ifdef H5 || APP-PLUS
  // 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替
  uni.chooseImage({
    count: 1,
    success: (res) => {
      // 文件路径
      const tempFilePaths = res.tempFilePaths
      // 上传
      uploadFile(tempFilePaths[0])
    },
  })
  // #endif

  // #ifdef MP-WEIXIN
  // uni.chooseMedia 仅支持微信小程序端
  uni.chooseMedia({
    // 文件个数
    count: 1,
    // 文件类型
    mediaType: ['image'],
    success: (res) => {
      // 本地路径
      const { tempFilePath } = res.tempFiles[0]
      // 上传
      uploadFile(tempFilePath)
    },
  })
  // #endif
}

// 文件上传-兼容小程序端、H5端、App端
const uploadFile = (file: string) => {
  // 文件上传
  uni.uploadFile({
    url: '/member/profile/avatar',
    name: 'file',
    filePath: file,
    success: (res) => {
      if (res.statusCode === 200) {
        const avatar = JSON.parse(res.data).result.avatar
        // 个人信息页数据更新
        profile.value!.avatar = avatar
        // Store头像更新
        memberStore.profile!.avatar = avatar
        uni.showToast({ icon: 'success', title: '更新成功' })
      } else {
        uni.showToast({ icon: 'error', title: '出现错误' })
      }
    },
  })
}
</script>

<template>
  <view class="viewport">
    ...
    <!-- 头像 -->
    <view class="avatar">
      <view class="avatar-content" @tap="onAvatarChange">
        <image class="image" :src="profile?.avatar" mode="aspectFill" />
        <text class="text">点击修改头像</text>
      </view>
    </view>
  ...
    </view>
  </view>
</template>

修改用户昵称

import type { ProfileDetail, ProfileParams } from '@/types/member'
import { http } from '@/utils/http'

/**
 * 修改个人信息
 */
export const putMemberProfileAPI = (data: ProfileParams) => {
    return http<ProfileDetail>({
        method: 'PUT',
        url: '/member/profile',
        data,
    })
}
// 用户信息类型文件

/** 封装通用信息 */
type BaseProfile = {
    /** 用户ID */
    id: number
    /** 头像  */
    avatar: string
    /** 账户名  */
    account: string
    /** 昵称 */
    nickname?: string
}

/** 个人信息 用户详情信息 */
export type ProfileDetail = BaseProfile & {
    /** 性别 */
    gender?: Gender
    /** 生日 */
    birthday?: string
    /** 省市区 */
    fullLocation?: string
    /** 职业 */
    profession?: string
}

/** 性别 */
export type Gender = '女' | '男'

/** 个人信息 修改请求体参数 */
export type ProfileParams = Pick<
    ProfileDetail,
    'nickname' | 'gender' | 'birthday' | 'profession'
> & {
    /** 省份编码 */
    provinceCode?: string
    /** 城市编码 */
    cityCode?: string
    /** 区/县编码 */
    countyCode?: string
}
  1. Pick<目标类型, 选取的属性>泛型工具方法是TS提供的, 用于提取已有类型中的参数, 组合成新的类型
<script setup lang="ts">
import { putMemberProfileAPI } from '@/services/profile'
import type { ProfileDetail } from '@/types/member'
import { ref } from 'vue'

// 获取个人信息
const profile = ref({} as ProfileDetail)

// 表单提交
const onSubmit = async () => {
  const res = await putMemberProfileAPI({
    nickname: profile.value.nickname,
  })
  // 更新stroe的昵称
  memberStore.profile!.nickname = res.result.nickname
  uni.showToast({
    icon: 'success',
    titlelist: '修改成功',
  })
  // 返回上一页
  setTimeout(() => {
    uni.navigateBack()
  }, 400)
}
</script>

<template>
  <view class="viewport">
    <!-- 表单 -->
    <view class="form">
      <!-- 表单内容 -->
      <view class="form-content">
        <view class="form-item">
          <text class="label">账号</text>
          <text class="account">{{ profile?.account }}</text>
        </view>
        <view class="form-item">
          <text class="label">昵称</text>
          <input class="input" type="text" placeholder="请填写昵称" v-model="profile!.nickname" />
        </view>
      </view>
      <!-- 提交按钮 -->
      <button class="form-button" @tap="onSubmit">保 存</button>
    </view>
  </view>
</template>
  1. 在TS中对数据的类型限制非常严格, 一个对象中的属性是不能随便添加的, 要和类型声明保持一致
  2. 仅做展示的对象数据, 一般只限制类型, 不给对象的定义初始值, 这样比较方便
  3. 对于需要双向绑定的数据, 就要使用 as类型断言, 告诉TS这个对象以后是什么样子, 这个对象才能顺利的进行数据双向绑定, 也就相当于给对象进行了初始值的定义

修改性别信息

<script setup lang="ts">
import { putMemberProfileAPI } from '@/services/profile'
import type { ProfileDetail, Gender } from '@/types/member'
import { ref } from 'vue'

// 获取个人信息
const profile = ref({} as ProfileDetail)

// 男女选项变化
const onGenderChange: UniHelper.RadioGroupOnChange = (ev) => {
  profile.value.gender = ev.detail.value as Gender
}

// 表单提交
const onSubmit = async () => {
  const res = await putMemberProfileAPI({
    gender: profile.value.gender,
  })
  // 更新stroe的昵称
  memberStore.profile!.nickname = res.result.nickname
  uni.showToast({
    icon: 'success',
    titlelist: '修改成功',
  })
  setTimeout(() => {
    uni.navigateBack()
  }, 400)
}
</script>

<template>
  <view class="viewport">
  ... ...
    <!-- 表单 -->
    <view class="form">
      <!-- 表单内容 -->
      <view class="form-content">
        <view class="form-item">
          <text class="label">性别</text>
          <radio-group @change="onGenderChange">
            <label class="radio">
              <radio value="男" color="#27ba9b" :checked="profile?.gender === '男'" />
              男
            </label>
            <label class="radio">
              <radio value="女" color="#27ba9b" :checked="profile?.gender === '女'" />
              女
            </label>
          </radio-group>
        </view>
  
      </view>
      <!-- 提交按钮 -->
      <button class="form-button" @tap="onSubmit">保 存</button>
    </view>
  </view>
</template>
  1. ev.detail.value 拿到的是string类型数据
  2. profile.value.gender 限制的数据类型是 gender
  3. 如果直接赋值TS会爆红, 所以要通过类型断言告诉TS, ev拿到的就是gender类型

修改生日信息

<script setup lang="ts">
import { putMemberProfileAPI } from '@/services/profile'
import type { ProfileDetail, Gender } from '@/types/member'
import { ref } from 'vue'

// 获取个人信息
const profile = ref({} as ProfileDetail)

// 修改生日
const onBirthdayChange: UniHelper.DatePickerOnChange = (ev) => {
  profile.value.birthday = ev.detail.value
}

// 表单提交
const onSubmit = async () => {
  const res = await putMemberProfileAPI({
    birthday: profile.value.birthday,
  })
  // 更新stroe的昵称
  memberStore.profile!.nickname = res.result.nickname
  uni.showToast({
    icon: 'success',
    titlelist: '修改成功',
  })
  setTimeout(() => {
    uni.navigateBack()
  }, 400)
}
</script>

<template>
  <view class="viewport">
    ... ...
    <!-- 表单 -->
    <view class="form">
      <!-- 表单内容 -->
      <view class="form-content">
        <view class="form-item">
          <text class="label">生日</text>
          <picker
            class="picker"
            mode="date"
            start="1900-01-01"
            :end="new Date()"
            @change="onBirthdayChange"
            :value="profile?.birthday"
          >
            <view v-if="profile?.birthday">{{ profile?.birthday }}</view>
            <view class="placeholder" v-else>请选择日期</view>
          </picker>
        </view>
      </view>
      <!-- 提交按钮 -->
      <button class="form-button" @tap="onSubmit">保 存</button>
    </view>
  </view>
</template>

修改用户所在城市

<script setup lang="ts">
import { putMemberProfileAPI } from '@/services/profile'
import type { ProfileDetail } from '@/types/member'
import { ref } from 'vue'

// 获取个人信息
const profile = ref({} as ProfileDetail)

// 修改城市
let fullLocationCode: [string, string, string] = ['', '', '']
const onFullLocationChange: UniHelper.RegionPickerOnChange = (ev) => {
  // 修改前端界面
  profile.value.fullLocation = ev.detail.value.join(' ')

  // 提交后端更新
  fullLocationCode = ev.detail.code!
}

// 表单提交
const onSubmit = async () => {
  const res = await putMemberProfileAPI({
    provinceCode: fullLocationCode[0],
    cityCode: fullLocationCode[1],
    countyCode: fullLocationCode[2],
  })
  // 更新stroe的昵称
  memberStore.profile!.nickname = res.result.nickname
  uni.showToast({
    icon: 'success',
    titlelist: '修改成功',
  })
  setTimeout(() => {
    uni.navigateBack()
  }, 400)
}
</script>

<template>
  <view class="viewport">
    ... 
    <!-- 表单 -->
    <view class="form">
      <!-- 表单内容 -->
      <view class="form-content">
        ...
        <view class="form-item">
          <text class="label">城市</text>
          <picker
            class="picker"
            mode="region"
            :value="profile?.fullLocation?.split(' ')"
            @change="onFullLocationChange"
          >
            <view v-if="profile?.fullLocation">{{ profile?.fullLocation }}</view>
            <view class="placeholder" v-else>请选择城市</view>
          </picker>
        </view>
      </view>
      <!-- 提交按钮 -->
      <button class="form-button" @tap="onSubmit">保 存</button>
    </view>
  </view>
</template>
  1. 在TS中, 长度和类型都已经确定的数组称为元组

实现效果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2182654.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

每日一题|983. 最低票价|动态规划、记忆化递归

本题求解最小值&#xff0c;思路是动态规划&#xff0c;但是遇到的问题是&#xff1a;动态规划更新的顺序和步长&#xff0c;以及可能存在的递归溢出问题。 1、确定dp数组含义 dp[i]表示第i天到最后一天&#xff08;可能不在需要出行的天数里&#xff09;&#xff0c;需要花费…

图神经网络DGL库之消息传递

图神经网络DGL库之消息传递 1 消息传递1.1 图解1.2 语法格式1.2.1 message函数1.2.2 reduce函数1.2.3 update函数1.2.4 apply_nodes函数1.2.5 apply_edges函数 2 具体例子2.1 建图2.2 消息传递2.2.1 函数构造2.2.2 边更新2.2.3 节点更新2.2.4 消息聚合1 未使用更新函数2 使用更…

M3u8视频由手机拷贝到电脑之后,通过potplayer播放报错找不到文件地址怎么解决?

该文章前面三节主要介绍M3u8视频是什么&#xff0c;视频播放错误(找不到地址)的解决方法在后面 M3U8是一种多媒体播放列表文件格式&#xff0c;主要用于流媒体播放。 一、文件格式特点 1. 文本文件&#xff1a;M3U8是一个采用 UTF-8 编码的文本文件&#xff0c;这意味着它可…

Shell入门基础学习笔记

目录 第1章 Shell概述 第2章 Shell解析器 第3章 Shell脚本入门 第4章 Shell中的变量 4.1 系统变量 4.2 自定义变量 4.3 特殊变量&#xff1a;$n 4.4 特殊变量&#xff1a;$# 4.5 特殊变量&#xff1a;$*、$ 4.6 特殊变量&#xff1a;$&#xff1f; 第5章 运算符 …

玩机进阶教程----MTK芯片机型修改串码IMEI 修改MEID 修复基带步骤详细演示 总结

在前面的博文中有对MTK芯片机型修改参数步骤做过解析。但其中有些步骤友友不太了解。在以前MTK芯片 3G 4G的机型中有使用老版本修改工具SN_Writer_Tool来修改,但对于新版本mtk芯片机型兼容性不是太好。而且局限于必须有基带BP AP文件。今天针对新工具Modem META 修改 做个补充…

国外问卷调查匠哥已经不带人了,但是还可以交流

国外问卷调查匠哥已经不带人了&#xff0c;但是还可以来和匠哥交流&#xff0c; 为啥不带人了呢&#xff1f; 从今年年初开始&#xff0c;匠哥在带学员的过程中发现&#xff1a; 跟往年同样的收费&#xff0c;同样的教学&#xff0c;甚至我付出的时间精力比以前还多&#xff…

Java | Leetcode Java题解之第447题回旋镖的数量

题目&#xff1a; 题解&#xff1a; class Solution {public int numberOfBoomerangs(int[][] points) {int ans 0;for (int[] p : points) {Map<Integer, Integer> cnt new HashMap<Integer, Integer>();for (int[] q : points) {int dis (p[0] - q[0]) * (p[…

如何构建一个生产级的AI平台(1)?

本文概述了生成式 AI 平台的常见组件、它们的作用以及它们的实现方式。 本文重点介绍部署 AI 应用程序的整体架构。 它讨论了需要哪些组件以及构建这些组件时的注意事项。 它不是关于如何构建 AI 应用程序。 这就是整体架构的样子。 这是一个相当复杂的系统。 这篇文章将从最…

基于Leaflet和天地图的细直箭头和突击方向标绘实战

目录 前言 一、细直箭头和突击方向的类设计 1、总体类图 2、对象区别 二、标绘绘制的具体实现 1、绘制时序图 2、相关点的具体绘制 3、最终的成果 三、总结 前言 今天是10月1日国庆节&#xff0c;迎来我们伟大祖国75周年的华诞。有国才有家&#xff0c;在这里首先祝我们…

【vs code(cursor) ssh连不上服务器(2)】但是 Terminal 可以连上,问题解决 ✅

【vs code(cursor) ssh连不上服务器】但是 Terminal 可以连上&#xff0c;问题解决 ✅ 对于类似的问题&#xff0c;之前的解决方法是清洗配置文件再重新连接。当重新连接不起作用时&#xff0c;可以再试下本文的方法。 问题描述&#xff1a;SSH 超时错误 vs code 连不上 ssh…

解决方案:机器学习中,回归及分类常用的模型评估指标有哪些

文章目录 一、现象二、解决方案回归任务的评价指标&#xff1a;均方误差 (MSE):平均绝对误差 (MAE): 分类任务的评价指标&#xff1a;准确率 (Accuracy):混淆矩阵 (Confusion Matrix):精确度 (Precision):召回率 (Recall):F1分数 (F1 Score):ROC曲线 (Receiver Operating Chara…

Qt的互斥量用法

目的 互斥量的概念 互斥量是一个可以处于两态之一的变量:解锁和加锁。这样&#xff0c;只需要一个二进制位表示它&#xff0c;不过实际上&#xff0c;常常使用一个整型量&#xff0c;0表示解锁&#xff0c;而其他所有的值则表示加锁。互斥量使用两个过程。当一个线程(或进程)…

ubuntu 24.04如何分配内存

24版与之前有一点不同&#xff0c;这里记录一下我的经历&#xff0c;希望有帮助 1.进入ubuntu直接试用&#xff0c;没有之前的安装向导&#xff08;如图&#xff09;&#xff0c;在屏幕的左上角会找到安装Ubuntu 2.分配内存 24的手动分配内存&#xff0c;不需要分配系统内存&…

IOT平台颜值天花板?延凡科技物联网平台让人惊叹不已

IOT平台颜值天花板&#xff1f;延凡科技物联网平台让人惊叹不已 在物联网的时代&#xff0c;AIOT平台凭借智能化的管理和决策能力&#xff0c;为多个行业带来了巨大的提升。本文将为大家介绍AIOT物联网平台的核心功能、应用场景以及它是如何改变我们的生活的。 平台简介 AIOT物…

二维环境下的TDOA测距定位的MATLAB代码,带中文注释

TDOA测距定位程序介绍 概述 本MATLAB程序实现了基于时间差到达&#xff08;TDOA&#xff09;技术的二维测距定位&#xff0c;能够处理4个或任意数量&#xff08;大于3个&#xff09;的锚节点。在无线定位和导航系统中&#xff0c;TDOA是一种常用的定位方法&#xff0c;通过测量…

一款免费开源的接口测试工具——ApiFox详细教程

前言 APIfox是一种功能强大的接口测试工具&#xff0c;它可以帮助用户轻松地进行REST API的自动化测试和文档编写。本文将从以下几个方面介绍APIfox的基本使用方法、特点和优势。 一、什么是APIfox&#xff1f; APIfox是一款基于Web的REST API测试工具&#xff0c;通过创建测…

论文笔记:LAFF 文本到视频检索的新基准

整理了ECCV2022 Lightweight Attentional Feature Fusion: A New Baseline for Text-to-Video Retrieval 论文的阅读笔记 背景模型问题定义LAFF(Lightweight Attention Feature Fusion)LAFF Block 实验消融实验可视化对比试验 这篇文章提出了一种新颖灵活的特征融合方式&#x…

初步认识产品经理

产品经理 思考问题的维度 1️⃣为什么要抓住核心用户&#xff1f; 所有和产品有关系的群体就是用户&#xff0c;存在共性和差异了解用户的付费点&#xff0c;更好的优化产品是否使用&#xff1a;&#xff08;目标用户-已使用产品&#xff1a;种子用户-尝鲜&#xff1b;核心用…

【Golang】深入解读Go语言中的错误(error)与异常(panic)

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

【Pyecharts】时间线柱状图x轴坐标重复出现并重叠

问题描述 如图右侧显示多的一列坐标 解决方案 降低pyecharts版本&#xff1a;pip install pyecharts2.0.5