登录,个人中心,联系客服

This commit is contained in:
Jason
2022-09-08 16:28:56 +08:00
parent fca4c24a8e
commit 04fad4b18c
49 changed files with 600 additions and 247 deletions

View File

@@ -1,14 +1,13 @@
<script setup lang="ts">
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
import request from './utils/request'
import { onLaunch } from '@dcloudio/uni-app'
import { useAppStore } from './stores/app'
import { useUserStore } from './stores/user'
const { getConfig } = useAppStore()
const { getUser } = useUserStore()
console.log(useUserStore())
onLaunch(() => {
console.log('App Launch')
})
onShow(() => {
console.log('App Show')
})
onHide(() => {
console.log('App Hide')
getConfig()
getUser()
})
</script>
<style lang="scss">

View File

@@ -1,6 +1,10 @@
import request from '@/utils/request'
//注册
export function smsSend(data: Record<string, any>) {
//发送短信
export function smsSend(data: any) {
return request.post({ url: '/sms/send', data: data })
}
export function getConfig() {
return request.get({ url: '/config' })
}

View File

@@ -4,3 +4,8 @@ import request from '@/utils/request'
export function getIndex() {
return request.get({ url: '/index' })
}
// 装修页面
export function getDecorate(data: any) {
return request.get({ url: '/decorate', data })
}

5
app/src/api/user.ts Normal file
View File

@@ -0,0 +1,5 @@
import request from '@/utils/request'
export function getUserCenter() {
return request.get({ url: '/user/center' })
}

View File

@@ -1,27 +0,0 @@
<template>
<view>
<view v-for="(item, index) in pages" :key="index">
<template v-if="item.name == 'search'">
<w-search :content="item.content" :styles="item.styles" />
</template>
<template v-if="item.name == 'banner'">
<w-banner :content="item.content" :styles="item.styles" />
</template>
<template v-if="item.name == 'nav'">
<w-nav :content="item.content" :styles="item.styles" />
</template>
</view>
</view>
</template>
<script setup lang="ts">
import { PropType } from 'vue'
defineProps({
pages: {
type: Array as PropType<any[]>
}
})
</script>
<style></style>

View File

@@ -1,19 +1,29 @@
<template>
<view class="banner h-[400rpx] bg-white">
<view
class="banner h-[340rpx] bg-white translate-y-0"
v-if="content.data.length && content.enabled"
>
<swiper
class="swiper h-full"
indicator-dots
:indicator-dots="content.data.length > 1"
indicator-active-color="#4173ff"
:autoplay="true"
>
<swiper-item v-for="(item, index) in content.data" :key="index">
<u-image mode="aspectFit" width="100%" height="100%" :src="item.image" />
<u-image
mode="aspectFit"
width="100%"
height="100%"
:src="getImageUrl(item.image)"
/>
</swiper-item>
</swiper>
</view>
</template>
<script setup lang="ts">
import { useAppStore } from '@/stores/app'
const props = defineProps({
content: {
type: Object,
@@ -24,6 +34,7 @@ const props = defineProps({
default: () => ({})
}
})
const { getImageUrl } = useAppStore()
</script>
<style></style>

View File

@@ -0,0 +1,52 @@
<template>
<view
class="customer-service bg-white flex flex-col justify-center items-center mx-[36rpx] mt-[20rpx] rounded-lg px-[110rpx] pt-[100rpx] pb-[160rpx]"
>
<u-image width="280" height="280" :src="getImageUrl(content.qrcode)" />
<view v-if="content.title" class="text-lg mt-[14rpx] font-medium">{{ content.title }}</view>
<view v-if="content.time" class="text-content mt-[40rpx]"
>服务时间{{ content.time }}</view
>
<view v-if="content.mobile" class="text-content mt-[14rpx] flex flex-wrap">
客服电话{{ content.mobile }}
<!-- #ifdef H5 -->
<a class="ml-[10rpx] phone text-muted underline" :href="'tel:' + content.mobile">
拨打
</a>
<!-- #endif -->
<!-- #ifndef H5 -->
<view class="ml-[10rpx] phone text-muted underline" @click="handleCall">拨打</view>
<!-- #endif -->
</view>
<view class="mt-[100rpx] w-full">
<u-button type="primary" shape="circle" @click="saveImageToPhotosAlbum(content.qrcode)">
保存二维码图片
</u-button>
</view>
</view>
</template>
<script lang="ts" setup>
import { useAppStore } from '@/stores/app'
import { saveImageToPhotosAlbum } from '@/utils/file'
const props = defineProps({
content: {
type: Object,
default: () => ({})
},
styles: {
type: Object,
default: () => ({})
}
})
const { getImageUrl } = useAppStore()
const handleCall = () => {
uni.makePhoneCall({
phoneNumber: String(props.content.mobile)
})
}
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,50 @@
<template>
<div class="my-service bg-white mx-[20rpx] mt-[20rpx] rounded-lg">
<div
v-if="content.title"
class="title px-[30rpx] py-[20rpx] border-light border-solid border-0 border-b"
>
<div>{{ content.title }}</div>
</div>
<div v-if="content.style == 1" class="flex flex-wrap pt-[40rpx] pb-[20rpx]">
<div
v-for="(item, index) in content.data"
:key="index"
class="flex flex-col items-center w-1/4 mb-[15px]"
>
<u-image width="52" height="52" :src="getImageUrl(item.image)" alt="" />
<div class="mt-[7px]">{{ item.name }}</div>
</div>
</div>
<div v-if="content.style == 2">
<div
v-for="(item, index) in content.data"
:key="index"
class="flex items-center border-light border-solid border-0 border-b h-[100rpx] px-[24rpx]"
>
<u-image width="48" height="48" :src="item.image" alt="" />
<div class="ml-[20rpx] flex-1">{{ item.name }}</div>
<div class="text-muted">
<u-icon name="arrow-right" />
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useAppStore } from '@/stores/app'
const props = defineProps({
content: {
type: Object,
default: () => ({})
},
styles: {
type: Object,
default: () => ({})
}
})
const { getImageUrl } = useAppStore()
</script>
<style lang="scss"></style>

View File

@@ -1,12 +1,12 @@
<template>
<view class="nav pt-[30rpx] pb-[16rpx] bg-white">
<view class="nav pt-[30rpx] pb-[16rpx] bg-white" v-if="content.data.length && content.enabled">
<view class="nav-item flex flex-wrap">
<view
v-for="(item, index) in content.data"
:key="index"
class="flex flex-col items-center w-1/5 mb-[30rpx]"
>
<u-image width="41px" height="41px" :src="item.image" alt="" />
<u-image width="41px" height="41px" :src="getImageUrl(item.image)" alt="" />
<view class="mt-[14rpx]">{{ item.name }}</view>
</view>
</view>
@@ -14,6 +14,8 @@
</template>
<script setup lang="ts">
import { useAppStore } from '@/stores/app'
const props = defineProps({
content: {
type: Object,
@@ -24,6 +26,8 @@ const props = defineProps({
default: () => ({})
}
})
const { getImageUrl } = useAppStore()
</script>
<style></style>

View File

@@ -0,0 +1,41 @@
<template>
<view
class="banner h-[200rpx] mx-[20rpx] mt-[20rpx] translate-y-0"
v-if="content.data.length && content.enabled"
>
<swiper
class="swiper h-full"
:indicator-dots="content.data.length > 1"
indicator-active-color="#4173ff"
:autoplay="true"
>
<swiper-item v-for="(item, index) in content.data" :key="index">
<u-image
mode="aspectFit"
width="100%"
height="100%"
:src="getImageUrl(item.image)"
:border-radius="14"
/>
</swiper-item>
</swiper>
</view>
</template>
<script setup lang="ts">
import { useAppStore } from '@/stores/app'
const props = defineProps({
content: {
type: Object,
default: () => ({})
},
styles: {
type: Object,
default: () => ({})
}
})
const { getImageUrl } = useAppStore()
</script>
<style></style>

View File

@@ -0,0 +1,45 @@
<template>
<div class="user-info flex items-center px-[50rpx]">
<navigator
v-if="isLogin"
class="flex items-center"
hover-class="none"
url="/pages/login/login"
>
<u-avatar :src="user.avatar" :size="120"></u-avatar>
<div class="text-white text-3xl ml-[20rpx]">{{ user.nickname }}</div>
</navigator>
<navigator v-else class="flex items-center" hover-class="none" url="/pages/login/login">
<u-avatar src="/static/images/user/default_avatar.png" :size="120"></u-avatar>
<div class="text-white text-3xl ml-[20rpx]">未登录</div>
</navigator>
</div>
</template>
<script lang="ts" setup>
const props = defineProps({
content: {
type: Object,
default: () => ({})
},
styles: {
type: Object,
default: () => ({})
},
user: {
type: Object,
default: () => ({})
},
isLogin: {
type: Boolean
}
})
</script>
<style lang="scss" scoped>
.user-info {
background: url(/static/images/user/my_topbg.png);
height: 115px;
background-position: bottom;
background-size: 100% auto;
}
</style>

View File

@@ -0,0 +1,21 @@
import { ref } from 'vue'
export function useLockFn(fn: (...args: any[]) => Promise<any>) {
const isLock = ref(false)
const lockFn = async (...args: any[]) => {
if (isLock.value) return
isLock.value = true
try {
const res = await fn(...args)
isLock.value = false
return res
} catch (e) {
isLock.value = false
throw e
}
}
return {
isLock,
lockFn
}
}

View File

@@ -1,11 +1,9 @@
import { createSSRApp } from 'vue'
// import { createPinia } from 'pinia'
import App from './App.vue'
import plugins from './plugins'
import './styles/index.scss'
export function createApp() {
const app = createSSRApp(App)
console.log(app.config.globalProperties)
app.use(plugins)
return {
app

View File

@@ -29,6 +29,12 @@
"style": {
"navigationBarTitleText": "注册"
}
},
{
"path": "pages/customer_service/customer_service",
"style": {
"navigationBarTitleText": "联系客服"
}
}
],
"globalStyle": {

View File

@@ -0,0 +1,26 @@
<template>
<view class="customer-service">
<view v-for="(item, index) in state.pages" :key="index">
<template v-if="item.name == 'customer-service'">
<w-customer-service :content="item.content" :styles="item.styles" />
</template>
</view>
</view>
</template>
<script setup lang="ts">
import { getDecorate } from '@/api/shop'
import { reactive } from 'vue'
const state = reactive<{
pages: any[]
}>({
pages: []
})
const getData = async () => {
const data = await getDecorate({ id: 3 })
state.pages = JSON.parse(data.pages)
}
getData()
</script>
<style></style>

View File

@@ -1,14 +1,26 @@
<template>
<view class="index">
<decoration :pages="state.pages" />
<view v-for="(item, index) in state.pages" :key="index">
<template v-if="item.name == 'search'">
<w-search :content="item.content" :styles="item.styles" />
</template>
<template v-if="item.name == 'banner'">
<w-banner :content="item.content" :styles="item.styles" />
</template>
<template v-if="item.name == 'nav'">
<w-nav :content="item.content" :styles="item.styles" />
</template>
</view>
<view class="article"> </view>
</view>
</template>
<script setup lang="ts">
import { getIndex } from '@/api/shop'
import { reactive } from 'vue'
const state = reactive({
import { reactive, ref } from 'vue'
const state = reactive<{
pages: any[]
}>({
pages: []
})
const getData = async () => {

View File

@@ -10,7 +10,7 @@
<u-form borderBottom>
<template v-if="scene == LoginTypeEnum.ACCOUNT">
<u-form-item borderBottom>
<u-icon class="mr-2" :size="36" name="/static/images/icon_user.png" />
<u-icon class="mr-2" :size="36" name="/static/images/icon/icon_user.png" />
<u-input
class="flex-1"
v-model="formData.username"
@@ -19,7 +19,11 @@
/>
</u-form-item>
<u-form-item borderBottom>
<u-icon class="mr-2" :size="36" name="/static/images/icon_password.png" />
<u-icon
class="mr-2"
:size="36"
name="/static/images/icon/icon_password.png"
/>
<u-input
class="flex-1"
v-model="formData.password"
@@ -36,7 +40,11 @@
</template>
<template v-if="scene == LoginTypeEnum.MOBILE">
<u-form-item borderBottom>
<u-icon class="mr-2" :size="36" name="/static/images/icon_mobile.png" />
<u-icon
class="mr-2"
:size="36"
name="/static/images/icon/icon_mobile.png"
/>
<u-input
class="flex-1"
v-model="formData.mobile"
@@ -45,7 +53,7 @@
/>
</u-form-item>
<u-form-item borderBottom>
<u-icon class="mr-2" :size="36" name="/static/images/icon_code.png" />
<u-icon class="mr-2" :size="36" name="/static/images/icon/icon_code.png" />
<u-input
class="flex-1"
v-model="formData.code"
@@ -77,7 +85,7 @@
</u-checkbox>
</view>
<view class="mt-[40rpx]">
<u-button type="primary" shape="circle" @click="accountLogin(scene)">
<u-button type="primary" shape="circle" @click="handleLogin(scene)">
</u-button>
</view>
@@ -95,7 +103,7 @@
<u-divider>第三方登录</u-divider>
<div class="flex justify-center mt-[40rpx]">
<div class="flex flex-col items-center" @click="wxLogin">
<u-icon name="/static/images/icon_wx.png" size="80" />
<u-icon name="/static/images/icon/icon_wx.png" size="80" />
<div class="text-sm mt-[10px]">微信登录</div>
</div>
</div>
@@ -108,7 +116,9 @@
import { login } from '@/api/account'
import { smsSend } from '@/api/app'
import { SMSEnum } from '@/enums/appEnums'
import { reactive, ref, shallowRef } from 'vue'
import { useLockFn } from '@/hooks/useLockFn'
import { useUserStore } from '@/stores/user'
import { reactive, ref, shallowRef, watch } from 'vue'
enum LoginTypeEnum {
MOBILE = 'mobile',
ACCOUNT = 'account',
@@ -118,6 +128,8 @@ const uCodeRef = shallowRef()
const scene = ref(LoginTypeEnum.ACCOUNT)
const codeTips = ref('')
const isCheckAgreement = ref(false)
const userStore = useUserStore()
const formData = reactive({
username: '',
password: '',
@@ -140,7 +152,8 @@ const sendSms = async () => {
uCodeRef.value?.start()
}
}
const accountLogin = async (scene: LoginTypeEnum, code?: string) => {
const loginFun = async (scene: LoginTypeEnum, code?: string) => {
if (!isCheckAgreement.value) return uni.$u.toast('请勾选已阅读并同意《服务协议》和《隐私协议》')
if (scene == LoginTypeEnum.ACCOUNT) {
if (!formData.username) return uni.$u.toast('请输入账号/手机号码')
@@ -155,17 +168,29 @@ const accountLogin = async (scene: LoginTypeEnum, code?: string) => {
scene
}
if (code) params.code = code
await login(params)
uni.$u.toast('登录成功')
uni.navigateBack()
uni.showLoading({
title: '请稍后...'
})
try {
const data = await login(params)
userStore.login(data.token)
await userStore.getUser()
uni.$u.toast('登录成功')
uni.hideLoading()
uni.navigateBack()
} catch (error: any) {
uni.hideLoading()
throw new Error(error)
}
}
const { isLock, lockFn: handleLogin } = useLockFn(loginFun)
const wxLogin = async () => {
const data: any = await uni.login({
provider: 'weixin'
})
console.log(data)
accountLogin(LoginTypeEnum.MNP, data.code)
handleLogin(LoginTypeEnum.MNP, data.code)
}
</script>

View File

@@ -1,5 +1,7 @@
<template>
<view class="register min-h-full flex flex-col items-center px-[40rpx] pt-[40rpx] box-border">
<view
class="register bg-white min-h-full flex flex-col items-center px-[40rpx] pt-[40rpx] box-border"
>
<view class="w-full">
<u-form borderBottom :label-width="150">
<u-form-item label="创建账号" borderBottom>

View File

@@ -1,7 +1,45 @@
<template>
<view class="content">个人设置</view>
<view class="user">
<view v-for="(item, index) in state.pages" :key="index">
<template v-if="item.name == 'user-info'">
<w-user-info
:content="item.content"
:styles="item.styles"
:user="userInfo"
:is-login="isLogin"
/>
</template>
<template v-if="item.name == 'my-service'">
<w-my-service :content="item.content" :styles="item.styles" />
</template>
<template v-if="item.name == 'user-banner'">
<w-user-banner :content="item.content" :styles="item.styles" />
</template>
</view>
</view>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
import { getDecorate } from '@/api/shop'
import { useUserStore } from '@/stores/user'
import { onShow } from '@dcloudio/uni-app'
import { storeToRefs } from 'pinia'
import { reactive } from 'vue'
const state = reactive<{
pages: any[]
}>({
pages: []
})
const getData = async () => {
const data = await getDecorate({ id: 2 })
state.pages = JSON.parse(data.pages)
}
const userStore = useUserStore()
const { userInfo, isLogin } = storeToRefs(userStore)
onShow(() => {
userStore.getUser()
})
getData()
</script>
<style></style>

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 679 B

After

Width:  |  Height:  |  Size: 679 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,8 +1,22 @@
import { defineStore } from 'pinia'
import { getConfig } from '@/api/app'
interface AppSate {
config: Record<string, any>
}
export const useAppStore = defineStore({
id: 'userStore',
state: () => ({}),
id: 'appStore',
state: (): AppSate => ({
config: {}
}),
getters: {},
actions: {}
actions: {
getImageUrl(url: string) {
return url ? `${this.config.domain}${url}` : ''
},
async getConfig() {
const data = await getConfig()
this.config = data
}
}
})

View File

@@ -1,12 +1,34 @@
import { getClient } from '@/utils/client'
import { getUserCenter } from '@/api/user'
import { TOKEN_KEY } from '@/enums/cacheEnums'
import cache from '@/utils/cache'
import { defineStore } from 'pinia'
export const useAppStore = defineStore({
id: 'appStore',
// convert to a function
state: () => ({
client: getClient()
interface UserSate {
userInfo: Record<string, any>
token: string | null
}
export const useUserStore = defineStore({
id: 'userStore',
state: (): UserSate => ({
userInfo: {},
token: cache.get(TOKEN_KEY) || null
}),
getters: {},
actions: {}
getters: {
isLogin: (state) => !!state.token
},
actions: {
async getUser() {
const data = await getUserCenter()
this.userInfo = data
},
login(token: string) {
this.token = token
cache.set(TOKEN_KEY, token)
},
logout() {
this.token = ''
this.userInfo = {}
cache.remove(TOKEN_KEY)
}
}
})

View File

@@ -12,7 +12,7 @@ const cache = {
data = JSON.stringify(data)
}
try {
window.localStorage.setItem(key, data)
uni.setStorageSync(key, data)
} catch (e) {
return null
}
@@ -20,13 +20,13 @@ const cache = {
get(key: string) {
key = this.getKey(key)
try {
const data = window.localStorage.getItem(key)
const data = uni.getStorageSync(key)
if (!data) {
return null
}
const { value, expire } = JSON.parse(data)
if (expire && expire < this.time()) {
window.localStorage.removeItem(key)
uni.removeStorageSync(key)
return null
}
return value
@@ -40,7 +40,7 @@ const cache = {
},
remove(key: string) {
key = this.getKey(key)
window.localStorage.removeItem(key)
uni.removeStorageSync(key)
},
getKey(key: string) {
return this.key + key

21
app/src/utils/file.ts Normal file
View File

@@ -0,0 +1,21 @@
export async function saveImageToPhotosAlbum(url: string) {
if (!url) return uni.$u.$toast('图片不存在')
//#ifdef H5
uni.$u.$toast('长按图片保存')
//#endif
try {
const res: any = await uni.downloadFile({ url, timeout: 10000 })
await uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath
})
uni.showToast({
title: '保存成功',
icon: 'success'
})
} catch (error: any) {
uni.showToast({
title: error.errMsg || '保存失败',
icon: 'none'
})
}
}