模拟打开的时候,如果是灰的需要修改
https://blog.csdn.net/qq_42880714/article/details/126509087
unit-create-view
uni-helper
uniapp小程序扩展
1、创建unit文件可自动注册路由
2、代码提示
3、鼠标悬停提示
npm i -D @types/wechat-miniprogram @uni-helper/uni-app-types
安装类型声明文件
npx degit dcloudio/uni-preset-vue#vite-ts uni-app-Vue3-TS
{
"extends": "@vue/tsconfig/tsconfig.json",
"compilerOptions": {
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
},
"lib": [
"esnext",
"dom"
],
"types": [
"@dcloudio/types",
"@types/wechat-miniprogram",
"@uni-helper/uni-app-types"
],
"ignoreDeprecations": "5.0" //TS废弃了之前的版本 所以使用这个
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
]
}
git clone http://git.itcast.cn/heimaqianduan/erabbit-uni-app-vue3-ts.git
heima-shop
npm install -g pnpm
pnpm set registry https://registry.npmmirror.com
使用管理员身份进入cmd 运行以上命令即可
npm i @dcloudio/uni-ui
// pages.json
// 组件自动映入规则
"easycom": {
// 开始自动扫描
"autoscan": true,
// 正则方式匹配
"custom": {
// uni-ui 规则如下配置
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},
pnpm i -D @uni-helper/uni-ui-types
{
"extends": "@vue/tsconfig/tsconfig.json",
"compilerOptions": {
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
},
"lib": [
"esnext",
"dom"
],
"types": [
"@dcloudio/types",
"@types/wechat-miniprogram",
"@uni-helper/uni-app-types",
"@uni-helper/uni-ui-types" // [!code 添加了ui ++]
],
"ignoreDeprecations": "5.0" //TS废弃了之前的版本 所以使用这个
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
]
}
import { defineStore } from 'pinia'
import { ref } from 'vue'
// 定义 Store
export const useMemberStore = defineStore(
'member',
() => {
// 会员信息
const profile = ref<any>()
// 保存会员信息,登录时使用
const setProfile = (val: any) => {
profile.value = val
}
// 清理会员信息,退出时使用
const clearProfile = () => {
profile.value = undefined
}
// 记得 return
return {
profile,
setProfile,
clearProfile,
}
},
// TODO: 持久化
{
// 配置持久化 网页端只要 persist:true
persist: {
// 调整为兼容多端的API
storage: {
getItem(key) {
return uni.getStorageSync(key)
},
setItem(key, value) {
uni.setStorageSync(key, value)
},
},
},
},
)
// src/utils/http.ts
import { useMemberStore } from '@/stores'
// 请求基地址
const baseURL = 'https://pcapi-xiaotuxian-front-devtest.itheima.net'
// 拦截器配置
const httpInterceptor = {
// 拦截前触发
invoke(options: UniApp.RequestOptions) {
// 1. 非 http 开头需拼接地址
if (!options.url.startsWith('http')) {
options.url = baseURL + options.url
}
// 2. 请求超时
options.timeout = 10000
// 3. 添加小程序端请求头标识
options.header = {
'source-client': 'miniapp',
...options.header,
}
// 4. 添加 token 请求头标识
const memberStore = useMemberStore()
const token = memberStore.profile?.token
if (token) {
options.header.Authorization = token
}
},
}
// 拦截 request 请求
uni.addInterceptor('request', httpInterceptor)
// 拦截 uploadFile 文件上传
uni.addInterceptor('uploadFile', httpInterceptor)
<script setup lang="ts">
import { useMemberStore } from '@/stores'
import '@/utils/http'
const memberStore = useMemberStore()
// 测试请求
const getDate = () => {
uni.request({
method: 'GET',
url: '/home/banner',
})
}
</script>
<template>
<view class="my">
<view>会员信息:{{ memberStore.profile }}</view>
<button
@tap="
memberStore.setProfile({
nickname: '黑马先锋',
})
"
size="mini"
plain
type="primary"
>
保存用户信息
</button>
<button @tap="memberStore.clearProfile()" size="mini" plain type="warn">清理用户信息</button>
<!-- @tap是uni-app框架的监听 其他是演示什么的 -->
<button @tap="getDate" size="mini" plain type="primary">测试请求</button>
</view>
</template>
<style lang="scss">
//
</style>
Cannot find module '@/stores'. Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option?
报错信息
解决方案
{
"extends": "@vue/tsconfig/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"sourceMap": true,
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"lib": ["esnext", "dom"],
"types": ["@dcloudio/types",
"miniprogram-api-typings",
"@uni-helper/uni-app-types",
"@uni-helper/uni-ui-types" // uni-ui 组件类型
]
},
"vueCompilerOptions": {
// experimentalRuntimeMode 已废弃,现调整为 nativeTags,请升级 Volar 插件至最新版本
"nativeTags": ["block", "component", "template", "slot"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
/**
* 请求函数
* @param UniApp.RequestOptions
* @returns Promise
* 1. 返回 Promise 对象,用于处理返回值类型
* 2. 获取数据成功
* 2.1 提取核心数据 res.data
* 2.2 添加类型,支持泛型
* 3. 获取数据失败
* 3.1 401错误 -> 清理用户信息,跳转到登录页
* 3.2 其他错误 -> 根据后端错误信息轻提示
* 3.3 网络错误 -> 提示用户换网络
*/
type Data<T> = {
code: string
msg: string
result: T
}
// 2.2 添加类型,支持泛型
export const http = <T>(options: UniApp.RequestOptions) => {
// 1. 返回 Promise 对象 一个异步处理函数 可以处于进行中、已成功、已失败三种状态 用于解决回调地域和异步代码复杂性问题
// 其中 resolve是成功调用的函数 另一个是失败调用的函数
return new Promise<Data<T>>((resolve, reject) => {
uni.request({
// 使用展开符 具体情况可以看我笔记的下面
...options,
// 响应成功
success(res) {
// 状态码 2xx,参考 axios 的设计
if (res.statusCode >= 200 && res.statusCode < 300) {
// 2.1 提取核心数据 res.data as是强制转换
resolve(res.data as Data<T>)
} else if (res.statusCode === 401) {
// 401错误 -> 清理用户信息,跳转到登录页
const memberStore = useMemberStore()
memberStore.clearProfile()
uni.navigateTo({ url: '/pages/login/login' })
reject(res)
} else {
// 其他错误 -> 根据后端错误信息轻提示
uni.showToast({
// 指定不显示图标
icon: 'none',
// 如果请求数据中有msg则显示 否则标题为请求错误
title: (res.data as Data<T>).msg || '请求错误',
})
reject(res)
}
},
// 响应失败
fail(err) {
uni.showToast({
icon: 'none',
title: '网络错误,换个网络试试',
})
reject(err)
},
})
})
}
//my.vue 的测试请i去更新
// 测试请求
const getDate = async () => {
const res = await http({
method: 'GET',
url: '/home/banner',
header: {},
})
console.log('请求成功', res)
}
…options
<!-- src/pages/index/componets/CustomNavbar.vue -->
<script setup lang="ts">
const { safeAreaInsets } = uni.getSystemInfoSync()
</script>
<template>
<view class="navbar" :style="{ paddingTop: safeAreaInsets?.top + 'px' }">
<!-- logo文字 -->
<view class="logo">
<image class="logo-image" src="@/static/images/logo.png"></image>
<text class="logo-text">新鲜 · 亲民 · 快捷</text>
</view>
<!-- 搜索条 -->
<view class="search">
<text class="icon-search">搜索商品</text>
<text class="icon-scan"></text>
</view>
</view>
</template>
<style lang="scss">
/* 自定义导航条 */
.navbar {
background-image: url(@/static/images/navigator_bg.png);
background-size: cover;
position: relative;
display: flex;
flex-direction: column;
padding-top: 20px;
.logo {
display: flex;
align-items: center;
height: 64rpx;
padding-left: 30rpx;
padding-top: 20rpx;
.logo-image {
width: 166rpx;
height: 39rpx;
}
.logo-text {
flex: 1;
line-height: 28rpx;
color: #fff;
margin: 2rpx 0 0 20rpx;
padding-left: 20rpx;
border-left: 1rpx solid #fff;
font-size: 26rpx;
}
}
.search {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10rpx 0 26rpx;
height: 64rpx;
margin: 16rpx 20rpx;
color: #fff;
font-size: 28rpx;
border-radius: 32rpx;
background-color: rgba(255, 255, 255, 0.5);
}
.icon-search {
&::before {
margin-right: 10rpx;
}
}
.icon-scan {
font-size: 30rpx;
padding: 15rpx;
}
}
</style>
// src/pages.json
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom", // 隐藏默认导航
"navigationBarTextStyle": "white",
"navigationBarTitleText": "首页"
}
}
这是一个常见的设计模式,称为"智能组件和哑组件"模式。
在这种模式中,父组件负责获取数据和处理业务逻辑,而子组件负责接收数据并展示。这种方式有几个优点:
另外,这种设计模式也有助于提高代码的可测试性和可维护性,因为业务逻辑更加集中,更容易进行单元测试和重构。
// 修改小程序的颜色 类似html5中为body指定颜色
page {
background-color: #f7f7f7;
}
获取前台数据
滚动实现
在子组件中获取数据而不在父组件中获取数据通常被称为"自包含组件"或"自管理组件"模式。在这种模式下,子组件负责管理自己的状态和数据获取逻辑,而不依赖于外部组件传递数据。
这种模式的优点包括:
然而,这种模式也存在一些局限性:
这样就能实现全部一起开始请求
<script setup lang="ts">
import { getHomeBannerAPI, getHomeCategoryAPI, getHomeHotAPI } from '@/services/home'
import CoustomNavbar from './components/CustomNavbar.vue'
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import type { BannerItem, CategoryItem, HotItem } from '@/types/home'
import CategoryPanel from './components/CategoryPanel.vue'
import HotPanel from './components/HotPanel.vue'
import type { XtxGuessInstance } from '@/types/component'
import PageSkeleton from './components/PageSkeleton.vue'
// 获取轮播图数据
const bannerList = ref<BannerItem[]>([])
const getHomeBannerDate = async () => {
const res = await getHomeBannerAPI()
bannerList.value = res.result
}
// 获取前台数据
const categoryList = ref<CategoryItem[]>([])
const getHomeCategoryDate = async () => {
const res = await getHomeCategoryAPI()
categoryList.value = res.result
}
const hotList = ref<HotItem[]>([])
// 获取热门推荐数据
const getgetHomeHotDate = async () => {
const res = await getHomeHotAPI()
hotList.value = res.result
}
// 获取猜你喜欢组件实例
const guessRef = ref<XtxGuessInstance>()
// 滚动触底事件
const onScrolltolower = () => {
guessRef.value?.getMore()
}
// 是否加载中标记
const isLoading = ref(false)
// 加载设置
onLoad(async () => {
isLoading.value = true
await Promise.all([getHomeBannerDate(), getHomeCategoryDate(), getgetHomeHotDate()])
isLoading.value = false
})
// 下拉刷新状态
const isTriggered = ref(false)
// 自定义下拉刷新被触发
const onRefresherrefresh = async () => {
console.log('被下拉')
// 开启动画
isTriggered.value = true
// 重置猜你喜欢组件数据
guessRef.value?.resetData() // 加载数据
await Promise.all([
getHomeBannerDate(),
getHomeCategoryDate(),
getgetHomeHotDate(),
guessRef.value?.getMore(),
])
// 关闭动画
isTriggered.value = false
}
</script>
<template>
<!-- 自定义导航栏 -->
<CoustomNavbar />
<!-- 使用这个来实现上面的不会滚动 -->
<!-- <scroll-view scroll-y @scrolltolower="onScrolltolower"> -->
<scroll-view
refresher-enabled
@refresherrefresh="onRefresherrefresh"
@scrolltolower="onScrolltolower"
:refresher-triggered="isTriggered"
class="scroll-view"
scroll-y
>
<PageSkeleton v-if="isLoading" />
<template v-else>
<XtxSwiper :list="bannerList" />
<CategoryPanel :list="categoryList" />
<!-- 热门推荐 -->
<HotPanel :list="hotList" />
<!-- 猜你喜欢,已经在pages.json中实现了自动导入 -->
<XtxGuess ref="guessRef" />
</template>
</scroll-view>
</template>
<style lang="scss">
// 修改小程序的颜色 类似html5中为body指定颜色
page {
background-color: #f7f7f7;
height: 100%;
display: flex;
flex-direction: column;
}
.scroll-view {
flex: 1;
}
</style>
注意!! 删掉hot页面的时候不会减少
import { http } from '@/utils/http'
import type { PageParams } from '@/types/global'
type HotParams = PageParams & {
/** Tab 项的 id,默认查询全部 Tab 项的第 1 页数据 */
subType?: string
}
/**
* 通用热门推荐类型
* @param url 请求地址
* @param data 请求参数
*/
export const getHotRecommendAPI = (url: string, data?: HotParams ) => {
return http({
method: 'GET',
url,
data,
})
}
动态获取热门数据
开发页面环境
小程序的页面跳转分为普通页面和tab页面
普通页面使用navigateTo tab页面使用switchTab
// 模拟手机号码快捷登录
const onGetphonenumberSimple = async () => {
const res = await postLoginWxMinSimpleAPI('17338870680')
const memberStore = useMemberStore()
// pinia自带存储数据方法
memberStore.setProfile(res.result)
uni.showToast({ icon: 'none', title: '登录成功' })
// 使用这段代码是因为 跳转tab页面会销毁掉之前的页面 所以会看不到提示
setTimeout(() => {
uni.switchTab({ url: '/pages/my/my ' })
}, 500)
}
// 获取轮播图函数
export const useBannerList = () => {
const bannerList = ref<BannerItem[]>([])
const getBannerData = async () => {
const res = await getHomeBannerAPI(2)
bannerList.value = res.result
}
return { getBannerData, bannerList }
}
import { useBannerList } from '@/composables/index'
const { getBannerData, bannerList } = useBannerList()
//在index.vue中需要取个别名
import { useBannerList } from '@/composables/index'
const { getBannerData: getHomeBannerDate, bannerList } = useBannerList()
{
// pages.json
// 组件自动映入规则
"easycom": {
// 开始自动扫描
"autoscan": true,
// 正则方式匹配
"custom": {
// uni-ui 规则如下配置
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
// 以Xtx 开头的组件,在components文件中查找引入(需要重启服务器)
"^Xtx(.*)": "@/components/Xtx$1.vue"
}
},
"pages": [
//pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
// src/pages.json
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom", // 隐藏默认导航
"navigationBarTextStyle": "white",
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/my/my",
"style": {
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/cart/cart",
"style": {
"navigationBarTitleText": "购物车"
}
},
{
"path": "pages/category/category",
"style": {
"navigationBarTitleText": "分类"
}
},
{
"path": "pages/login/login",
"style": {
"navigationStyle": "custom", // 隐藏默认导航
"navigationBarTextStyle": "white",
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/hot/hot",
"style": {
"navigationBarTitleText": "热门推荐"
}
},
{
"path": "pages/goods/goods",
"style": {
"navigationBarTitleText": "商品详情"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
// 设置 TabBar
"tabBar": {
"color": "#333",
"selectedColor": "#27ba9b",
"backgroundColor": "#fff",
"borderStyle": "white",
"list": [
{
"text": "首页",
"pagePath": "pages/index/index",
"iconPath": "static/tabs/home_default.png",
"selectedIconPath": "static/tabs/home_selected.png"
},
{
"text": "分类",
"pagePath": "pages/category/category",
"iconPath": "static/tabs/category_default.png",
"selectedIconPath": "static/tabs/category_selected.png"
},
{
"text": "购物车",
"pagePath": "pages/cart/cart",
"iconPath": "static/tabs/cart_default.png",
"selectedIconPath": "static/tabs/cart_selected.png"
},
{
"text": "我的",
"pagePath": "pages/my/my",
"iconPath": "static/tabs/user_default.png",
"selectedIconPath": "static/tabs/user_selected.png"
}
]
},
"subPackages": [
{
"root": "pagesMember",
"pages": [
{
"path": "settings/settings",
"style": {
"navigationBarTitleText": "设置"
}
}
]
}
]
}
<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<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">
<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="请填写昵称" :value="profile?.nickname" />
</view>
<view class="form-item">
<text class="label">性别</text>
<radio-group>
<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"
:value="profile?.birthday"
start="1900-01-01"
:end="new Date()"
>
<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" :value="profile?.fullLocation?.split(' ')" mode="region">
<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="请填写职业" :value="profile?.profession" />
</view>
</view>
<!-- 提交按钮 -->
<button class="form-button">保 存</button>
</view>
</view>
</template>
<style lang="scss">
page {
background-color: #f4f4f4;
}
.viewport {
display: flex;
flex-direction: column;
height: 100%;
background-image: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/order_bg.png);
background-size: auto 420rpx;
background-repeat: no-repeat;
}
// 导航栏
.navbar {
position: relative;
.title {
height: 40px;
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
font-weight: 500;
color: #fff;
}
.back {
position: absolute;
height: 40px;
width: 40px;
left: 0;
font-size: 20px;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
}
}
// 头像
.avatar {
text-align: center;
width: 100%;
height: 260rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.image {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background-color: #eee;
}
.text {
display: block;
padding-top: 20rpx;
line-height: 1;
font-size: 26rpx;
color: #fff;
}
}
// 表单
.form {
background-color: #f4f4f4;
&-content {
margin: 20rpx 20rpx 0;
padding: 0 20rpx;
border-radius: 10rpx;
background-color: #fff;
}
&-item {
display: flex;
height: 96rpx;
line-height: 46rpx;
padding: 25rpx 10rpx;
background-color: #fff;
font-size: 28rpx;
border-bottom: 1rpx solid #ddd;
&:last-child {
border: none;
}
.label {
width: 180rpx;
color: #333;
}
.account {
color: #666;
}
.input {
flex: 1;
display: block;
height: 46rpx;
}
.radio {
margin-right: 20rpx;
}
.picker {
flex: 1;
}
.placeholder {
color: #808080;
}
}
&-button {
height: 80rpx;
text-align: center;
line-height: 80rpx;
margin: 30rpx 20rpx;
color: #fff;
border-radius: 80rpx;
font-size: 30rpx;
background-color: #27ba9b;
}
}
</style>
const onSubmit = async () => {
console.log('123')
const res = await putMemberProfileAPI({
nickname: profile.value?.nickname,
})
uni.showToast({ icon: 'success', title: '保存成功' })
}
<!-- 提交按钮 -->
<button @tap="onSubmit" class="form-button">保 存</button>
// 这里将gender设置的类型不是string 所以需要使用as 指定为gender
const onGenderChange: UniHelper.RadioGroupOnChange = (ev) => {
profile.value.gender = ev.detail.value as Gender
}
<script setup lang="ts">
import { getMemberProfileAPI, putMemberProfileAPI } from '@/services/profile'
import { useMemberStore } from '@/stores'
import type { Gender, 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()
})
const memberStore = useMemberStore()
// 修改头像
const onAvatarChange = () => {
// 调用拍照/选择图片
uni.chooseMedia({
// 文件个数
count: 1,
// 文件类型
mediaType: ['image'],
success: (res) => {
// 本地路径
const { tempFilePath } = res.tempFiles[0]
// 文件上传
uni.uploadFile({
url: '/member/profile/avatar',
name: 'file', // 后端数据字段名
filePath: tempFilePath, // 新头像
success: (res) => {
// 判断状态码是否上传成功
if (res.statusCode === 200) {
// 提取头像
const { avatar } = JSON.parse(res.data).result
// 当前页面更新头像
profile.value!.avatar = avatar
// 更新 Store 头像
memberStore.profile!.avatar = avatar
uni.showToast({ icon: 'success', title: '更新成功' })
} else {
uni.showToast({ icon: 'error', title: '出现错误' })
}
},
})
},
})
}
// 这里将gender设置的类型不是string 所以需要使用as 指定为gender
const onGenderChange: UniHelper.RadioGroupOnChange = (ev) => {
profile.value.gender = ev.detail.value as Gender
}
// 修改生日
const onBirthdayChange: UniHelper.DatePickerOnChange = (ev) => {
profile.value.birthday = ev.detail.value
}
// 修改城市
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 { nickname, gender, birthday, profession } = profile.value
const res = await putMemberProfileAPI({
nickname,
gender,
birthday,
profession,
provinceCode: fullLocationCode[0],
cityCode: fullLocationCode[1],
countyCode: fullLocationCode[2],
})
// 更新store昵称
memberStore.profile!.nickname = res.result.nickname
uni.showToast({ icon: 'success', title: '保存成功' })
setTimeout(() => {
uni.navigateBack()
}, 400)
}
</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 @tap="onAvatarChange" class="avatar-content">
<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()"
:value="profile.birthday"
@change="onBirthdayChange"
>
<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="请填写职业" :value="profile?.profession" />
</view>
</view>
<!-- 提交按钮 -->
<button @tap="onSubmit" class="form-button">保 存</button>
</view>
</view>
</template>
<style lang="scss">
page {
background-color: #f4f4f4;
}
.viewport {
display: flex;
flex-direction: column;
height: 100%;
background-image: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/order_bg.png);
background-size: auto 420rpx;
background-repeat: no-repeat;
}
// 导航栏
.navbar {
position: relative;
.title {
height: 40px;
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
font-weight: 500;
color: #fff;
}
.back {
position: absolute;
height: 40px;
width: 40px;
left: 0;
font-size: 20px;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
}
}
// 头像
.avatar {
text-align: center;
width: 100%;
height: 260rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.image {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background-color: #eee;
}
.text {
display: block;
padding-top: 20rpx;
line-height: 1;
font-size: 26rpx;
color: #fff;
}
}
// 表单
.form {
background-color: #f4f4f4;
&-content {
margin: 20rpx 20rpx 0;
padding: 0 20rpx;
border-radius: 10rpx;
background-color: #fff;
}
&-item {
display: flex;
height: 96rpx;
line-height: 46rpx;
padding: 25rpx 10rpx;
background-color: #fff;
font-size: 28rpx;
border-bottom: 1rpx solid #ddd;
&:last-child {
border: none;
}
.label {
width: 180rpx;
color: #333;
}
.account {
color: #666;
}
.input {
flex: 1;
display: block;
height: 46rpx;
}
.radio {
margin-right: 20rpx;
}
.picker {
flex: 1;
}
.placeholder {
color: #808080;
}
}
&-button {
height: 80rpx;
text-align: center;
line-height: 80rpx;
margin: 30rpx 20rpx;
color: #fff;
border-radius: 80rpx;
font-size: 30rpx;
background-color: #27ba9b;
}
}
</style>
fy-content: center; align-items: center; font-size: 16px; font-weight: 500; color: #fff; }
.back { position: absolute; height: 40px; width: 40px; left: 0; font-size: 20px; color: #fff; display: flex; justify-content: center; align-items: center; } }
// 头像 .avatar { text-align: center; width: 100%; height: 260rpx; display: flex; flex-direction: column; justify-content: center; align-items: center;
.image { width: 160rpx; height: 160rpx; border-radius: 50%; background-color: #eee; }
.text { display: block; padding-top: 20rpx; line-height: 1; font-size: 26rpx; color: #fff; } }
// 表单 .form { background-color: #f4f4f4;
&-content { margin: 20rpx 20rpx 0; padding: 0 20rpx; border-radius: 10rpx; background-color: #fff; }
&-item { display: flex; height: 96rpx; line-height: 46rpx; padding: 25rpx 10rpx; background-color: #fff; font-size: 28rpx; border-bottom: 1rpx solid #ddd;
&:last-child {
border: none;
}
.label {
width: 180rpx;
color: #333;
}
.account {
color: #666;
}
.input {
flex: 1;
display: block;
height: 46rpx;
}
.radio {
margin-right: 20rpx;
}
.picker {
flex: 1;
}
.placeholder {
color: #808080;
}
}
&-button { height: 80rpx; text-align: center; line-height: 80rpx; margin: 30rpx 20rpx; color: #fff; border-radius: 80rpx; font-size: 30rpx; background-color: #27ba9b; } }
# TS开发项目于JS开发项目的不同
+ 不同之处就是指定类型
+ 在编写发送请求的时候,js项目只要http{}然后开始写相关接口数据而Ts项目还要给http<>{}指定一个类型,用来判断请求返回值是否符合编写时的要求
+ TS的类型有别于java类型,简单的number是对应int、long。但自定义的数据类型,如果要用java来理解的话,我认为TS类型像数据结构,像枚举。 TS定义的类型就是数据的组织形式,这个组织形式是根据返回的数据不同而不同