新增
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { onLaunch } from '@dcloudio/uni-app'
|
||||
import { useAppStore } from './stores/app'
|
||||
import { useUserStore } from './stores/user'
|
||||
import { useThemeStore } from './stores/theme'
|
||||
import { useRoute, useRouter } from 'uniapp-router-next'
|
||||
const appStore = useAppStore()
|
||||
const { getUser } = useUserStore()
|
||||
const { getTheme } = useThemeStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
//#ifdef H5
|
||||
const setH5WebIcon = () => {
|
||||
const config = appStore.getWebsiteConfig
|
||||
let favicon: HTMLLinkElement = document.querySelector('link[rel="icon"]')!
|
||||
if (favicon) {
|
||||
favicon.href = config.h5_favicon
|
||||
return
|
||||
}
|
||||
favicon = document.createElement('link')
|
||||
favicon.rel = 'icon'
|
||||
favicon.href = config.h5_favicon
|
||||
document.head.appendChild(favicon)
|
||||
}
|
||||
//#endif
|
||||
|
||||
const getConfig = async () => {
|
||||
await appStore.getConfig()
|
||||
//#ifdef H5
|
||||
setH5WebIcon()
|
||||
//#endif
|
||||
const { status, page_status, page_url } = appStore.getH5Config
|
||||
if (route.meta.webview) return
|
||||
//处理关闭h5渠道
|
||||
//#ifdef H5
|
||||
if (status == 0) {
|
||||
if (page_status == 1) return (location.href = page_url)
|
||||
router.reLaunch('/pages/empty/empty')
|
||||
}
|
||||
//#endif
|
||||
}
|
||||
|
||||
onLaunch(async () => {
|
||||
getTheme()
|
||||
getConfig()
|
||||
//#ifdef H5
|
||||
setH5WebIcon()
|
||||
//#endif
|
||||
await getUser()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
//
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
import { client } from '@/utils/client'
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 登录
|
||||
export function login(data: Record<string, any>) {
|
||||
return request.post({ url: '/login/account', data: { ...data, terminal: client } })
|
||||
}
|
||||
|
||||
//注册
|
||||
export function register(data: Record<string, any>) {
|
||||
return request.post({ url: '/login/register', data: { ...data, channel: client } })
|
||||
}
|
||||
|
||||
//向微信请求code的链接
|
||||
export function getWxCodeUrl(data: Record<string, any>) {
|
||||
return request.get({ url: '/login/codeUrl', data })
|
||||
}
|
||||
|
||||
export function OALogin(data: Record<string, any>) {
|
||||
return request.post({ url: '/login/oaLogin', data })
|
||||
}
|
||||
|
||||
export function mnpLogin(data: Record<string, any>) {
|
||||
return request.post({ url: '/login/mnpLogin', data })
|
||||
}
|
||||
|
||||
//更新微信小程序头像昵称
|
||||
export function updateUser(data: Record<string, any>, header: any) {
|
||||
return request.post({ url: '/login/updateUser', data, header })
|
||||
}
|
||||
|
||||
//小程序绑定微信
|
||||
export function mnpAuthBind(data: Record<string, any>) {
|
||||
return request.post({ url: '/login/mnpAuthBind', data })
|
||||
}
|
||||
|
||||
//公众号绑定微信
|
||||
export function oaAuthBind(data: Record<string, any>) {
|
||||
return request.post({ url: '/login/oaAuthBind', data })
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
//发送短信
|
||||
export function smsSend(data: any) {
|
||||
return request.post({ url: '/sms/sendCode', data: data })
|
||||
}
|
||||
|
||||
export function getConfig() {
|
||||
return request.get({ url: '/index/config' })
|
||||
}
|
||||
|
||||
export function getPolicy(data: any) {
|
||||
return request.get({ url: '/index/policy', data: data })
|
||||
}
|
||||
|
||||
export function uploadImage(file: any, token?: string) {
|
||||
return request.uploadFile({
|
||||
url: '/upload/image',
|
||||
filePath: file,
|
||||
name: 'file',
|
||||
header: {
|
||||
token
|
||||
},
|
||||
fileType: 'image'
|
||||
})
|
||||
}
|
||||
|
||||
export function wxJsConfig(data: any) {
|
||||
return request.get({ url: '/wechat/jsConfig', data })
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* @description 获取文章分类
|
||||
* @return { Promise }
|
||||
*/
|
||||
export function getArticleCate() {
|
||||
return request.get({ url: '/article/cate' })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取文章列表
|
||||
* @return { Promise }
|
||||
*/
|
||||
export function getArticleList(data: Record<string, any>) {
|
||||
return request.get({ url: '/article/lists', data: data })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取文章详情
|
||||
* @param { number } id
|
||||
* @return { Promise }
|
||||
*/
|
||||
export function getArticleDetail(data: { id: number }) {
|
||||
return request.get({ url: '/article/detail', data: data })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 加入收藏
|
||||
* @param { number } id
|
||||
* @return { Promise }
|
||||
*/
|
||||
export function addCollect(data: { id: number }) {
|
||||
return request.post({ url: '/article/addCollect', data: data }, { isAuth: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 取消收藏
|
||||
* @param { number } id
|
||||
* @return { Promise }
|
||||
*/
|
||||
export function cancelCollect(data: { id: number }) {
|
||||
return request.post({ url: '/article/cancelCollect', data: data }, { isAuth: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取收藏列表
|
||||
* @return { Promise }
|
||||
*/
|
||||
export function getCollect() {
|
||||
return request.get({ url: '/article/collect' })
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
//支付方式
|
||||
export function getPayWay(data: any) {
|
||||
return request.get({ url: '/pay/payWay', data }, { isAuth: true })
|
||||
}
|
||||
|
||||
// 预支付
|
||||
export function prepay(data: any) {
|
||||
return request.post({ url: '/pay/prepay', data }, { isAuth: true })
|
||||
}
|
||||
|
||||
// 预支付
|
||||
export function getPayResult(data: any) {
|
||||
return request.get({ url: '/pay/payStatus', data }, { isAuth: true })
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
//充值
|
||||
export function recharge(data: any) {
|
||||
return request.post({ url: '/recharge/recharge', data }, { isAuth: true })
|
||||
}
|
||||
|
||||
//充值记录
|
||||
export function rechargeRecord(data: any) {
|
||||
return request.get({ url: '/recharge/lists', data }, { isAuth: true })
|
||||
}
|
||||
|
||||
// 充值配置
|
||||
export function rechargeConfig() {
|
||||
return request.get({ url: '/recharge/config' }, { isAuth: true })
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
//首页数据
|
||||
export function getIndex() {
|
||||
return request.get({ url: '/index/index' })
|
||||
}
|
||||
|
||||
// 装修页面
|
||||
export function getDecorate(data: any) {
|
||||
return request.get({ url: '/index/decorate', data }, { ignoreCancel: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 热门搜索
|
||||
* @return { Promise }
|
||||
*/
|
||||
export function getHotSearch() {
|
||||
return request.get({ url: '/search/hotLists' })
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getUserCenter(header?: any) {
|
||||
return request.get({ url: '/user/center', header }, { ignoreCancel: true })
|
||||
}
|
||||
|
||||
// 个人信息
|
||||
export function getUserInfo() {
|
||||
return request.get({ url: '/user/info' }, { isAuth: true })
|
||||
}
|
||||
|
||||
// 个人编辑
|
||||
export function userEdit(data: any) {
|
||||
return request.post({ url: '/user/setInfo', data }, { isAuth: true })
|
||||
}
|
||||
|
||||
// 绑定手机
|
||||
export function userBindMobile(data: any, header?: any) {
|
||||
return request.post({ url: '/user/bindMobile', data, header }, { isAuth: true })
|
||||
}
|
||||
|
||||
// 微信电话
|
||||
export function userMnpMobile(data: any) {
|
||||
return request.post({ url: '/user/getMobileByMnp', data }, { isAuth: true })
|
||||
}
|
||||
|
||||
// 更改手机号
|
||||
export function userChangePwd(data: any) {
|
||||
return request.post({ url: '/user/changePassword', data }, { isAuth: true })
|
||||
}
|
||||
|
||||
//忘记密码
|
||||
export function forgotPassword(data: Record<string, any>) {
|
||||
return request.post({ url: '/user/resetPassword', data })
|
||||
}
|
||||
|
||||
//余额明细
|
||||
export function accountLog(data: any) {
|
||||
return request.get({ url: '/account_log/lists', data })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<button
|
||||
class="avatar-upload p-0 m-0 rounded"
|
||||
:style="styles"
|
||||
hover-class="none"
|
||||
open-type="chooseAvatar"
|
||||
@click="chooseAvatar"
|
||||
@chooseavatar="chooseAvatar"
|
||||
>
|
||||
<image class="w-full h-full" mode="heightFix" :src="modelValue" v-if="modelValue" />
|
||||
<slot v-else>
|
||||
<div
|
||||
:style="styles"
|
||||
class="border border-dotted border-light flex w-full h-full flex-col items-center justify-center text-muted text-xs box-border rounded"
|
||||
>
|
||||
<u-icon name="plus" :size="36" />
|
||||
添加图片
|
||||
</div>
|
||||
</slot>
|
||||
</button>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { uploadImage } from '@/api/app'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { addUnit } from '@/utils/util'
|
||||
import { isBoolean } from 'lodash'
|
||||
import { computed, CSSProperties, onUnmounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String
|
||||
},
|
||||
fileKey: {
|
||||
type: String,
|
||||
default: 'uri'
|
||||
},
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: 120
|
||||
},
|
||||
round: {
|
||||
type: [Boolean, String, Number],
|
||||
default: false
|
||||
},
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: string): void
|
||||
}>()
|
||||
const userStore = useUserStore()
|
||||
const styles = computed<CSSProperties>(() => {
|
||||
const size = addUnit(props.size)
|
||||
return {
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: isBoolean(props.round) ? (props.round ? '50%' : '') : addUnit(props.round)
|
||||
}
|
||||
})
|
||||
|
||||
const chooseAvatar = (e: any) => {
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.navigateTo({
|
||||
url: '/uni_modules/vk-uview-ui/components/u-avatar-cropper/u-avatar-cropper?destWidth=300&rectWidth=200&fileType=jpg'
|
||||
})
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
const path = e.detail?.avatarUrl
|
||||
if (path) {
|
||||
uploadImageIng(path)
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
const uploadImageIng = async (file: string) => {
|
||||
uni.showLoading({
|
||||
title: '正在上传中...'
|
||||
})
|
||||
try {
|
||||
const res: any = await uploadImage(file, userStore.temToken!)
|
||||
uni.hideLoading()
|
||||
console.log(res)
|
||||
emit('update:modelValue', res[props.fileKey])
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
uni.$u.toast(error)
|
||||
}
|
||||
}
|
||||
// 监听从裁剪页发布的事件,获得裁剪结果
|
||||
uni.$on('uAvatarCropper', (path) => {
|
||||
uploadImageIng(path)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
uni.$off('uAvatarCropper')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.avatar-upload {
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<u-swiper
|
||||
v-if="lists.length"
|
||||
:list="lists"
|
||||
:mode="mode"
|
||||
:height="height"
|
||||
:effect3d="effect3d"
|
||||
:indicator-pos="indicatorPos"
|
||||
:autoplay="autoplay"
|
||||
:interval="interval"
|
||||
:duration="duration"
|
||||
:circular="circular"
|
||||
:borderRadius="borderRadius"
|
||||
:current="current"
|
||||
:name="name"
|
||||
:bg-color="bgColor"
|
||||
@click="handleClick"
|
||||
@change="handleChange"
|
||||
>
|
||||
</u-swiper>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {useAppStore} from "@/stores/app";
|
||||
import {navigateTo, navigateToMiniProgram, LinkTypeEnum} from "@/utils/util";
|
||||
import {watchEffect, computed} from "vue";
|
||||
import {useRouter} from "uniapp-router-next";
|
||||
|
||||
const emit = defineEmits(["change"]);
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
content?: any; // 轮播图数据
|
||||
mode?: string; // 指示器模式 rect / dot / number / none
|
||||
height?: string; // 轮播图组件高度
|
||||
indicatorPos?: string; // 指示器的位置 topLeft / topCenter / topRight / bottomLeft / bottomRight
|
||||
effect3d?: boolean; // 是否开启3D效果
|
||||
autoplay?: boolean; // 是否自动播放
|
||||
interval?: number | string; // 自动轮播时间间隔,单位ms
|
||||
duration?: number | string; // 切换一张轮播图所需的时间,单位ms
|
||||
circular?: boolean; // 是否衔接播放
|
||||
current?: number; // 默认显示第几项
|
||||
name?: string; // 显示的属性
|
||||
borderRadius?: string; //轮播图圆角值,单位rpx
|
||||
bgColor?: string; // 背景颜色
|
||||
}>(),
|
||||
{
|
||||
content: {
|
||||
data: [],
|
||||
},
|
||||
mode: "round",
|
||||
indicatorPos: "bottomCenter",
|
||||
height: "340",
|
||||
effect3d: false,
|
||||
autoplay: true,
|
||||
interval: "2500",
|
||||
duration: 300,
|
||||
circular: true,
|
||||
current: 0,
|
||||
name: "image",
|
||||
borderRadius: "0",
|
||||
bgColor: "#f3f4f6",
|
||||
}
|
||||
);
|
||||
|
||||
const {getImageUrl} = useAppStore();
|
||||
|
||||
watchEffect(() => {
|
||||
try {
|
||||
const content = props?.content;
|
||||
const len = content?.data?.length;
|
||||
if (!len) return;
|
||||
for (let i = 0; i < len; i++) {
|
||||
const item = content.data[i];
|
||||
item.image = getImageUrl(item.image);
|
||||
}
|
||||
emit("change", 0);
|
||||
} catch (error) {
|
||||
//TODO handle the exception
|
||||
console.log("轮播图数据错误", error);
|
||||
}
|
||||
});
|
||||
|
||||
const lists = computed(() => props.content.data || []);
|
||||
const router = useRouter();
|
||||
|
||||
const handleClick = (index: number) => {
|
||||
const link = props.content.data[index]?.link;
|
||||
if (!link) return
|
||||
navigateTo(link);
|
||||
};
|
||||
|
||||
const handleChange = (index: number) => {
|
||||
emit("change", index);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<view>
|
||||
<u-popup v-model="showPopup" mode="bottom" border-radius="14" :mask-close-able="false">
|
||||
<view class="h-[1000rpx] p-[40rpx]">
|
||||
<view class="flex items-center">
|
||||
<image
|
||||
class="w-[100rpx] h-[100rpx] rounded"
|
||||
mode="heightFix"
|
||||
:src="logo"
|
||||
></image>
|
||||
<text class="text-3xl ml-5 font-bold">{{ title }}</text>
|
||||
</view>
|
||||
<view class="mt-5 text-muted">
|
||||
建议使用您的微信头像和昵称,以便获得更好的体验
|
||||
</view>
|
||||
<view class="mt-[30rpx]">
|
||||
<form @submit="handleSubmit">
|
||||
<u-form-item required label="头像" :labelWidth="120">
|
||||
<view class="flex-1">
|
||||
<avatar-upload v-model="avatar"></avatar-upload>
|
||||
</view>
|
||||
</u-form-item>
|
||||
<u-form-item required label="昵称" :labelWidth="120">
|
||||
<input
|
||||
class="flex-1 h-[60rpx]"
|
||||
name="nickname"
|
||||
type="nickname"
|
||||
placeholder="请输入昵称"
|
||||
/>
|
||||
</u-form-item>
|
||||
<view class="mt-[80rpx]">
|
||||
<button
|
||||
class="bg-primary rounded-full text-white text-lg h-[80rpx] leading-[80rpx]"
|
||||
hover-class="none"
|
||||
form-type="submit"
|
||||
>
|
||||
确定
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="flex justify-center mt-[60rpx]">
|
||||
<view class="text-muted" @click="showPopup = false">暂不登录</view>
|
||||
</view>
|
||||
</form>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean
|
||||
},
|
||||
logo: {
|
||||
type: String
|
||||
},
|
||||
title: {
|
||||
type: String
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:show', show: boolean): void
|
||||
(event: 'update', value: any): void
|
||||
}>()
|
||||
|
||||
const showPopup = computed({
|
||||
get() {
|
||||
return props.show
|
||||
},
|
||||
set(val) {
|
||||
emit('update:show', val)
|
||||
}
|
||||
})
|
||||
|
||||
const avatar = ref()
|
||||
|
||||
const handleSubmit = (e: any) => {
|
||||
const { nickname } = e.detail.value
|
||||
if (!avatar.value)
|
||||
return uni.$u.toast('请添加头像')
|
||||
if (!nickname)
|
||||
return uni.$u.toast('请输入昵称')
|
||||
emit('update', {
|
||||
avatar: avatar.value,
|
||||
nickname
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<navigator :url="`/pages/news_detail/news_detail?id=${newsId}`">
|
||||
<view class="news-card flex bg-white px-[20rpx] py-[32rpx]">
|
||||
<view class="mr-[20rpx]" v-if="item.image">
|
||||
<u-image :src="item.image" width="240" height="180"></u-image>
|
||||
</view>
|
||||
<view class="news-card-content flex flex-col justify-between flex-1">
|
||||
<view class="news-card-content-title text-base">{{ item.title }}</view>
|
||||
<view class="news-card-content-intro text-gray-400 text-sm mt-[16rpx]">
|
||||
{{ item.desc }}
|
||||
</view>
|
||||
|
||||
<view class="text-muted text-xs w-full flex justify-between mt-[12rpx]">
|
||||
<view>{{ item.create_time }}</view>
|
||||
<view class="flex items-center">
|
||||
<image
|
||||
src="/static/images/icon/icon_visit.png"
|
||||
class="w-[30rpx] h-[30rpx]"
|
||||
></image>
|
||||
<view class="ml-[10rpx]">{{ item.click }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</navigator>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
item: any
|
||||
newsId: number
|
||||
}>(),
|
||||
{
|
||||
item: {},
|
||||
newsId: ''
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.news-card {
|
||||
border-bottom: 1px solid #f8f8f8;
|
||||
&-content {
|
||||
&-title {
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
&-intro {
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<view
|
||||
class="page-status"
|
||||
v-if="status !== PageStatusEnum['NORMAL']"
|
||||
:class="{ 'page-status--fixed': fixed }"
|
||||
>
|
||||
<!-- Loading -->
|
||||
<template v-if="status === PageStatusEnum['LOADING']">
|
||||
<slot name="loading">
|
||||
<u-loading :size="60" mode="flower" />
|
||||
</slot>
|
||||
</template>
|
||||
<!-- Error -->
|
||||
<template v-if="status === PageStatusEnum['ERROR']">
|
||||
<slot name="error"></slot>
|
||||
</template>
|
||||
<!-- Empty -->
|
||||
<template v-if="status === PageStatusEnum['EMPTY']">
|
||||
<slot name="empty"></slot>
|
||||
</template>
|
||||
</view>
|
||||
<template v-else>
|
||||
<slot> </slot>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PageStatusEnum } from '@/enums/appEnums'
|
||||
|
||||
const props = defineProps({
|
||||
status: {
|
||||
type: String,
|
||||
default: PageStatusEnum['LOADING']
|
||||
},
|
||||
fixed: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-status {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #ffffff;
|
||||
&--fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 900;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<u-popup
|
||||
v-model="showPay"
|
||||
mode="bottom"
|
||||
safe-area-inset-bottom
|
||||
:mask-close-able="false"
|
||||
border-radius="14"
|
||||
closeable
|
||||
@close="handleClose"
|
||||
>
|
||||
<view class="h-[900rpx]">
|
||||
<page-status :status="popupStatus" :fixed="false">
|
||||
<template #error>
|
||||
<u-empty text="订单信息错误,无法查询到订单信息" mode="order"></u-empty>
|
||||
</template>
|
||||
<template #default>
|
||||
<view class="payment h-full w-full flex flex-col">
|
||||
<view class="header py-[50rpx] flex flex-col items-center">
|
||||
<price
|
||||
:content="payData.order_amount"
|
||||
mainSize="44rpx"
|
||||
minorSize="40rpx"
|
||||
fontWeight="500"
|
||||
color="#333"
|
||||
></price>
|
||||
</view>
|
||||
<view class="main flex-1 mx-[20rpx]">
|
||||
<view>
|
||||
<view class="payway-lists">
|
||||
<u-radio-group v-model="payWay" class="w-full">
|
||||
<view
|
||||
class="p-[20rpx] flex items-center w-full payway-item"
|
||||
v-for="(item, index) in payData.lists"
|
||||
:key="index"
|
||||
@click="selectPayWay(item.pay_way)"
|
||||
>
|
||||
<u-icon
|
||||
class="flex-none"
|
||||
:size="48"
|
||||
:name="item.icon"
|
||||
></u-icon>
|
||||
<view class="mx-[16rpx] flex-1">
|
||||
<view class="payway-item--name flex-1">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<view class="text-muted text-xs">{{
|
||||
item.extra
|
||||
}}</view>
|
||||
</view>
|
||||
|
||||
<u-radio activeColor="var(--color-primary)" class="mr-[-20rpx]" :name="item.pay_way">
|
||||
</u-radio>
|
||||
</view>
|
||||
</u-radio-group>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="submit-btn p-[20rpx]">
|
||||
<u-button
|
||||
@click="handlePay"
|
||||
shape="circle"
|
||||
type="primary"
|
||||
:loading="isLock"
|
||||
>
|
||||
立即支付
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</page-status>
|
||||
</view>
|
||||
</u-popup>
|
||||
|
||||
<u-popup
|
||||
class="pay-popup"
|
||||
v-model="showCheckPay"
|
||||
round
|
||||
mode="center"
|
||||
borderRadius="10"
|
||||
:maskCloseAble="false"
|
||||
>
|
||||
<view class="content bg-white w-[560rpx] p-[40rpx]">
|
||||
<view class="text-2xl font-medium text-center"> 支付确认 </view>
|
||||
<view class="pt-[30rpx] pb-[40rpx]">
|
||||
<view> 请在微信内完成支付,如果您已支付成功,请点击`已完成支付`按钮 </view>
|
||||
</view>
|
||||
<view class="flex">
|
||||
<view class="flex-1 mr-[20rpx]">
|
||||
<u-button
|
||||
shape="circle"
|
||||
type="primary"
|
||||
plain
|
||||
size="medium"
|
||||
hover-class="none"
|
||||
:customStyle="{ width: '100%' }"
|
||||
@click="queryPayResult(false)"
|
||||
>
|
||||
重新支付
|
||||
</u-button>
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<u-button
|
||||
shape="circle"
|
||||
type="primary"
|
||||
size="medium"
|
||||
hover-class="none"
|
||||
:customStyle="{ width: '100%' }"
|
||||
@click="queryPayResult()"
|
||||
>
|
||||
已完成支付
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { pay, PayWayEnum } from '@/utils/pay'
|
||||
import { getPayWay, prepay, getPayResult } from '@/api/pay'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useLockFn } from '@/hooks/useLockFn'
|
||||
import { series } from '@/utils/util'
|
||||
import { ClientEnum, PageStatusEnum, PayStatusEnum } from '@/enums/appEnums'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { client } from '@/utils/client'
|
||||
/*
|
||||
页面参数 orderId:订单id,from:订单来源
|
||||
*/
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
showCheck: {
|
||||
type: Boolean
|
||||
},
|
||||
// 订单id
|
||||
orderId: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
//订单来源
|
||||
from: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
//h5微信支付回跳路径,一般为拉起支付的页面路径
|
||||
redirect: {
|
||||
type: String
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:showCheck', 'update:show', 'close', 'success', 'fail'])
|
||||
|
||||
const payWay = ref()
|
||||
const popupStatus = ref(PageStatusEnum.LOADING)
|
||||
const payData = ref<any>({
|
||||
order_amount: '',
|
||||
lists: []
|
||||
})
|
||||
|
||||
const showCheckPay = computed({
|
||||
get() {
|
||||
return props.showCheck
|
||||
},
|
||||
set(value) {
|
||||
emit('update:showCheck', value)
|
||||
}
|
||||
})
|
||||
|
||||
const showPay = computed({
|
||||
get() {
|
||||
return props.show
|
||||
},
|
||||
set(value) {
|
||||
emit('update:show', value)
|
||||
}
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
showPay.value = false
|
||||
emit('close')
|
||||
}
|
||||
const getPayData = async () => {
|
||||
popupStatus.value = PageStatusEnum.LOADING
|
||||
try {
|
||||
payData.value = await getPayWay({
|
||||
order_id: props.orderId,
|
||||
from: props.from
|
||||
})
|
||||
popupStatus.value = PageStatusEnum.NORMAL
|
||||
const checkPay =
|
||||
payData.value.lists.find((item: any) => item.is_default) || payData.value.lists[0]
|
||||
payWay.value = checkPay?.pay_way
|
||||
} catch (error) {
|
||||
popupStatus.value = PageStatusEnum.ERROR
|
||||
}
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
const selectPayWay = (pay: number) => {
|
||||
payWay.value = pay
|
||||
}
|
||||
const payment = (() => {
|
||||
// 查询是否绑定微信
|
||||
const checkIsBindWx = async () => {
|
||||
if (
|
||||
userStore.userInfo.is_auth == 0 &&
|
||||
[ClientEnum.OA_WEIXIN, ClientEnum.MP_WEIXIN].includes(client) &&
|
||||
payWay.value == PayWayEnum.WECHAT
|
||||
) {
|
||||
const res: any = await uni.showModal({
|
||||
title: '温馨提示',
|
||||
content: '当前账号未绑定微信,无法完成支付',
|
||||
confirmText: '去绑定'
|
||||
})
|
||||
if (res.confirm) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user_set/user_set'
|
||||
})
|
||||
}
|
||||
return Promise.reject()
|
||||
}
|
||||
}
|
||||
|
||||
// 调用预支付
|
||||
const prepayTask = async () => {
|
||||
uni.showLoading({
|
||||
title: '正在支付中'
|
||||
})
|
||||
const data = await prepay({
|
||||
order_id: props.orderId,
|
||||
from: props.from,
|
||||
pay_way: payWay.value,
|
||||
redirect: props.redirect
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
//拉起支付
|
||||
const payTask = async (data: any) => {
|
||||
try {
|
||||
const res = await pay.payment(data.pay_way, data.config)
|
||||
return res
|
||||
} catch (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
return series(checkIsBindWx, prepayTask, payTask)
|
||||
})()
|
||||
const { isLock, lockFn: handlePay } = useLockFn(async () => {
|
||||
try {
|
||||
const res: PayStatusEnum = await payment()
|
||||
handlePayResult(res)
|
||||
uni.hideLoading()
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
console.log(error)
|
||||
}
|
||||
})
|
||||
|
||||
const handlePayResult = (status: PayStatusEnum) => {
|
||||
switch (status) {
|
||||
case PayStatusEnum.SUCCESS:
|
||||
emit('success')
|
||||
break
|
||||
case PayStatusEnum.FAIL:
|
||||
emit('fail')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const queryPayResult = async (confirm = true) => {
|
||||
const res = await getPayResult({
|
||||
order_id: props.orderId,
|
||||
from: props.from
|
||||
})
|
||||
|
||||
if (res.pay_status === 0) {
|
||||
if (confirm == true) {
|
||||
uni.$u.toast('您的订单还未支付,请重新支付')
|
||||
}
|
||||
showPay.value = true
|
||||
handlePayResult(PayStatusEnum.FAIL)
|
||||
} else {
|
||||
if (confirm == false) {
|
||||
uni.$u.toast('您的订单已经支付,请勿重新支付')
|
||||
}
|
||||
handlePayResult(PayStatusEnum.SUCCESS)
|
||||
}
|
||||
showCheckPay.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(value) => {
|
||||
if (value) {
|
||||
if (!props.orderId) {
|
||||
popupStatus.value = PageStatusEnum.ERROR
|
||||
return
|
||||
}
|
||||
getPayData()
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.payway-lists {
|
||||
.payway-item {
|
||||
border-bottom: 1px solid;
|
||||
@apply border-page;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<view class="price-container">
|
||||
<view
|
||||
:class="['price-wrap', { 'price-wrap--disabled': lineThrough }]"
|
||||
:style="{ color: color }"
|
||||
>
|
||||
<!-- Prefix -->
|
||||
<view class="fix-pre" :style="{ fontSize: minorSize }">
|
||||
<slot name="prefix">{{ prefix }}</slot>
|
||||
</view>
|
||||
|
||||
<!-- Content -->
|
||||
<view :style="{ 'font-weight': fontWeight }">
|
||||
<!-- Integer -->
|
||||
<text :style="{ fontSize: mainSize }">{{ integer }}</text>
|
||||
<!-- Decimals -->
|
||||
<text :style="{ fontSize: minorSize }">{{ decimals }}</text>
|
||||
</view>
|
||||
|
||||
<!-- Suffix -->
|
||||
<view class="fix-suf" :style="{ fontSize: minorSize }">
|
||||
<slot name="suffix">{{ suffix }}</slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* @description 价格展示,适用于有前后缀,小数样式不一
|
||||
* @property {String|Number} content 价格 (必填项)
|
||||
* @property {Number} prec 小数位 (默认: 2)
|
||||
* @property {Boolean} autoPrec 自动小数位【注:以prec为最大小数位】 (默认: true)
|
||||
* @property {String} color 颜色 (默认: 'unset')
|
||||
* @property {String} mainSize 主要内容字体大小 (默认: 46rpx)
|
||||
* @property {String} minorSize 主要内容字体大小 (默认: 32rpx)
|
||||
* @property {Boolean} lineThrough 贯穿线 (默认: false)
|
||||
* @property {String|Number} fontWeight 字重 (默认: normal)
|
||||
* @property {String} prefix 前缀 (默认: ¥)
|
||||
* @property {String} suffix 后缀
|
||||
* @example <price content="100" suffix="\/元" />
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import { formatPrice } from '@/utils/util'
|
||||
|
||||
/** Props Start **/
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
content: string | number // 标题
|
||||
prec?: number // 小数数量
|
||||
autoPrec?: boolean // 动态小数
|
||||
color?: string // 颜色
|
||||
mainSize?: string // 主要内容字体大小
|
||||
minorSize?: string // 次要内容字体大小
|
||||
lineThrough?: boolean // 贯穿线
|
||||
fontWeight?: string // 字重
|
||||
prefix?: string // 前缀
|
||||
suffix?: string // 后缀
|
||||
}>(),
|
||||
{
|
||||
content: '',
|
||||
prec: 2,
|
||||
autoPrec: true,
|
||||
color: '#FA8919',
|
||||
mainSize: '36rpx',
|
||||
minorSize: '28rpx',
|
||||
lineThrough: false,
|
||||
fontWeight: 'normal',
|
||||
prefix: '¥',
|
||||
suffix: ''
|
||||
}
|
||||
)
|
||||
/** Props End **/
|
||||
|
||||
/** Computed Start **/
|
||||
/**
|
||||
* @description 金额主体部分
|
||||
*/
|
||||
const integer = computed(() => {
|
||||
return formatPrice({
|
||||
price: props.content,
|
||||
take: 'int'
|
||||
})
|
||||
})
|
||||
/**
|
||||
* @description 金额小数部分
|
||||
*/
|
||||
const decimals = computed(() => {
|
||||
let decimals = formatPrice({
|
||||
price: props.content,
|
||||
take: 'dec',
|
||||
prec: props.prec
|
||||
})
|
||||
// 小数余十不能是 .10||.20||.30以此类推,
|
||||
decimals = decimals % 10 == 0 ? decimals.substr(0, decimals.length - 1) : decimals
|
||||
return props.autoPrec ? (decimals * 1 ? '.' + decimals : '') : props.prec ? '.' + decimals : ''
|
||||
})
|
||||
/** Computed End **/
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.price-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.price-wrap {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
&--disabled {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
display: block;
|
||||
content: '';
|
||||
height: 0.05em;
|
||||
background-color: currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<view
|
||||
:class="{ active, inactive: !active, tab: true }"
|
||||
:style="shouldShow ? '' : 'display: none;'"
|
||||
>
|
||||
<slot v-if="shouldRender"></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, provide, inject, watch, computed, onMounted, getCurrentInstance } from 'vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
dot?: boolean | string
|
||||
name?: boolean | string
|
||||
info?: any
|
||||
}>(),
|
||||
{
|
||||
dot: false,
|
||||
name: ''
|
||||
}
|
||||
)
|
||||
|
||||
const active = ref<boolean>(false)
|
||||
const shouldShow = ref<boolean>(false)
|
||||
const shouldRender = ref<boolean>(false)
|
||||
const inited = ref(undefined)
|
||||
|
||||
const updateTabs: any = inject('updateTabs')
|
||||
const handleChange: any = inject('handleChange')
|
||||
|
||||
const updateRender = (value) => {
|
||||
inited.value = inited.value || value
|
||||
active.value = value
|
||||
shouldRender.value = inited.value!
|
||||
shouldShow.value = value
|
||||
}
|
||||
const update = () => {
|
||||
if (updateTabs) {
|
||||
updateTabs()
|
||||
}
|
||||
}
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
console.log(instance)
|
||||
handleChange(instance?.props, updateRender)
|
||||
|
||||
onMounted(() => {
|
||||
update()
|
||||
})
|
||||
|
||||
const changeData = computed(() => {
|
||||
const { dot, info } = props
|
||||
return {
|
||||
dot,
|
||||
info
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => changeData.value,
|
||||
() => {
|
||||
update()
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => props.name,
|
||||
(val) => {
|
||||
update()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tab.active {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.tab.inactive {
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<u-tabbar
|
||||
v-if="showTabbar"
|
||||
v-model="current"
|
||||
v-bind="tabbarStyle"
|
||||
:list="tabbarList"
|
||||
@change="handleChange"
|
||||
:hide-tab-bar="true"
|
||||
></u-tabbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { navigateTo } from '@/utils/util'
|
||||
import { computed, ref } from 'vue'
|
||||
const current = ref()
|
||||
const appStore = useAppStore()
|
||||
const tabbarList = computed(() => {
|
||||
return appStore.getTabbarConfig
|
||||
?.filter((item: any) => item.is_show == 1)
|
||||
.map((item: any) => {
|
||||
return {
|
||||
iconPath: item.unselected,
|
||||
selectedIconPath: item.selected,
|
||||
text: item.name,
|
||||
link: item.link,
|
||||
pagePath: item.link.path
|
||||
}
|
||||
})
|
||||
})
|
||||
const showTabbar = computed(() => {
|
||||
const currentPages = getCurrentPages()
|
||||
const currentPage = currentPages[currentPages.length - 1]
|
||||
const current = tabbarList.value.findIndex((item: any) => {
|
||||
return item.pagePath === '/' + currentPage.route
|
||||
})
|
||||
return current >= 0
|
||||
})
|
||||
|
||||
const tabbarStyle = computed(() => ({
|
||||
activeColor: appStore.getStyleConfig.selected_color,
|
||||
inactiveColor: appStore.getStyleConfig.default_color
|
||||
}))
|
||||
|
||||
const nativeTabbar = [
|
||||
'/pages/index/index',
|
||||
'/pages/news/news',
|
||||
'/pages/user/user'
|
||||
]
|
||||
const handleChange = (index: number) => {
|
||||
const selectTab = tabbarList.value[index]
|
||||
const navigateType = nativeTabbar.includes(selectTab.link.path) ? 'switchTab' : 'reLaunch'
|
||||
navigateTo(selectTab.link, navigateType)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,72 @@
|
||||
import { reactive } from 'vue'
|
||||
|
||||
/**
|
||||
* @description 触碰屏幕钩子函数
|
||||
* @return { Function } 暴露钩子
|
||||
*/
|
||||
export function useTouch() {
|
||||
// 最小移动距离
|
||||
const MIN_DISTANCE = 10
|
||||
|
||||
const touch = reactive<any>({
|
||||
direction: '',
|
||||
deltaX: 0,
|
||||
deltaY: 0,
|
||||
offsetX: 0,
|
||||
offsetY: 0
|
||||
})
|
||||
|
||||
/**
|
||||
* @description 计算距离
|
||||
* @return { string } 空字符串
|
||||
*/
|
||||
const getDirection = (x: number, y: number) => {
|
||||
if (x > y && x > MIN_DISTANCE) {
|
||||
return 'horizontal'
|
||||
}
|
||||
if (y > x && y > MIN_DISTANCE) {
|
||||
return 'vertical'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 重置参数
|
||||
*/
|
||||
const resetTouchStatus = () => {
|
||||
touch.direction = ''
|
||||
touch.deltaX = 0
|
||||
touch.deltaY = 0
|
||||
touch.offsetX = 0
|
||||
touch.offsetY = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 触发
|
||||
*/
|
||||
const touchStart = (event: any) => {
|
||||
resetTouchStatus()
|
||||
const events = event.touches[0]
|
||||
touch.startX = events.clientX
|
||||
touch.startY = events.clientY
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 移动
|
||||
*/
|
||||
const touchMove = (event: any) => {
|
||||
const events = event.touches[0]
|
||||
touch.deltaX = events.clientX - touch.startX
|
||||
touch.deltaY = events.clientY - touch.startY
|
||||
touch.offsetX = Math.abs(touch.deltaX)
|
||||
touch.offsetY = Math.abs(touch.deltaY)
|
||||
touch.direction = touch.direction || getDirection(touch.offsetX, touch.offsetY)
|
||||
}
|
||||
|
||||
return {
|
||||
touch,
|
||||
resetTouchStatus,
|
||||
touchStart,
|
||||
touchMove
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
<template>
|
||||
<view class="tabs">
|
||||
<u-sticky :enable="isFixed" :bg-color="stickyBgColor" :offset-top="top" :h5-nav-height="0">
|
||||
<view
|
||||
:id="id"
|
||||
:style="{
|
||||
background: bgColor
|
||||
}"
|
||||
>
|
||||
<scroll-view
|
||||
:style="{ height: height + 'rpx' }"
|
||||
scroll-x
|
||||
class="scroll-view"
|
||||
:scroll-left="scrollLeft"
|
||||
scroll-with-animation
|
||||
>
|
||||
<view class="scroll-box" :class="{ 'tabs-scorll-flex': !isScroll }">
|
||||
<view
|
||||
v-for="(item, index) in list"
|
||||
class="tab-item line1"
|
||||
:id="'tab-item-' + index"
|
||||
:key="index"
|
||||
@tap="clickTab(index)"
|
||||
:style="[tabItemStyle(index)]"
|
||||
>
|
||||
<u-badge
|
||||
:count="item[count] || item['dot'] || 0"
|
||||
:offset="offset"
|
||||
size="mini"
|
||||
></u-badge>
|
||||
{{ item[name] || item['name'] }}
|
||||
</view>
|
||||
<view v-if="showBar" class="tab-bar" :style="[tabBarStyle]"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</u-sticky>
|
||||
<view
|
||||
class="tab-content"
|
||||
@touchstart="onTouchStart"
|
||||
@touchmove="onTouchMove"
|
||||
@touchcancel="onTouchEnd"
|
||||
@touchend="onTouchEnd"
|
||||
>
|
||||
<!-- <view class="tab-track" :class="{'tab-animated': animated}" :style="[trackStyle]"> -->
|
||||
<view>
|
||||
<slot></slot>
|
||||
</view>
|
||||
<!-- </view> -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getRect } from '@/utils/util'
|
||||
import {
|
||||
ref,
|
||||
reactive,
|
||||
computed,
|
||||
watch,
|
||||
provide,
|
||||
nextTick,
|
||||
onMounted,
|
||||
getCurrentInstance
|
||||
} from 'vue'
|
||||
import { useTouch } from './hooks/useTouch'
|
||||
|
||||
// Touch 钩子
|
||||
const { touch, resetTouchStatus, touchStart, touchMove } = useTouch()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'change', value: number): void
|
||||
}>()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
isScroll?: boolean // 导航菜单是否需要滚动,如只有2或者3个的时候,就不需要滚动了,此时使用flex平分tab的宽度
|
||||
current?: number | string // 当前活动tab的索引
|
||||
height?: number | string // 导航栏的高度和行高
|
||||
fontSize?: number | string // 字体大小
|
||||
duration?: number | string // 过渡动画时长, 单位ms
|
||||
activeColor?: number | string // 选中项的主题颜色
|
||||
inactiveColor?: number | string // 未选中项的颜色
|
||||
barWidth?: number | string // 菜单底部移动的bar的宽度,单位rpx
|
||||
barHeight?: number // 移动bar的高度
|
||||
gutter?: number | string // 单个tab的左或有内边距(左右相同)
|
||||
bgColor?: number | string // 导航栏的背景颜色
|
||||
name?: string // 读取传入的数组对象的属性(tab名称)
|
||||
count?: string // 读取传入的数组对象的属性(徽标数)
|
||||
offset?: number[] // 徽标数位置偏移
|
||||
bold?: boolean // 活动tab字体是否加粗
|
||||
activeItemStyle?: any // 当前活动tab item的样式
|
||||
showBar?: boolean // 是否显示底部的滑块
|
||||
barStyle?: any // 底部滑块的自定义样式
|
||||
itemWidth?: string // 标签的宽度
|
||||
isFixed?: boolean // 吸顶是否固定
|
||||
top?: number | string // 吸顶顶部距离
|
||||
stickyBgColor?: string // 吸顶颜色
|
||||
|
||||
swipeable?: boolean // 是否允许滑动切换
|
||||
// animated: boolean // 切换动画
|
||||
}>(),
|
||||
{
|
||||
isScroll: true,
|
||||
current: 0,
|
||||
height: 80,
|
||||
fontSize: 28,
|
||||
duration: 0.3,
|
||||
activeColor: 'var(--color-primary)',
|
||||
inactiveColor: '#333',
|
||||
barWidth: 40,
|
||||
barHeight: 4,
|
||||
gutter: 30,
|
||||
bgColor: '#FFFFFF',
|
||||
name: 'name',
|
||||
count: 'count',
|
||||
offset: [5, 20],
|
||||
bold: true,
|
||||
activeItemStyle: {},
|
||||
showBar: true,
|
||||
barStyle: {},
|
||||
itemWidth: 'auto',
|
||||
isFixed: false,
|
||||
top: 0,
|
||||
stickyBgColor: '#FFFFFF',
|
||||
|
||||
swipeable: true
|
||||
// animated: true
|
||||
}
|
||||
)
|
||||
|
||||
const list = ref<any>([])
|
||||
const childrens = ref<any>([])
|
||||
const scrollLeft = ref<number>(0) // 滚动scroll-view的左边滚动距离
|
||||
const tabQueryInfo = ref<any>([]) // 存放对tab菜单查询后的节点信息
|
||||
const componentWidth = ref<number>(0) // 屏幕宽度,单位为px
|
||||
const scrollBarLeft = ref<number>(0) // 移动bar需要通过translateX()移动的距离
|
||||
const parentLeft = ref<number>(0) // 父元素(tabs组件)到屏幕左边的距离
|
||||
const id = ref<string>('cu-tab') // id值
|
||||
const currentIndex = ref<any>(props.current)
|
||||
const barFirstTimeMove = ref<boolean>(true) // 滑块第一次移动时(页面刚生成时),无需动画,否则给人怪异的感觉
|
||||
const swiping = ref<boolean>(false)
|
||||
|
||||
//@ts-ignore
|
||||
const ctx = getCurrentInstance()
|
||||
|
||||
// 监听tab的变化,重新计算tab菜单的布局信息,因为实际使用中菜单可能是通过
|
||||
// 后台获取的(如新闻app顶部的菜单),获取返回需要一定时间,所以list变化时,重新获取布局信息
|
||||
watch(
|
||||
() => list.value,
|
||||
async (n, o) => {
|
||||
// list变动时,重制内部索引,否则可能导致超出数组边界的情况
|
||||
if (!barFirstTimeMove.value && n.length !== o.length) {
|
||||
currentIndex.value = 0
|
||||
}
|
||||
// 用$nextTick等待视图更新完毕后再计算tab的局部信息,否则可能因为tab还没生成就获取,就会有问题
|
||||
await nextTick()
|
||||
init()
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => props.current,
|
||||
(nVal, oVal) => {
|
||||
// 视图更新后再执行移动操作、
|
||||
nextTick(() => {
|
||||
currentIndex.value = nVal
|
||||
scrollByIndex()
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 移动bar的样式
|
||||
const tabBarStyle = computed(() => {
|
||||
const style = {
|
||||
width: props.barWidth + 'rpx',
|
||||
transform: `translate(${scrollBarLeft.value}px, -100%)`,
|
||||
// 滑块在页面渲染后第一次滑动时,无需动画效果
|
||||
'transition-duration': `${barFirstTimeMove.value ? 0 : props.duration}s`,
|
||||
'background-color': props.activeColor,
|
||||
height: props.barHeight + 'rpx',
|
||||
opacity: barFirstTimeMove.value ? 0 : 1,
|
||||
// 设置一个很大的值,它会自动取能用的最大值,不用高度的一半,是因为高度可能是单数,会有小数出现
|
||||
'border-radius': `${props.barHeight / 2}px`
|
||||
}
|
||||
Object.assign(style, props.barStyle)
|
||||
return style
|
||||
})
|
||||
// tab的样式
|
||||
const tabItemStyle = computed(() => {
|
||||
return (index) => {
|
||||
let style: any = {
|
||||
height: props.height + 'rpx',
|
||||
'line-height': props.height + 'rpx',
|
||||
'font-size': props.fontSize + 'rpx',
|
||||
padding: props.isScroll ? `0 ${props.gutter}rpx` : '',
|
||||
flex: props.isScroll ? 'auto' : '1',
|
||||
width: `${props.itemWidth}rpx`
|
||||
}
|
||||
// 字体加粗
|
||||
if (index == currentIndex.value && props.bold) style.fontWeight = 'bold'
|
||||
if (index == currentIndex.value) {
|
||||
style.color = props.activeColor
|
||||
// 给选中的tab item添加外部自定义的样式
|
||||
style = Object.assign(style, props.activeItemStyle)
|
||||
} else {
|
||||
style.color = props.inactiveColor
|
||||
}
|
||||
return style
|
||||
}
|
||||
})
|
||||
|
||||
// const trackStyle = computed(() => {
|
||||
// if (!props.animated) return ''
|
||||
// return {
|
||||
// left: -100 * currentIndex.value + '%',
|
||||
// 'transition-duration': props.duration + 's',
|
||||
// '-webkit-transition-duration': props.duration + 's',
|
||||
// }
|
||||
// })
|
||||
|
||||
const updateTabs = () => {
|
||||
list.value = childrens.value.map((item) => {
|
||||
const { name, dot, active, inited } = item.event
|
||||
const { updateRender } = item
|
||||
return {
|
||||
name,
|
||||
dot,
|
||||
active,
|
||||
inited,
|
||||
updateRender
|
||||
}
|
||||
})
|
||||
// nextTick(() => {
|
||||
// init()
|
||||
// })
|
||||
}
|
||||
|
||||
// 设置一个init方法,方便多处调用
|
||||
const init = async () => {
|
||||
// 获取tabs组件的尺寸信息
|
||||
const tabRect = await getRect('#' + id.value, false, ctx)
|
||||
// tabs组件距离屏幕左边的宽度
|
||||
parentLeft.value = tabRect.left
|
||||
// tabs组件的宽度
|
||||
componentWidth.value = tabRect.width
|
||||
getTabRect()
|
||||
}
|
||||
|
||||
// 点击某一个tab菜单
|
||||
const clickTab = (index) => {
|
||||
// 点击当前活动tab,不触发事件
|
||||
if (index == currentIndex.value) return
|
||||
nextTick(() => {
|
||||
currentIndex.value = index
|
||||
scrollByIndex()
|
||||
})
|
||||
// 发送事件给父组件
|
||||
emit('change', index)
|
||||
}
|
||||
|
||||
// 查询tab的布局信息
|
||||
const getTabRect = () => {
|
||||
// 创建节点查询
|
||||
const query: any = uni.createSelectorQuery().in(ctx)
|
||||
// 历遍所有tab,这里是执行了查询,最终使用exec()会一次性返回查询的数组结果
|
||||
for (let i = 0; i < list.value.length; i++) {
|
||||
// 只要size和rect两个参数
|
||||
query.select(`#tab-item-${i}`).fields({
|
||||
size: true,
|
||||
rect: true
|
||||
})
|
||||
}
|
||||
// 执行查询,一次性获取多个结果
|
||||
query.exec((res) => {
|
||||
tabQueryInfo.value = res
|
||||
// 初始化滚动条和移动bar的位置
|
||||
scrollByIndex()
|
||||
})
|
||||
}
|
||||
|
||||
// 滚动scroll-view,让活动的tab处于屏幕的中间位置
|
||||
const scrollByIndex = () => {
|
||||
// 当前活动tab的布局信息,有tab菜单的width和left(为元素左边界到父元素左边界的距离)等信息
|
||||
const tabInfo = tabQueryInfo.value[currentIndex.value]
|
||||
if (!tabInfo) return
|
||||
// 活动tab的宽度
|
||||
const tabWidth = tabInfo.width
|
||||
// 活动item的左边到tabs组件左边的距离,用item的left减去tabs的left
|
||||
const offsetLeft = tabInfo.left - parentLeft.value
|
||||
// 将活动的tabs-item移动到屏幕正中间,实际上是对scroll-view的移动
|
||||
const scrollLefts = offsetLeft - (componentWidth.value - tabWidth) / 2
|
||||
scrollLeft.value = scrollLefts < 0 ? 0 : scrollLefts
|
||||
// 当前活动item的中点点到左边的距离减去滑块宽度的一半,即可得到滑块所需的移动距离
|
||||
const left = tabInfo.left + tabInfo.width / 2 - parentLeft.value
|
||||
// 计算当前活跃item到组件左边的距离
|
||||
scrollBarLeft.value = left - uni.upx2px(props.barWidth) / 2
|
||||
// 第一次移动滑块的时候,barFirstTimeMove为true,放到延时中将其设置false
|
||||
// 延时是因为scrollBarLeft作用于computed计算时,需要一个过程需,否则导致出错
|
||||
if (barFirstTimeMove.value == true) {
|
||||
setTimeout(() => {
|
||||
barFirstTimeMove.value = false
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 更新子组件的显示
|
||||
childrens.value.forEach((item, ind) => {
|
||||
const active = ind === currentIndex.value
|
||||
if (active !== item.event.active || !item.event.inited) {
|
||||
item.updateRender(active)
|
||||
}
|
||||
})
|
||||
}
|
||||
// 子组件调用此函数而产生的事件通信
|
||||
const handleChange = (event, updateRender) => {
|
||||
childrens.value.push({ event: event, updateRender })
|
||||
}
|
||||
// 手指触摸
|
||||
const onTouchStart = (event) => {
|
||||
if (!props.swipeable) return
|
||||
swiping.value = true
|
||||
touchStart(event)
|
||||
}
|
||||
// 手指滑动
|
||||
const onTouchMove = (event) => {
|
||||
if (!props.swipeable || !swiping.value) return
|
||||
touchMove(event)
|
||||
}
|
||||
// 手指滑动结束
|
||||
const onTouchEnd = () => {
|
||||
if (!props.swipeable || !swiping.value) return
|
||||
const minSwipeDistance = 50
|
||||
if (touch.direction === 'horizontal' && touch.offsetX >= minSwipeDistance) {
|
||||
let index,
|
||||
len = list.value.length,
|
||||
curIndex = currentIndex.value
|
||||
if (touch.deltaX <= 0) {
|
||||
curIndex >= len - 1 ? (index = 0) : (index = curIndex + 1)
|
||||
} else {
|
||||
curIndex <= 0 ? (index = len - 1) : (index = curIndex - 1)
|
||||
}
|
||||
nextTick(() => {
|
||||
currentIndex.value = index
|
||||
scrollByIndex()
|
||||
})
|
||||
// 发送事件给父组件
|
||||
emit('change', index)
|
||||
}
|
||||
swiping.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateTabs()
|
||||
})
|
||||
|
||||
provide('handleChange', handleChange)
|
||||
provide('updateTabs', updateTabs)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* #ifndef APP-NVUE */
|
||||
::-webkit-scrollbar,
|
||||
::-webkit-scrollbar,
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
.scroll-box {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
/* #ifdef MP-TOUTIAO */
|
||||
white-space: nowrap;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.tab-fixed {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* #ifdef H5 */
|
||||
// 通过样式穿透,隐藏H5下,scroll-view下的滚动条
|
||||
scroll-view ::v-deep ::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
.scroll-view {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
position: relative;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-block;
|
||||
/* #endif */
|
||||
text-align: center;
|
||||
transition-property: background-color, color;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
position: absolute;
|
||||
bottom: 6rpx;
|
||||
}
|
||||
|
||||
.tabs-scorll-flex {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
// .tab-content {
|
||||
// overflow: hidden;
|
||||
// .tab-track {
|
||||
// position: relative;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// }
|
||||
// .tab-animated {
|
||||
// display: flex;
|
||||
// transition-property: left;
|
||||
// }
|
||||
// }
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<view
|
||||
class="banner translate-y-0"
|
||||
:class="{ 'px-[20rpx]': !isLargeScreen }"
|
||||
v-if="content.data.length && content.enabled"
|
||||
>
|
||||
<LSwiper
|
||||
:content="content"
|
||||
:height="isLargeScreen ? '1100' : '321'"
|
||||
:circular="true"
|
||||
:effect3d="false"
|
||||
:border-radius="isLargeScreen ? '0' : '14'"
|
||||
interval="7000"
|
||||
bgColor="transparent"
|
||||
@change="handleChange"
|
||||
></LSwiper>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LSwiper from '@/components/l-swiper/l-swiper.vue'
|
||||
import {useAppStore} from "@/stores/app";
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
isLargeScreen: {
|
||||
type: Boolean
|
||||
}
|
||||
})
|
||||
const {getImageUrl} = useAppStore();
|
||||
|
||||
const handleChange = (index: number) => {
|
||||
emit('change', getImageUrl(props['content'].data[index].bg))
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<view class="bg-white p-[30rpx] flex text-[#101010] font-medium text-lg">
|
||||
联系我们
|
||||
</view>
|
||||
<view
|
||||
class="customer-service bg-white flex flex-col justify-center items-center mx-[36rpx] mt-[30rpx] rounded-[20rpx] px-[20rpx] pb-[100rpx]"
|
||||
>
|
||||
<view
|
||||
class="w-full border-solid border-0 border-b border-[#f5f5f5] p-[30rpx] text-center text-[#101010] text-base font-medium">
|
||||
{{ content.title }}
|
||||
</view>
|
||||
|
||||
<view class="mt-[60rpx]">
|
||||
<!-- 这样渲染是为了能在小程序中长按识别二维码 -->
|
||||
<u-parse :html="richTxt"></u-parse>
|
||||
<!-- <u-image width="200" height="200" border-radius="10rpx" :src="getImageUrl(content.qrcode)"/>-->
|
||||
</view>
|
||||
<view v-if="content.remark" class="text-sm mt-[40rpx] font-medium">{{ content.remark }}</view>
|
||||
<view v-if="content.mobile" class="text-sm mt-[24rpx] flex flex-wrap">
|
||||
<!-- #ifdef H5 -->
|
||||
<a class="ml-[10rpx] phone text-primary underline" :href="'tel:' + content.mobile">
|
||||
{{ content.mobile }}
|
||||
</a>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 -->
|
||||
<view class="ml-[10rpx] phone text-primary underline" @click="handleCall">
|
||||
{{ content.mobile }}
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
<view v-if="content.time" class="text-muted text-sm mt-[30rpx]">
|
||||
服务时间:{{ content.time }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {useAppStore} from '@/stores/app'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const {getImageUrl} = useAppStore()
|
||||
|
||||
const richTxt = computed(() => {
|
||||
const src = getImageUrl(props.content.qrcode)
|
||||
return `<img src="${src}" style="width: 100px;height: 100px; border-radius: 8px" />`
|
||||
})
|
||||
|
||||
const handleCall = () => {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: String(props.content.mobile)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<view
|
||||
class="banner h-[200rpx] mx-[20rpx] mt-[20rpx] translate-y-0"
|
||||
v-if="showList.length && content.enabled"
|
||||
>
|
||||
<swiper
|
||||
class="swiper h-full"
|
||||
:indicator-dots="showList.length > 1"
|
||||
indicator-active-color="#4173ff"
|
||||
:autoplay="true"
|
||||
>
|
||||
<swiper-item
|
||||
v-for="(item, index) in showList"
|
||||
:key="index"
|
||||
@click="handleClick(item.link)"
|
||||
>
|
||||
<u-image
|
||||
mode="widthFix"
|
||||
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'
|
||||
import { navigateTo } from '@/utils/util'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
const handleClick = (link: any) => {
|
||||
navigateTo(link)
|
||||
}
|
||||
const { getImageUrl } = useAppStore()
|
||||
|
||||
const showList = computed(() => {
|
||||
return props.content.data?.filter((item: any) => item.is_show == '1') || []
|
||||
})
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="my-service bg-white mx-[20rpx] mt-[20rpx] rounded-lg p-[30rpx]">
|
||||
<div
|
||||
v-if="content.title"
|
||||
class="title font-medium text-lg"
|
||||
>
|
||||
<div>{{ content.title }}</div>
|
||||
</div>
|
||||
<div v-if="content.style == 1" class="grid grid-cols-4 gap-x-9 gap-y-7">
|
||||
<div
|
||||
v-for="(item, index) in showList"
|
||||
:key="index"
|
||||
class="flex flex-col items-center pt-[40rpx]"
|
||||
@click="handleClick(item.link)"
|
||||
>
|
||||
<u-image width="52" height="52" :src="getImageUrl(item.image)" alt="" />
|
||||
<div class="mt-[22rpx] text-sm">{{ item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="content.style == 2">
|
||||
<div
|
||||
v-for="(item, index) in showList"
|
||||
:key="index"
|
||||
class="flex items-center border-light border-solid border-0 border-b h-[100rpx] px-[24rpx]"
|
||||
@click="handleClick(item.link)"
|
||||
>
|
||||
<u-image width="48" height="48" :src="getImageUrl(item.image)" alt="" />
|
||||
<div class="ml-[20rpx] flex-1 text-sm">{{ 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'
|
||||
import { navigateTo } from '@/utils/util'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
const { getImageUrl } = useAppStore()
|
||||
const handleClick = (link: any) => {
|
||||
navigateTo(link)
|
||||
}
|
||||
|
||||
const showList = computed(() => {
|
||||
return props.content.data?.filter((item: any) => item.is_show == '1') || []
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="relative mx-[20rpx] mt-[20rpx]">
|
||||
<swiper
|
||||
class="py-[20rpx] bg-white rounded-lg"
|
||||
:style="{
|
||||
height: navList[0].length > content.per_line ? '288rpx' : '132rpx'
|
||||
}"
|
||||
:autoplay="false"
|
||||
:indicator-dots="false"
|
||||
@change="swiperChange"
|
||||
>
|
||||
<swiper-item v-for="(sItem, sIndex) in navList" :key="sIndex">
|
||||
<view class="nav" v-if="navList.length && content.enabled">
|
||||
<view
|
||||
class="grid grid-rows-auto gap-y-3 w-full"
|
||||
:style="{ 'grid-template-columns': `repeat(${content.per_line}, 1fr)` }"
|
||||
>
|
||||
<view
|
||||
v-for="(item, index) in sItem"
|
||||
:key="index"
|
||||
class="flex flex-col items-center"
|
||||
@click="handleClick(item.link)"
|
||||
>
|
||||
<u-image width="82" height="82" :src="getImageUrl(item.image)" alt=""/>
|
||||
<view class="mt-[14rpx] text-xs">{{ item.name }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, watch, computed} from 'vue'
|
||||
import {useAppStore} from '@/stores/app'
|
||||
import {navigateTo, sliceArray} from '@/utils/util'
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const {getImageUrl} = useAppStore()
|
||||
const swiperCurrent = ref<number>(0)
|
||||
const navList = ref<Record<string, any>>([])
|
||||
|
||||
const pagesNum = computed<number>(() => {
|
||||
return props.content.per_line * props.content.show_line
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.content.data,
|
||||
(val) => {
|
||||
const num = props.content.style === 1 ? val.length : pagesNum.value
|
||||
navList.value = sliceArray(val, num)
|
||||
console.log(navList.value)
|
||||
},
|
||||
{deep: true, immediate: true}
|
||||
)
|
||||
|
||||
const handleClick = (link: any) => {
|
||||
navigateTo(link)
|
||||
}
|
||||
|
||||
const swiperChange = (e: any) => {
|
||||
swiperCurrent.value = e.detail.current
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<!-- #ifndef H5 -->
|
||||
<u-sticky h5-nav-height="0" bg-color="transparent">
|
||||
<u-navbar
|
||||
:class="{ 'fixed top-0 z-10': isLargeScreen }"
|
||||
:is-back="false"
|
||||
:is-fixed="true"
|
||||
:title="metaData.title"
|
||||
:custom-title="metaData.title_type == 2"
|
||||
:border-bottom="false"
|
||||
:title-bold="true"
|
||||
:background="{ background: 'rgba(256,256, 256, 0)' }"
|
||||
:title-color="percent > 0.5 ? '#000' : metaData.text_color == 1 ? '#fff' : '#000'"
|
||||
>
|
||||
<template #default>
|
||||
<navigator
|
||||
url="/pages/search/search"
|
||||
class="mini-search"
|
||||
hover-class="none"
|
||||
:style="{ opacity: isLargeScreen ? 1 : percent }"
|
||||
>
|
||||
<u-icon name="search"></u-icon>
|
||||
</navigator>
|
||||
</template>
|
||||
<template #title>
|
||||
<image
|
||||
class="!h-[54rpx]"
|
||||
:src="metaData.title_img"
|
||||
mode="widthFix"
|
||||
></image>
|
||||
</template>
|
||||
</u-navbar>
|
||||
</u-sticky>
|
||||
<!-- #endif -->
|
||||
<navigator
|
||||
v-if="!isLargeScreen"
|
||||
url="/pages/search/search"
|
||||
class="px-[24rpx] mt-[24rpx] mb-[30rpx]"
|
||||
:style="{ opacity: 1 - percent }"
|
||||
hover-class="none"
|
||||
>
|
||||
<u-search
|
||||
placeholder="请输入关键词搜索"
|
||||
:height="72"
|
||||
:disabled="true"
|
||||
:show-action="false"
|
||||
bgColor="#ffffff"
|
||||
></u-search>
|
||||
</navigator>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
pageMeta: {
|
||||
type: Object,
|
||||
default: () => []
|
||||
},
|
||||
content: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
isLargeScreen: {
|
||||
type: Boolean
|
||||
},
|
||||
percent: {
|
||||
type: Number
|
||||
}
|
||||
})
|
||||
|
||||
const metaData: any = computed(() => {
|
||||
return props.pageMeta[0].content
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mini-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
margin-left: 20rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<view
|
||||
class="banner h-[200rpx] mx-[20rpx] mt-[20rpx] translate-y-0"
|
||||
v-if="showList.length && content.enabled"
|
||||
>
|
||||
<swiper
|
||||
class="swiper h-full"
|
||||
:indicator-dots="showList.length > 1"
|
||||
indicator-active-color="#4173ff"
|
||||
:autoplay="true"
|
||||
>
|
||||
<swiper-item
|
||||
v-for="(item, index) in showList"
|
||||
:key="index"
|
||||
@click="handleClick(item.link)"
|
||||
>
|
||||
<u-image
|
||||
mode="widthFix"
|
||||
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'
|
||||
import { navigateTo } from '@/utils/util'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
const handleClick = (link: any) => {
|
||||
navigateTo(link)
|
||||
}
|
||||
const { getImageUrl } = useAppStore()
|
||||
|
||||
const showList = computed(() => {
|
||||
return props.content.data?.filter((item: any) => item.is_show == '1') || []
|
||||
})
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<view class="user-info mb-[0rpx]">
|
||||
<!-- #ifndef H5 -->
|
||||
<u-sticky
|
||||
h5-nav-height="0"
|
||||
bg-color="transparent"
|
||||
>
|
||||
<u-navbar
|
||||
:is-back="false"
|
||||
:is-fixed="false"
|
||||
:title="metaData.title"
|
||||
:custom-title="metaData.title_type == 2"
|
||||
:border-bottom="false"
|
||||
:title-bold="true"
|
||||
:background="{ background: 'rgba(256,256, 256, 0)' }"
|
||||
:title-color="$theme.navColor"
|
||||
>
|
||||
<template #title>
|
||||
<image
|
||||
class="!h-[54rpx]"
|
||||
:src="metaData.title_img"
|
||||
mode="widthFix"
|
||||
></image>
|
||||
</template>
|
||||
</u-navbar>
|
||||
</u-sticky>
|
||||
<!-- #endif -->
|
||||
<view class="flex items-center justify-between px-[50rpx] pb-[50rpx] pt-[40rpx]">
|
||||
<view
|
||||
v-if="isLogin"
|
||||
class="flex items-center"
|
||||
@click="navigateTo('/pages/user_data/user_data')"
|
||||
>
|
||||
<u-avatar :src="user.avatar" :size="120"></u-avatar>
|
||||
<view class="text-white ml-[20rpx]">
|
||||
<view class="text-2xl">{{ user.nickname }}</view>
|
||||
<view class="text-xs mt-[18rpx]" @click.stop="copy(user.account)">
|
||||
账号:{{ user.account }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<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>
|
||||
<view class="text-white text-3xl ml-[20rpx]">未登录</view>
|
||||
</navigator>
|
||||
<navigator v-if="isLogin" hover-class="none" url="/pages/user_set/user_set">
|
||||
<u-icon name="setting" color="#fff" :size="48"></u-icon>
|
||||
</navigator>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useCopy } from '@/hooks/useCopy'
|
||||
import {computed} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
pageMeta: {
|
||||
type: Object,
|
||||
default: () => []
|
||||
},
|
||||
content: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
user: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
isLogin: {
|
||||
type: Boolean
|
||||
}
|
||||
})
|
||||
const { copy } = useCopy()
|
||||
|
||||
const metaData: any = computed(() => {
|
||||
return props.pageMeta[0].content
|
||||
})
|
||||
|
||||
const navigateTo = (url: string) => {
|
||||
uni.navigateTo({
|
||||
url
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-info {
|
||||
background: url(../../../static/images/user/my_topbg.png),
|
||||
linear-gradient(90deg, $u-type-primary, $u-minor-color);
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom;
|
||||
background-size: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { isDevMode } from "@/utils/env";
|
||||
const envBaseUrl = import.meta.env.VITE_APP_BASE_URL || "";
|
||||
|
||||
let baseUrl = `${envBaseUrl}/`;
|
||||
|
||||
/*
|
||||
* 微信小程序在`VITE_APP_BASE_URL`存在或`dev`模式下
|
||||
* 使用`VITE_APP_BASE_URL`的值
|
||||
* 其他情况使用`[baseUrl]`,方便服务端替换
|
||||
*/
|
||||
|
||||
//#ifdef MP-WEIXIN
|
||||
baseUrl = isDevMode() || envBaseUrl ? baseUrl : "[baseUrl]";
|
||||
//#endif
|
||||
|
||||
const config = {
|
||||
version: "1.9.0", //版本号
|
||||
baseUrl, //请求接口域名
|
||||
urlPrefix: "api", //请求默认前缀
|
||||
timeout: 60 * 1000, //请求超时时长
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,5 @@
|
||||
//菜单主题类型
|
||||
export enum AgreementEnum {
|
||||
PRIVACY = 'privacy',
|
||||
SERVICE = 'service'
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//菜单主题类型
|
||||
export enum ThemeEnum {
|
||||
LIGHT = 'light',
|
||||
DARK = 'dark'
|
||||
}
|
||||
|
||||
// 客户端
|
||||
export enum ClientEnum {
|
||||
MP_WEIXIN = 1, // 微信-小程序
|
||||
OA_WEIXIN = 2, // 微信-公众号
|
||||
H5 = 3, // H5
|
||||
IOS = 5, //苹果
|
||||
ANDROID = 6 //安卓
|
||||
}
|
||||
|
||||
export enum SMSEnum {
|
||||
LOGIN = 'YZMDL',
|
||||
BIND_MOBILE = 'BDSJHM',
|
||||
CHANGE_MOBILE = 'BGSJHM',
|
||||
FIND_PASSWORD = 'ZHDLMM'
|
||||
}
|
||||
|
||||
export enum SearchTypeEnum {
|
||||
HISTORY = 'history'
|
||||
}
|
||||
|
||||
// 用户资料
|
||||
export enum FieldType {
|
||||
NONE = '',
|
||||
AVATAR = 'avatar',
|
||||
USERNAME = 'account',
|
||||
NICKNAME = 'nickname',
|
||||
SEX = 'sex'
|
||||
}
|
||||
|
||||
// 支付结果
|
||||
export enum PayStatusEnum {
|
||||
SUCCESS = 'success',
|
||||
FAIL = 'fail',
|
||||
PENDING = 'pending'
|
||||
}
|
||||
|
||||
// 页面状态
|
||||
export enum PageStatusEnum {
|
||||
LOADING = 'loading', // 加载中
|
||||
NORMAL = 'normal', // 正常
|
||||
ERROR = 'error', // 异常
|
||||
EMPTY = 'empty' // 为空
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// 本地缓冲key
|
||||
|
||||
//token
|
||||
export const TOKEN_KEY = 'token'
|
||||
|
||||
// 搜索历史记录
|
||||
export const HISTORY = 'history'
|
||||
|
||||
export const BACK_URL = 'back_url'
|
||||
|
||||
export const PAY_STATUS_EVENT = 'event:payStatus'
|
||||
@@ -0,0 +1,22 @@
|
||||
export enum ContentTypeEnum {
|
||||
// json
|
||||
JSON = 'application/json;charset=UTF-8',
|
||||
// form-data 上传资源(图片,视频)
|
||||
FORM_DATA = 'multipart/form-data;charset=UTF-8'
|
||||
}
|
||||
|
||||
export enum RequestMethodsEnum {
|
||||
GET = 'GET',
|
||||
POST = 'POST'
|
||||
}
|
||||
|
||||
export enum RequestCodeEnum {
|
||||
SUCCESS = 1, //成功
|
||||
FAILED = 0, // 失败
|
||||
TOKEN_INVALID = -1 // TOKEN参数无效
|
||||
}
|
||||
|
||||
export enum RequestErrMsgEnum {
|
||||
ABORT = 'request:fail abort',
|
||||
TIMEOUT = 'request:fail timeout'
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export function useCopy() {
|
||||
const copy = (text: string) => {
|
||||
uni.setClipboardData({
|
||||
data: String(text)
|
||||
})
|
||||
}
|
||||
return {
|
||||
copy
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createSSRApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import plugins from './plugins'
|
||||
import router from './router'
|
||||
import './styles/index.scss'
|
||||
import { setupMixin } from './mixins'
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
setupMixin(app)
|
||||
app.use(plugins)
|
||||
app.use(router)
|
||||
return {
|
||||
app
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"name" : "",
|
||||
"appid" : "",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
"usingComponents" : true,
|
||||
"nvueStyleCompiler" : "uni-app",
|
||||
"compilerVersion" : 3,
|
||||
"splashscreen" : {
|
||||
"alwaysShowBeforeRender" : true,
|
||||
"waiting" : true,
|
||||
"autoclose" : true,
|
||||
"delay" : 0
|
||||
},
|
||||
/* 模块配置 */
|
||||
"modules" : {},
|
||||
/* 应用发布信息 */
|
||||
"distribute" : {
|
||||
/* android打包配置 */
|
||||
"android" : {
|
||||
"permissions" : [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
/* ios打包配置 */
|
||||
"ios" : {},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs" : {}
|
||||
}
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "wx386a75e518b38935",
|
||||
"setting" : {
|
||||
"urlCheck" : false,
|
||||
"es6" : true,
|
||||
"minified" : true
|
||||
},
|
||||
"__usePrivacyCheck__": true,
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-baidu" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-toutiao" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"uniStatistics" : {
|
||||
"enable" : false
|
||||
},
|
||||
"vueVersion" : "3",
|
||||
"h5" : {
|
||||
"router" : {
|
||||
"mode" : "history",
|
||||
"base" : "/mobile/"
|
||||
},
|
||||
"title" : "加载中"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { App } from 'vue'
|
||||
import theme from './theme'
|
||||
export function setupMixin(app: App) {
|
||||
app.mixin(theme)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
$theme() {
|
||||
const themeStore = useThemeStore()
|
||||
const appStore = useAppStore()
|
||||
return {
|
||||
primaryColor: themeStore.primaryColor,
|
||||
pageStyle: themeStore.vars,
|
||||
navColor: themeStore.navColor,
|
||||
navBgColor: themeStore.navBgColor,
|
||||
title: appStore.getWebsiteConfig.shop_name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<view class="h-screen flex flex-col justify-center items-center">
|
||||
<view>
|
||||
<u-empty text="对不起,您访问的页面不存在" mode="data"></u-empty>
|
||||
</view>
|
||||
<view class="w-full px-[100rpx] mt-[40rpx]">
|
||||
<router-navigate
|
||||
class="bg-primary rounded-full text-btn-text leading-[80rpx] text-center"
|
||||
to="/"
|
||||
nav-type="reLaunch"
|
||||
>
|
||||
返回首页
|
||||
</router-navigate>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<view class="recharge p-[20rpx]">
|
||||
<view class="bg-white rounded-[14rpx] p-[40rpx]">
|
||||
<view class="text-content">充值金额</view>
|
||||
<view class="border-0 border-b border-solid border-light">
|
||||
<input
|
||||
v-model="money"
|
||||
class="text-[60rpx] h-[60rpx] py-[24rpx]"
|
||||
placeholder="0.00"
|
||||
type="digit"
|
||||
/>
|
||||
</view>
|
||||
<view class="mt-[20rpx] text-xs text-muted">
|
||||
当前可用余额
|
||||
<text class="text-primary"> {{ wallet.user_money }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-[40rpx]">
|
||||
<u-button :loading="isLock" type="primary" shape="circle" @click="rechargeLock">
|
||||
立即充值
|
||||
</u-button>
|
||||
</view>
|
||||
<view class="flex justify-center m-[60rpx]">
|
||||
<navigator url="/packages/pages/recharge_record/recharge_record" hover-class="none">
|
||||
<text class="text-content text-sm">充值记录</text>
|
||||
</navigator>
|
||||
</view>
|
||||
<payment
|
||||
v-model:show="payState.showPay"
|
||||
v-model:show-check="payState.showCheck"
|
||||
:order-id="payState.orderId"
|
||||
:from="payState.from"
|
||||
:redirect="payState.redirect"
|
||||
@success="handlePaySuccess"
|
||||
@fail="handlePayFail"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { recharge, rechargeConfig } from '@/api/recharge'
|
||||
import { useLockFn } from '@/hooks/useLockFn'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import { reactive, ref } from 'vue'
|
||||
const money = ref('')
|
||||
|
||||
const payState = reactive({
|
||||
orderId: '',
|
||||
from: '',
|
||||
showPay: false,
|
||||
showCheck: false,
|
||||
redirect: '/packages/pages/recharge/recharge'
|
||||
})
|
||||
const wallet = reactive({
|
||||
user_money: '',
|
||||
min_amount: 0
|
||||
})
|
||||
|
||||
const { isLock, lockFn: rechargeLock } = useLockFn(async () => {
|
||||
const minNum = wallet.min_amount
|
||||
if (!money.value) return uni.$u.toast('请输入充值金额')
|
||||
if (minNum == 0 && Number(money.value) == minNum) {
|
||||
return uni.$u.toast(`充值金额必须大于0`)
|
||||
}
|
||||
if (Number(money.value) < minNum) return uni.$u.toast(`最低充值金额${minNum}`)
|
||||
const data = await recharge({
|
||||
money: money.value
|
||||
})
|
||||
payState.orderId = data.order_id
|
||||
payState.from = data.from
|
||||
payState.showPay = true
|
||||
})
|
||||
|
||||
const handlePaySuccess = async () => {
|
||||
payState.showPay = false
|
||||
payState.showCheck = false
|
||||
uni.navigateTo({
|
||||
url: `/pages/payment_result/payment_result?id=${payState.orderId}&from=${payState.from}`
|
||||
})
|
||||
}
|
||||
|
||||
const handlePayFail = async () => {
|
||||
uni.$u.toast('支付失败')
|
||||
}
|
||||
|
||||
const getWallet = async () => {
|
||||
const data = await rechargeConfig()
|
||||
Object.assign(wallet, data)
|
||||
}
|
||||
|
||||
onLoad((options: any) => {
|
||||
// h5支付用于弹起手动确认支付弹窗
|
||||
if (options?.checkPay) {
|
||||
payState.orderId = options.id
|
||||
payState.from = options.from
|
||||
payState.showCheck = true
|
||||
}
|
||||
})
|
||||
onShow(() => {
|
||||
getWallet()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<z-paging
|
||||
ref="paging"
|
||||
v-model="dataList"
|
||||
@query="queryList"
|
||||
:show-loading-more-when-reload="true"
|
||||
>
|
||||
<view class="pt-2.5">
|
||||
<view
|
||||
v-for="item in dataList"
|
||||
:key="item.id"
|
||||
class="bg-white border-solid border-b border-0 border-light px-[26rpx] py-[24rpx]"
|
||||
>
|
||||
<view class="flex justify-between">
|
||||
<view class="mr-2">{{ item.tips }}</view>
|
||||
<view class="text-lg text-primary"> +{{ item.order_amount }} </view>
|
||||
</view>
|
||||
<view class="text-sm text-muted mr-1">{{ item.create_time }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</z-paging>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef } from 'vue'
|
||||
import { rechargeRecord } from '@/api/recharge'
|
||||
|
||||
const paging = shallowRef()
|
||||
const dataList = ref<any[]>([])
|
||||
|
||||
const queryList = async (pageNo: number, pageSize: number) => {
|
||||
try {
|
||||
const data = await rechargeRecord({
|
||||
page_no: pageNo,
|
||||
page_size: pageSize
|
||||
})
|
||||
paging.value.complete(data.lists)
|
||||
} catch (error) {
|
||||
paging.value.complete(false)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<z-paging
|
||||
ref="paging"
|
||||
v-model="dataList"
|
||||
@query="queryList"
|
||||
:show-loading-more-when-reload="true"
|
||||
>
|
||||
<view class="user-wallet">
|
||||
<view class="p-[20rpx]">
|
||||
<view
|
||||
class="bg-primary rounded-[14rpx] flex items-center justify-between pl-[44rpx] py-[54rpx] text-white"
|
||||
>
|
||||
<view>
|
||||
<view class="text-sm">钱包余额</view>
|
||||
<view class="text-[60rpx]">{{ wallet.user_money }}</view>
|
||||
</view>
|
||||
<navigator
|
||||
v-if="wallet.status"
|
||||
url="/packages/pages/recharge/recharge"
|
||||
hover-class="none"
|
||||
>
|
||||
<view class="text-primary px-[30rpx] py-[15rpx] bg-white rounded-l-full">
|
||||
去充值
|
||||
</view>
|
||||
</navigator>
|
||||
</view>
|
||||
</view>
|
||||
<u-tabs
|
||||
:list="tabList"
|
||||
:is-scroll="false"
|
||||
v-model="current"
|
||||
activeColor="var(--color-primary)"
|
||||
@change="changeType"
|
||||
></u-tabs>
|
||||
|
||||
<view class="pt-2.5">
|
||||
<view
|
||||
v-for="item in dataList"
|
||||
:key="item.id"
|
||||
class="bg-white border-solid border-b border-0 border-light px-[26rpx] py-[24rpx]"
|
||||
>
|
||||
<view class="flex justify-between">
|
||||
<view class="mr-2">{{ item.type_desc }}</view>
|
||||
<view
|
||||
class="text-lg"
|
||||
:class="{
|
||||
'text-primary': item.action == 1
|
||||
}"
|
||||
>
|
||||
{{ item.change_amount_desc }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="text-sm text-muted mr-1">{{ item.create_time }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</z-paging>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef } from 'vue'
|
||||
import { accountLog } from '@/api/user'
|
||||
import { rechargeConfig } from '@/api/recharge'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
const tabList = ref([
|
||||
{
|
||||
name: '全部',
|
||||
type: ''
|
||||
},
|
||||
{
|
||||
name: '收入',
|
||||
type: 1
|
||||
},
|
||||
{
|
||||
name: '支出',
|
||||
type: 2
|
||||
}
|
||||
])
|
||||
const paging = shallowRef()
|
||||
const dataList = ref<any[]>([])
|
||||
const current = ref(0)
|
||||
|
||||
const changeType = (index: number) => {
|
||||
current.value = index
|
||||
paging.value.reload()
|
||||
}
|
||||
|
||||
const queryList = async (pageNo: number, pageSize: number) => {
|
||||
try {
|
||||
const action = tabList.value[current.value].type
|
||||
const data = await accountLog({
|
||||
action,
|
||||
type: 'um',
|
||||
page_no: pageNo,
|
||||
page_size: pageSize
|
||||
})
|
||||
paging.value.complete(data.lists)
|
||||
} catch (error) {
|
||||
paging.value.complete(false)
|
||||
}
|
||||
}
|
||||
|
||||
const wallet = ref<any>({})
|
||||
const getWallet = async () => {
|
||||
wallet.value = await rechargeConfig()
|
||||
}
|
||||
onShow(() => {
|
||||
getWallet()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,247 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
// #ifndef H5
|
||||
"navigationStyle": "custom",
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
"navigationBarTitleText": "首页"
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/news/news",
|
||||
"style": {
|
||||
// #ifndef H5
|
||||
"navigationStyle": "custom",
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
"navigationBarTitleText": "资讯",
|
||||
// #endif
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/user",
|
||||
"style": {
|
||||
// #ifndef H5
|
||||
"navigationStyle": "custom",
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
"navigationBarTitleText": "个人中心"
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/login",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录"
|
||||
},
|
||||
"meta": {
|
||||
"white": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/register/register",
|
||||
"style": {
|
||||
"navigationBarTitleText": "注册"
|
||||
},
|
||||
"meta": {
|
||||
"white": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/forget_pwd/forget_pwd",
|
||||
"style": {
|
||||
"navigationBarTitleText": "忘记密码"
|
||||
},
|
||||
"meta": {
|
||||
"white": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/customer_service/customer_service",
|
||||
"style": {
|
||||
"navigationBarTitleText": "联系客服"
|
||||
},
|
||||
"meta": {
|
||||
"white": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/news_detail/news_detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user_set/user_set",
|
||||
"style": {
|
||||
"navigationBarTitleText": "个人设置"
|
||||
},
|
||||
"meta": {
|
||||
"auth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/collection/collection",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的收藏"
|
||||
},
|
||||
"meta": {
|
||||
"auth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/as_us/as_us",
|
||||
"style": {
|
||||
"navigationBarTitleText": "关于我们"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/agreement/agreement",
|
||||
"style": {
|
||||
"navigationBarTitleText": "协议"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/change_password/change_password",
|
||||
"style": {
|
||||
"navigationBarTitleText": "修改密码"
|
||||
},
|
||||
"meta": {
|
||||
"auth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user_data/user_data",
|
||||
"style": {
|
||||
"navigationBarTitleText": "个人资料"
|
||||
},
|
||||
"meta": {
|
||||
"auth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/search/search",
|
||||
"style": {
|
||||
"navigationBarTitleText": "搜索"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/webview/webview"
|
||||
},
|
||||
{
|
||||
"path": "pages/bind_mobile/bind_mobile",
|
||||
"style": {
|
||||
"navigationBarTitleText": "绑定手机号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/empty/empty",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/payment_result/payment_result",
|
||||
"style": {
|
||||
"navigationBarTitleText": "支付结果"
|
||||
},
|
||||
"meta": {
|
||||
"auth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "uni_modules/vk-uview-ui/components/u-avatar-cropper/u-avatar-cropper",
|
||||
"style": {
|
||||
"navigationBarTitleText": "头像裁剪",
|
||||
"navigationBarBackgroundColor": "#000000"
|
||||
}
|
||||
}
|
||||
],
|
||||
"subPackages": [{
|
||||
"root": "packages",
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/404/404",
|
||||
"style": {
|
||||
"navigationBarTitleText": "404"
|
||||
},
|
||||
"name": "404",
|
||||
"meta": {
|
||||
"white": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user_wallet/user_wallet",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的钱包"
|
||||
},
|
||||
"meta": {
|
||||
"auth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/recharge/recharge",
|
||||
"style": {
|
||||
"navigationBarTitleText": "充值"
|
||||
},
|
||||
"meta": {
|
||||
"auth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/recharge_record/recharge_record",
|
||||
"style": {
|
||||
"navigationBarTitleText": "充值记录"
|
||||
},
|
||||
"meta": {
|
||||
"auth": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "商城",
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"backgroundColor": "#F8F8F8",
|
||||
"h5": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
"tabBar": {
|
||||
"custom": true,
|
||||
"iconWidth": "20px",
|
||||
"list": [
|
||||
{
|
||||
"iconPath": "static/images/tabbar/home.png",
|
||||
"selectedIconPath": "static/images/tabbar/home_s.png",
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
"iconPath": "static/images/tabbar/news.png",
|
||||
"selectedIconPath": "static/images/tabbar/news_s.png",
|
||||
"pagePath": "pages/news/news",
|
||||
"text": "资讯"
|
||||
},
|
||||
{
|
||||
"iconPath": "static/images/tabbar/user.png",
|
||||
"selectedIconPath": "static/images/tabbar/user_s.png",
|
||||
"pagePath": "pages/user/user",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
},
|
||||
"easycom": {
|
||||
"custom": {
|
||||
"router-navigate": "uniapp-router-next/components/router-navigate/router-navigate.vue",
|
||||
"^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)": "z-paging/components/z-paging$1/z-paging$1.vue",
|
||||
"^w-(.*)": "@/components/widgets/$1/$1.vue"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<view class="p-[30rpx]">
|
||||
<u-parse :html="agreementContent"></u-parse>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getPolicy } from '@/api/app'
|
||||
|
||||
let agreementType = ref('') // 协议类型
|
||||
const agreementContent = ref('') // 协议内容
|
||||
|
||||
const getData = async (type) => {
|
||||
const res = await getPolicy({ type })
|
||||
agreementContent.value = res.content
|
||||
uni.setNavigationBarTitle({
|
||||
title: String(res.title)
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((options: any) => {
|
||||
if (options.type) {
|
||||
agreementType = options.type
|
||||
getData(agreementType)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<view class="as-us flex flex-1 flex-col items-center">
|
||||
<image :src="appStore.getWebsiteConfig.shop_logo" mode="" class="img"></image>
|
||||
<view class="text-content mt-[20rpx]">当前版本{{ appStore.config.version }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/stores/app'
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.as-us {
|
||||
.img {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 20rpx;
|
||||
margin-top: 96rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<view class="bg-white min-h-full flex flex-col items-center px-[40rpx] pt-[40rpx] box-border">
|
||||
<view class="w-full">
|
||||
<view class="text-2xl font-medium mb-[60rpx]">绑定手机号</view>
|
||||
<view
|
||||
class="px-[18rpx] border border-solid border-lightc border-light rounded-[10rpx] h-[100rpx] items-center flex"
|
||||
>
|
||||
<u-input
|
||||
class="flex-1"
|
||||
v-model="formData.mobile"
|
||||
:border="false"
|
||||
placeholder="请输入手机号码"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
class="px-[18rpx] border border-solid border-lightc border-light rounded-[10rpx] h-[100rpx] items-center flex mt-[40rpx]"
|
||||
>
|
||||
<u-input
|
||||
class="flex-1"
|
||||
v-model="formData.code"
|
||||
placeholder="请输入验证码"
|
||||
:border="false"
|
||||
/>
|
||||
|
||||
<view
|
||||
class="border-l border-solid border-0 border-light pl-3 leading-4 ml-3 w-[180rpx]"
|
||||
@click="sendSms"
|
||||
>
|
||||
<u-verification-code
|
||||
ref="uCodeRef"
|
||||
:seconds="60"
|
||||
@change="codeChange"
|
||||
change-text="x秒"
|
||||
/>
|
||||
<text :class="formData.mobile ? 'text-primary' : 'text-muted'">
|
||||
{{ codeTips }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-[40rpx]">
|
||||
<u-button
|
||||
type="primary"
|
||||
hover-class="none"
|
||||
:customStyle="{
|
||||
height: '100rpx',
|
||||
opacity:
|
||||
formData.mobile && formData.code
|
||||
? '1'
|
||||
: '0.5'
|
||||
}"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
确定
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { userBindMobile } from '@/api/user'
|
||||
import { smsSend } from '@/api/app'
|
||||
import { SMSEnum } from '@/enums/appEnums'
|
||||
import { reactive, ref, shallowRef } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
const uCodeRef = shallowRef()
|
||||
const codeTips = ref('')
|
||||
|
||||
const userStore = useUserStore()
|
||||
const codeChange = (text: string) => {
|
||||
codeTips.value = text
|
||||
}
|
||||
|
||||
const formData = reactive({
|
||||
type: 'bind',
|
||||
mobile: '',
|
||||
code: ''
|
||||
})
|
||||
const sendSms = async () => {
|
||||
if (!formData.mobile) return uni.$u.toast('请输入手机号码')
|
||||
if (uCodeRef.value?.canGetCode) {
|
||||
await smsSend({
|
||||
scene: SMSEnum.BIND_MOBILE,
|
||||
mobile: formData.mobile
|
||||
})
|
||||
uni.$u.toast('发送成功')
|
||||
uCodeRef.value?.start()
|
||||
}
|
||||
}
|
||||
const handleConfirm = async () => {
|
||||
if (!formData.mobile) return uni.$u.toast('请输入手机号码')
|
||||
if (!formData.code) return uni.$u.toast('请输入验证码')
|
||||
await userBindMobile(formData, { token: userStore.temToken })
|
||||
uni.$u.toast('绑定成功')
|
||||
userStore.login(userStore.temToken!)
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
page {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<view
|
||||
class="register bg-white min-h-full flex flex-col items-center px-[40rpx] pt-[100rpx] box-border"
|
||||
>
|
||||
<view class="w-full">
|
||||
<view class="text-2xl font-medium mb-[60rpx]">
|
||||
{{ type == 'set' ? '设置登录密码' : '修改登录密码' }}
|
||||
</view>
|
||||
<u-form borderBottom :label-width="150">
|
||||
<u-form-item label="原密码" borderBottom v-if="type != 'set'">
|
||||
<u-input
|
||||
class="flex-1"
|
||||
type="password"
|
||||
v-model="formData.old_password"
|
||||
:border="false"
|
||||
placeholder="请输入原来的密码"
|
||||
/>
|
||||
</u-form-item>
|
||||
<u-form-item label="新密码" borderBottom>
|
||||
<u-input
|
||||
class="flex-1"
|
||||
type="password"
|
||||
v-model="formData.password"
|
||||
placeholder="6-20位数字+字母或符号组合"
|
||||
:border="false"
|
||||
/>
|
||||
</u-form-item>
|
||||
<u-form-item label="确认密码" borderBottom>
|
||||
<u-input
|
||||
class="flex-1"
|
||||
type="password"
|
||||
v-model="formData.password_confirm"
|
||||
placeholder="再次输入新密码"
|
||||
:border="false"
|
||||
/>
|
||||
</u-form-item>
|
||||
</u-form>
|
||||
<view class="mt-[100rpx]">
|
||||
<u-button type="primary" shape="circle" @click="handleConfirm"> 确定 </u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { userChangePwd } from '@/api/user'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { reactive, ref } from 'vue'
|
||||
|
||||
const type = ref('')
|
||||
const formData = reactive<any>({
|
||||
password: '',
|
||||
password_confirm: ''
|
||||
})
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!formData.old_password && type.value != 'set') return uni.$u.toast('请输入原来的密码')
|
||||
if (!formData.password) return uni.$u.toast('请输入密码')
|
||||
if (!formData.password_confirm) return uni.$u.toast('请输入确认密码')
|
||||
if (formData.password != formData.password_confirm) return uni.$u.toast('两次输入的密码不一致')
|
||||
await userChangePwd(formData)
|
||||
uni.$u.toast('操作成功')
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
type.value = options.type || ''
|
||||
if (type.value == 'set') {
|
||||
uni.setNavigationBarTitle({
|
||||
title: '设置登录密码'
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
page {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<z-paging
|
||||
ref="paging"
|
||||
v-model="collectData"
|
||||
@query="queryList"
|
||||
:fixed="false"
|
||||
height="100%"
|
||||
use-page-scroll
|
||||
>
|
||||
<u-swipe-action
|
||||
:show="item.show"
|
||||
:index="index"
|
||||
v-for="(item, index) in collectData"
|
||||
:key="item.id"
|
||||
@click="handleCollect"
|
||||
:options="options"
|
||||
btn-width="120"
|
||||
>
|
||||
<news-card :item="item" :newsId="item.article_id"></news-card>
|
||||
</u-swipe-action>
|
||||
</z-paging>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, shallowRef } from 'vue'
|
||||
import { getCollect, cancelCollect } from '@/api/news'
|
||||
|
||||
const paging = shallowRef()
|
||||
const options = reactive([
|
||||
{
|
||||
text: '取消收藏',
|
||||
style: {
|
||||
color: '#FFFFFF',
|
||||
backgroundColor: '#FF2C3C'
|
||||
}
|
||||
}
|
||||
])
|
||||
const collectData: any = ref([])
|
||||
|
||||
const queryList = async (pageNo, pageSize) => {
|
||||
const { lists } = await getCollect()
|
||||
lists.forEach((item: any) => {
|
||||
item.show = false
|
||||
})
|
||||
collectData.value = lists
|
||||
paging.value.complete(lists)
|
||||
}
|
||||
|
||||
const handleCollect = async (index: number): Promise<void> => {
|
||||
try {
|
||||
const article_id: number = collectData.value[index].article_id
|
||||
await cancelCollect({ id: article_id })
|
||||
uni.$u.toast('已取消收藏')
|
||||
paging.value.reload()
|
||||
} catch (err) {
|
||||
//TODO handle the exception
|
||||
console.log('取消收藏报错=>', err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<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.data)
|
||||
}
|
||||
getData()
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<view
|
||||
class="register bg-white min-h-full flex flex-col items-center px-[40rpx] pt-[100rpx] box-border"
|
||||
>
|
||||
<view class="w-full">
|
||||
<view class="text-2xl font-medium mb-[60rpx]">忘记登录密码</view>
|
||||
<u-form borderBottom :label-width="150">
|
||||
<u-form-item label="手机号" borderBottom>
|
||||
<u-input
|
||||
class="flex-1"
|
||||
v-model="formData.mobile"
|
||||
:border="false"
|
||||
placeholder="请输入手机号码"
|
||||
/>
|
||||
</u-form-item>
|
||||
<u-form-item label="验证码" borderBottom>
|
||||
<u-input
|
||||
class="flex-1"
|
||||
v-model="formData.code"
|
||||
placeholder="请输入验证码"
|
||||
:border="false"
|
||||
/>
|
||||
<view
|
||||
class="border-l border-solid border-0 border-light pl-3 text-muted leading-4 ml-3 w-[180rpx]"
|
||||
@click="sendSms"
|
||||
>
|
||||
<u-verification-code
|
||||
ref="uCodeRef"
|
||||
:seconds="60"
|
||||
@change="codeChange"
|
||||
change-text="x秒"
|
||||
/>
|
||||
<text :class="formData.mobile ? 'text-primary' : 'text-muted'">
|
||||
{{ codeTips }}
|
||||
</text>
|
||||
</view>
|
||||
</u-form-item>
|
||||
<u-form-item label="新密码" borderBottom>
|
||||
<u-input
|
||||
class="flex-1"
|
||||
type="password"
|
||||
v-model="formData.password"
|
||||
placeholder="6-20位数字+字母或符号组合"
|
||||
:border="false"
|
||||
/>
|
||||
</u-form-item>
|
||||
<u-form-item label="确认密码" borderBottom>
|
||||
<u-input
|
||||
class="flex-1"
|
||||
type="password"
|
||||
v-model="formData.password_confirm"
|
||||
placeholder="再次输入新密码"
|
||||
:border="false"
|
||||
/>
|
||||
</u-form-item>
|
||||
</u-form>
|
||||
<view class="mt-[100rpx]">
|
||||
<u-button type="primary" shape="circle" @click="handleConfirm"> 确定 </u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { smsSend } from '@/api/app'
|
||||
import { forgotPassword } from '@/api/user'
|
||||
import { SMSEnum } from '@/enums/appEnums'
|
||||
import { reactive, ref, shallowRef } from 'vue'
|
||||
|
||||
const uCodeRef = shallowRef()
|
||||
const codeTips = ref('')
|
||||
const formData = reactive({
|
||||
mobile: '',
|
||||
code: '',
|
||||
password: '',
|
||||
password_confirm: ''
|
||||
})
|
||||
|
||||
const codeChange = (text: string) => {
|
||||
codeTips.value = text
|
||||
}
|
||||
|
||||
const sendSms = async () => {
|
||||
if (!formData.mobile) return
|
||||
if (uCodeRef.value?.canGetCode) {
|
||||
await smsSend({
|
||||
scene: SMSEnum.FIND_PASSWORD,
|
||||
mobile: formData.mobile
|
||||
})
|
||||
uni.$u.toast('发送成功')
|
||||
uCodeRef.value?.start()
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!formData.mobile) return uni.$u.toast('请输入手机号码')
|
||||
if (!formData.password) return uni.$u.toast('请输入密码')
|
||||
if (!formData.password_confirm) return uni.$u.toast('请输入确认密码')
|
||||
if (formData.password != formData.password_confirm) return uni.$u.toast('两次输入的密码不一致')
|
||||
await forgotPassword(formData)
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
page {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<!-- modal:隐私授权弹窗-->
|
||||
<view v-if="show" class="modal-box" @tap.stop>
|
||||
<view class="dialog" @tap.stop>
|
||||
<view class="title">隐私政策提示</view>
|
||||
<view class="content">
|
||||
欢迎使用{{
|
||||
appStore.getWebsiteConfig.shop_name
|
||||
}}小程序,请您在使用前点击
|
||||
<text
|
||||
class="text-[#243245]"
|
||||
hover-class="hover"
|
||||
@click="openContract"
|
||||
>
|
||||
{{ name }}
|
||||
</text>
|
||||
并仔细阅读,如您同意全部内容,请点击同意开始使用我们的服务。
|
||||
</view>
|
||||
<view class="btn-box">
|
||||
<button
|
||||
class="btn disagree"
|
||||
hover-class="hover"
|
||||
@click="disagreePrivacy"
|
||||
>
|
||||
不同意
|
||||
</button>
|
||||
<button
|
||||
class="btn bg-primary text-white"
|
||||
hover-class="hover"
|
||||
id="agree-btn"
|
||||
open-type="agreePrivacyAuthorization"
|
||||
@agreeprivacyauthorization="agreePrivacy"
|
||||
>
|
||||
同意
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
const appStore = useAppStore()
|
||||
|
||||
const name = ref<string>('')
|
||||
const show = ref<boolean>(false)
|
||||
|
||||
interface PrivacyRes {
|
||||
errMsg: string
|
||||
privacyContractName: string
|
||||
needAuthorization: boolean
|
||||
}
|
||||
|
||||
if (wx.getPrivacySetting) {
|
||||
wx.getPrivacySetting({
|
||||
success(res: PrivacyRes) {
|
||||
console.log(res)
|
||||
name.value = res.privacyContractName
|
||||
show.value = res.needAuthorization
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const openContract = () => {
|
||||
wx.openPrivacyContract({
|
||||
success: () => {},
|
||||
fail: () => {}
|
||||
})
|
||||
}
|
||||
|
||||
const disagreeHandle = () => {
|
||||
// 用户点击拒绝后
|
||||
show.value = false
|
||||
}
|
||||
|
||||
const disagreePrivacy = () => {
|
||||
uni.$u.toast('同意隐私政策后可继续使用')
|
||||
// wx.exitMiniProgram()
|
||||
}
|
||||
|
||||
const agreePrivacy = () => {
|
||||
show.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-box {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.modal-box .dialog {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 40rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
}
|
||||
|
||||
.modal-box .title {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
font-size: 34rpx;
|
||||
}
|
||||
|
||||
.modal-box .content {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-top: 20rpx;
|
||||
text-align: justify;
|
||||
line-height: 1.6;
|
||||
padding: 10rpx 20rpx;
|
||||
}
|
||||
|
||||
.modal-box .btn-box {
|
||||
margin-top: 50rpx;
|
||||
padding: 0 30rpx;
|
||||
padding-bottom: 30rpx;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-box .btn::after {
|
||||
border: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-box .btn-box .btn {
|
||||
width: 50%;
|
||||
height: 76rpx;
|
||||
line-height: 76rpx;
|
||||
margin: 0 10rpx;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 60px;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.modal-box .disagree {
|
||||
color: #0f0f0f;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<view class="index" :style="pageStyle">
|
||||
<!-- 组件 -->
|
||||
<template
|
||||
v-for="(item, index) in state.pages"
|
||||
:key="index"
|
||||
>
|
||||
<template v-if="item.name == 'search'">
|
||||
<w-search
|
||||
:pageMeta="state.meta"
|
||||
:content="item.content"
|
||||
:styles="item.styles"
|
||||
:percent="percent"
|
||||
:isLargeScreen="isLargeScreen"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="item.name == 'banner'">
|
||||
<w-banner
|
||||
:content="item.content"
|
||||
:styles="item.styles"
|
||||
:isLargeScreen="isLargeScreen"
|
||||
@change="handleBanner"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="item.name == 'nav'">
|
||||
<w-nav :content="item.content" :styles="item.styles"/>
|
||||
</template>
|
||||
<template v-if="item.name == 'middle-banner'">
|
||||
<w-middle-banner :content="item.content" :styles="item.styles" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<view class="article" v-if="state.article.length">
|
||||
<view
|
||||
class="flex items-center article-title mx-[20rpx] my-[30rpx] text-lg font-medium"
|
||||
>
|
||||
最新资讯
|
||||
</view>
|
||||
<news-card
|
||||
v-for="item in state.article"
|
||||
:key="item.id"
|
||||
:news-id="item.id"
|
||||
:item="item"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- #ifdef H5 -->
|
||||
<view class="text-center py-4 mb-12">
|
||||
<router-navigate
|
||||
class="mx-1 text-xs text-[#495770]"
|
||||
:to="{
|
||||
path: '/pages/webview/webview',
|
||||
query: {
|
||||
url: item.value
|
||||
}
|
||||
}"
|
||||
v-for="item in appStore.getCopyrightConfig"
|
||||
:key="item.key"
|
||||
>
|
||||
{{ item.key }}
|
||||
</router-navigate>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 返回顶部按钮 -->
|
||||
<u-back-top
|
||||
:scroll-top="scrollTop"
|
||||
:top="100"
|
||||
:customStyle="{
|
||||
backgroundColor: '#FFF',
|
||||
color: '#000',
|
||||
boxShadow: '0px 3px 6px rgba(0, 0, 0, 0.1)'
|
||||
}"
|
||||
>
|
||||
</u-back-top>
|
||||
|
||||
<!-- #ifdef MP -->
|
||||
<!-- 微信小程序隐私弹窗 -->
|
||||
<MpPrivacyPopup></MpPrivacyPopup>
|
||||
<!-- #endif -->
|
||||
|
||||
<tabbar/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {getIndex} from '@/api/shop'
|
||||
import {onLoad, onPageScroll} from "@dcloudio/uni-app";
|
||||
import {computed, reactive, ref} from 'vue'
|
||||
import {useAppStore} from '@/stores/app'
|
||||
|
||||
// #ifdef MP
|
||||
import MpPrivacyPopup from './component/mp-privacy-popup.vue'
|
||||
// #endif
|
||||
|
||||
const appStore = useAppStore()
|
||||
const state = reactive<{
|
||||
pages: any[]
|
||||
meta: any[]
|
||||
article: any[]
|
||||
bannerImage: string
|
||||
}>({
|
||||
pages: [],
|
||||
meta: [],
|
||||
article: [],
|
||||
bannerImage: ''
|
||||
})
|
||||
const scrollTop = ref<number>(0)
|
||||
const percent = ref<number>(0)
|
||||
|
||||
// 是否联动背景图
|
||||
const isLinkage = computed(() => {
|
||||
return state.pages.find((item: any) => item.name === 'banner')?.content.bg_style === 1
|
||||
})
|
||||
// 是否大屏banner
|
||||
const isLargeScreen = computed(() => {
|
||||
return state.pages.find((item: any) => item.name === 'banner')?.content.style === 2
|
||||
})
|
||||
|
||||
// 根页面样式
|
||||
const pageStyle = computed(() => {
|
||||
const {bg_type, bg_color, bg_image} = state.meta[0]?.content ?? {}
|
||||
if (!isLinkage.value) {
|
||||
return bg_type == 1 ?
|
||||
{'background-color': bg_color} :
|
||||
{'background-image': `url(${bg_image})`}
|
||||
}
|
||||
else return {'background-image': `url(${state.bannerImage})`}
|
||||
})
|
||||
|
||||
const handleBanner = (url: string) => {
|
||||
state.bannerImage = url
|
||||
}
|
||||
|
||||
const getData = async () => {
|
||||
const data = await getIndex()
|
||||
state.pages = JSON.parse(data?.page?.data)
|
||||
state.meta = JSON.parse(data?.page?.meta)
|
||||
state.article = data.article
|
||||
uni.setNavigationBarTitle({
|
||||
title: state.meta[0].content.title
|
||||
})
|
||||
}
|
||||
|
||||
onPageScroll((event: any) => {
|
||||
scrollTop.value = event.scrollTop
|
||||
const top = uni.upx2px(100)
|
||||
percent.value = event.scrollTop / top > 1 ? 1 : event.scrollTop / top
|
||||
})
|
||||
|
||||
onLoad(() => { getData() })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.index {
|
||||
position: relative;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% auto;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
transition: all 1s;
|
||||
min-height: calc(100vh - env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.article-title {
|
||||
&::before {
|
||||
content: '';
|
||||
width: 8rpx;
|
||||
height: 34rpx;
|
||||
display: block;
|
||||
margin-right: 10rpx;
|
||||
@apply bg-primary;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,480 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar :front-color="$theme.navColor" :background-color="$theme.navBgColor" />
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<view
|
||||
class="bg-white login min-h-full flex flex-col items-center px-[40rpx] pt-[120rpx] box-border"
|
||||
>
|
||||
<view>
|
||||
<image
|
||||
:src="appStore.getWebsiteConfig.shop_logo"
|
||||
mode="widthFix"
|
||||
class="w-[160rpx] h-[160rpx] rounded-full"
|
||||
/>
|
||||
</view>
|
||||
<view class="w-full mt-[140rpx] pb-[60rpx]">
|
||||
<block v-if="!phoneLogin">
|
||||
<!-- #ifdef MP-WEIXIN || H5 -->
|
||||
<view v-if="isOpenOtherAuth && isWeixin && inWxAuth">
|
||||
<u-button
|
||||
type="primary"
|
||||
@click="wxLogin"
|
||||
:customStyle="{ height: '100rpx' }"
|
||||
hover-class="none"
|
||||
>
|
||||
用户一键登录
|
||||
</u-button>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<view class="mt-[40rpx]">
|
||||
<u-button
|
||||
@click="phoneLogin = !phoneLogin"
|
||||
:customStyle="{ height: '100rpx' }"
|
||||
hover-class="none"
|
||||
>
|
||||
手机号登录
|
||||
</u-button>
|
||||
</view>
|
||||
</block>
|
||||
<block v-if="phoneLogin">
|
||||
<!-- 密码登录 -->
|
||||
<template
|
||||
v-if="
|
||||
formData.scene == LoginWayEnum.ACCOUNT &&
|
||||
includeLoginWay(LoginWayEnum.ACCOUNT)
|
||||
"
|
||||
>
|
||||
<view
|
||||
class="px-[18rpx] border border-solid border-lightc border-light rounded-[10rpx] h-[100rpx] items-center flex"
|
||||
>
|
||||
<u-input
|
||||
class="flex-1"
|
||||
v-model="formData.account"
|
||||
:border="false"
|
||||
placeholder="输入账号"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
class="px-[18rpx] py-[10rpx] border border-solid border-light rounded-[10rpx] flex h-[100rpx] items-center mt-[40rpx]"
|
||||
>
|
||||
<u-input
|
||||
class="flex-1"
|
||||
v-model="formData.password"
|
||||
type="password"
|
||||
placeholder="输入密码"
|
||||
:border="false"
|
||||
/>
|
||||
<navigator url="/pages/forget_pwd/forget_pwd" hover-class="none">
|
||||
<view
|
||||
class="border-l border-solid border-0 border-light pl-3 text-muted leading-4 ml-3"
|
||||
>
|
||||
忘记密码?
|
||||
</view>
|
||||
</navigator>
|
||||
</view>
|
||||
</template>
|
||||
<!-- 验证码登录 -->
|
||||
<template
|
||||
v-if="
|
||||
formData.scene == LoginWayEnum.MOBILE &&
|
||||
includeLoginWay(LoginWayEnum.MOBILE)
|
||||
"
|
||||
>
|
||||
<view
|
||||
class="px-[18rpx] border border-solid border-lightc border-light rounded-[10rpx] h-[100rpx] items-center flex"
|
||||
>
|
||||
<u-input
|
||||
class="flex-1"
|
||||
v-model="formData.account"
|
||||
:border="false"
|
||||
placeholder="请输入手机号码"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
class="px-[18rpx] border border-solid border-lightc border-light rounded-[10rpx] h-[100rpx] items-center flex mt-[40rpx]"
|
||||
>
|
||||
<u-input
|
||||
class="flex-1"
|
||||
v-model="formData.code"
|
||||
placeholder="请输入验证码"
|
||||
:border="false"
|
||||
/>
|
||||
|
||||
<view
|
||||
class="border-l border-solid border-0 border-light pl-3 leading-4 ml-3 w-[180rpx]"
|
||||
@click="sendSms"
|
||||
>
|
||||
<u-verification-code
|
||||
ref="uCodeRef"
|
||||
:seconds="60"
|
||||
@change="codeChange"
|
||||
change-text="x秒"
|
||||
/>
|
||||
<text :class="formData.account ? 'text-primary' : 'text-muted'">
|
||||
{{ codeTips }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</block>
|
||||
|
||||
<view class="mt-[40rpx]" v-if="isOpenAgreement">
|
||||
<u-checkbox v-model="isCheckAgreement" shape="circle">
|
||||
<view class="text-xs flex">
|
||||
已阅读并同意
|
||||
<view @click.stop>
|
||||
<navigator
|
||||
class="text-primary"
|
||||
hover-class="none"
|
||||
url="/pages/agreement/agreement?type=service"
|
||||
>
|
||||
《服务协议》
|
||||
</navigator>
|
||||
</view>
|
||||
|
||||
和
|
||||
<view @click.stop>
|
||||
<navigator
|
||||
class="text-primary"
|
||||
hover-class="none"
|
||||
url="/pages/agreement/agreement?type=privacy"
|
||||
>
|
||||
《隐私协议》
|
||||
</navigator>
|
||||
</view>
|
||||
</view>
|
||||
</u-checkbox>
|
||||
</view>
|
||||
<block v-if="phoneLogin">
|
||||
<view class="mt-[60rpx]">
|
||||
<u-button
|
||||
type="primary"
|
||||
@click="handleLogin(formData.scene)"
|
||||
:customStyle="{
|
||||
height: '100rpx',
|
||||
opacity: DisableStyle ? '1' : '0.5'
|
||||
}"
|
||||
hover-class="none"
|
||||
>
|
||||
登录
|
||||
</u-button>
|
||||
</view>
|
||||
<view class="flex justify-between mt-[40rpx]">
|
||||
<view
|
||||
>已有账号,使用
|
||||
<span
|
||||
class="text-primary"
|
||||
@click="changeLoginWay(LoginWayEnum.ACCOUNT)"
|
||||
v-if="
|
||||
formData.scene == LoginWayEnum.MOBILE &&
|
||||
includeLoginWay(LoginWayEnum.ACCOUNT)
|
||||
"
|
||||
>密码登录</span
|
||||
>
|
||||
<span
|
||||
class="text-primary"
|
||||
@click="changeLoginWay(LoginWayEnum.MOBILE)"
|
||||
v-if="
|
||||
formData.scene == LoginWayEnum.ACCOUNT &&
|
||||
includeLoginWay(LoginWayEnum.MOBILE)
|
||||
"
|
||||
>验证码登录</span
|
||||
>
|
||||
</view>
|
||||
<navigator url="/pages/register/register" hover-class="none"
|
||||
>注册账号</navigator
|
||||
>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
<!-- 协议弹框 -->
|
||||
<u-modal
|
||||
v-model="showModel"
|
||||
show-cancel-button
|
||||
:show-title="false"
|
||||
confirm-color="var(--color-primary)"
|
||||
@confirm=";(isCheckAgreement = true), (showModel = false)"
|
||||
@cancel="showModel = false"
|
||||
>
|
||||
<view class="text-center px-[70rpx] py-[60rpx]">
|
||||
<view> 请先阅读并同意 </view>
|
||||
<view class="flex justify-center">
|
||||
<navigator data-theme="" url="/pages/agreement/agreement?type=service">
|
||||
<view class="text-primary">《服务协议》</view>
|
||||
</navigator>
|
||||
和
|
||||
<navigator url="/pages/agreement/agreement?type=privacy">
|
||||
<view class="text-primary">《隐私协议》</view>
|
||||
</navigator>
|
||||
</view>
|
||||
</view>
|
||||
</u-modal>
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
|
||||
<mplogin-popup
|
||||
v-model:show="showLoginPopup"
|
||||
:logo="websiteConfig.shop_logo"
|
||||
:title="websiteConfig.shop_name"
|
||||
@update="handleUpdateUser"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { login, mnpLogin, updateUser, OALogin } from '@/api/account'
|
||||
import { smsSend } from '@/api/app'
|
||||
import { SMSEnum } from '@/enums/appEnums'
|
||||
import { BACK_URL } from '@/enums/constantEnums'
|
||||
import { useLockFn } from '@/hooks/useLockFn'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useRouter, useRoute } from 'uniapp-router-next'
|
||||
import cache from '@/utils/cache'
|
||||
import { isWeixinClient } from '@/utils/client'
|
||||
// #ifdef H5
|
||||
import wechatOa, { UrlScene } from '@/utils/wechat'
|
||||
// #endif
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import { computed, reactive, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
enum LoginWayEnum {
|
||||
ACCOUNT = 1,
|
||||
MOBILE = 2
|
||||
}
|
||||
|
||||
const isWeixin = ref(true)
|
||||
// #ifdef H5
|
||||
isWeixin.value = isWeixinClient()
|
||||
// #endif
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const appStore = useAppStore()
|
||||
const showModel = ref(false)
|
||||
const uCodeRef = shallowRef()
|
||||
const codeTips = ref('')
|
||||
const showLoginPopup = ref(false)
|
||||
const isCheckAgreement = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
scene: 1,
|
||||
account: '',
|
||||
password: '',
|
||||
code: ''
|
||||
})
|
||||
const phoneLogin = ref(false)
|
||||
const loginData = ref()
|
||||
const codeChange = (text: string) => {
|
||||
codeTips.value = text
|
||||
}
|
||||
|
||||
const websiteConfig = computed(() => appStore.getWebsiteConfig)
|
||||
|
||||
const sendSms = async () => {
|
||||
if (!formData.account) return
|
||||
if (uCodeRef.value?.canGetCode) {
|
||||
await smsSend({
|
||||
scene: SMSEnum.LOGIN,
|
||||
mobile: formData.account
|
||||
})
|
||||
uni.$u.toast('发送成功')
|
||||
uCodeRef.value?.start()
|
||||
}
|
||||
}
|
||||
|
||||
const changeLoginWay = (way: LoginWayEnum) => {
|
||||
formData.scene = way
|
||||
}
|
||||
|
||||
const includeLoginWay = (way: LoginWayEnum) => {
|
||||
return appStore.getLoginConfig.login_way?.includes(String(way))
|
||||
}
|
||||
|
||||
const inWxAuth = computed(() => {
|
||||
return appStore.getLoginConfig.wechat_auth
|
||||
})
|
||||
|
||||
const isOpenAgreement = computed(() => appStore.getLoginConfig.login_agreement == 1)
|
||||
|
||||
const isOpenOtherAuth = computed(() => appStore.getLoginConfig.third_auth == 1)
|
||||
const isForceBindMobile = computed(() => appStore.getLoginConfig.coerce_mobile == 1)
|
||||
|
||||
const loginFun = async () => {
|
||||
if (!isCheckAgreement.value && isOpenAgreement.value) return (showModel.value = true)
|
||||
if (formData.scene == LoginWayEnum.ACCOUNT) {
|
||||
if (!formData.account) return uni.$u.toast('请输入账号/手机号码')
|
||||
if (!formData.password) return uni.$u.toast('请输入密码')
|
||||
}
|
||||
if (formData.scene == LoginWayEnum.MOBILE) {
|
||||
if (!formData.account) return uni.$u.toast('请输入手机号码')
|
||||
if (!formData.code) return uni.$u.toast('请输入验证码')
|
||||
}
|
||||
uni.showLoading({
|
||||
title: '请稍后...'
|
||||
})
|
||||
try {
|
||||
const data = await login(formData)
|
||||
loginHandle(data)
|
||||
} catch (error: any) {
|
||||
uni.hideLoading()
|
||||
uni.$u.toast(error)
|
||||
}
|
||||
}
|
||||
|
||||
const loginHandle = async (data: any) => {
|
||||
const { token, mobile } = data
|
||||
if (!mobile && isForceBindMobile.value) {
|
||||
userStore.temToken = token
|
||||
router.navigateTo('/pages/bind_mobile/bind_mobile')
|
||||
uni.hideLoading()
|
||||
return
|
||||
}
|
||||
userStore.login(data.token)
|
||||
await userStore.getUser()
|
||||
uni.$u.toast('登录成功')
|
||||
uni.hideLoading()
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1) {
|
||||
const prevPage = pages[pages.length - 2]
|
||||
await router.navigateBack()
|
||||
// @ts-ignore
|
||||
const { onLoad, options } = prevPage
|
||||
// 刷新上一个页面
|
||||
onLoad && onLoad(options)
|
||||
} else if (cache.get(BACK_URL)) {
|
||||
try {
|
||||
router.redirectTo(cache.get(BACK_URL))
|
||||
} catch (error) {
|
||||
router.switchTab(cache.get(BACK_URL))
|
||||
}
|
||||
} else {
|
||||
router.reLaunch('/pages/index/index')
|
||||
}
|
||||
cache.remove(BACK_URL)
|
||||
}
|
||||
|
||||
const { lockFn: handleLogin } = useLockFn(loginFun)
|
||||
|
||||
const oaLogin = async (options: any = { getUrl: true }) => {
|
||||
const { code, getUrl } = options
|
||||
if (getUrl) {
|
||||
await wechatOa.getUrl(UrlScene.LOGIN)
|
||||
} else {
|
||||
const data = await OALogin({
|
||||
code
|
||||
})
|
||||
return data
|
||||
}
|
||||
return Promise.reject()
|
||||
}
|
||||
|
||||
const wxLogin = async () => {
|
||||
if (!isCheckAgreement.value && isOpenAgreement.value) {
|
||||
showModel.value = true
|
||||
console.log(showModel.value)
|
||||
return
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
|
||||
uni.showLoading({
|
||||
title: '请稍后...'
|
||||
})
|
||||
try {
|
||||
const { code }: any = await uni.login({
|
||||
provider: 'weixin'
|
||||
})
|
||||
const data = await mnpLogin({
|
||||
code: code
|
||||
})
|
||||
loginData.value = data
|
||||
if (data.is_new_user) {
|
||||
uni.hideLoading()
|
||||
userStore.temToken = data.token
|
||||
showLoginPopup.value = true
|
||||
return
|
||||
}
|
||||
loginHandle(data)
|
||||
} catch (error: any) {
|
||||
uni.hideLoading()
|
||||
uni.$u.toast(error)
|
||||
}
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
if (isWeixin.value) {
|
||||
oaLogin()
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
const handleUpdateUser = async (value: any) => {
|
||||
await updateUser(value, { token: userStore.temToken })
|
||||
showLoginPopup.value = false
|
||||
loginHandle(loginData.value)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => appStore.getLoginConfig,
|
||||
(value) => {
|
||||
if (value.login_way) {
|
||||
formData.scene = value.login_way[0]
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
const DisableStyle = computed(() => {
|
||||
if (formData.scene == 1 && formData.account && formData.password) {
|
||||
return true
|
||||
} else if (formData.scene == 2 && formData.account && formData.code) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const removeWxQuery = () => {
|
||||
const options = route.query
|
||||
if (options.code && options.state) {
|
||||
delete options.code
|
||||
delete options.state
|
||||
router.redirectTo({ path: route.path, query: options })
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(async () => {
|
||||
//#ifdef H5
|
||||
const options = wechatOa.getAuthData()
|
||||
try {
|
||||
if (options.code && options.scene === UrlScene.LOGIN) {
|
||||
uni.showLoading({
|
||||
title: '请稍后...'
|
||||
})
|
||||
const data = await oaLogin(options)
|
||||
if (data) {
|
||||
loginData.value = data
|
||||
|
||||
loginHandle(loginData.value)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
removeWxQuery()
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
//清除保存的授权数据
|
||||
wechatOa.setAuthData()
|
||||
}
|
||||
//#endif
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
page {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<z-paging
|
||||
auto-show-back-to-top
|
||||
:auto="i == index"
|
||||
ref="paging"
|
||||
v-model="dataList"
|
||||
:data-key="i"
|
||||
@query="queryList"
|
||||
:fixed="false"
|
||||
height="100%"
|
||||
>
|
||||
<block v-for="(newsItem, newsIndex) in dataList" :key="newsIndex">
|
||||
<news-card :item="newsItem" :newsId="newsItem.id"></news-card>
|
||||
</block>
|
||||
</z-paging>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, nextTick, shallowRef } from 'vue'
|
||||
import { getArticleList } from '@/api/news'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
cid: number
|
||||
i: number
|
||||
index: number
|
||||
}>(),
|
||||
{
|
||||
cid: 0
|
||||
}
|
||||
)
|
||||
|
||||
const paging = shallowRef<any>(null)
|
||||
const dataList = ref([])
|
||||
const isFirst = ref<boolean>(true)
|
||||
|
||||
watch(
|
||||
() => props.index,
|
||||
async () => {
|
||||
await nextTick()
|
||||
if (props.i == props.index && isFirst.value) {
|
||||
isFirst.value = false
|
||||
paging.value?.reload()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const queryList = async (page_no, page_size) => {
|
||||
try {
|
||||
const { lists } = await getArticleList({
|
||||
cid: props.cid,
|
||||
page_no,
|
||||
page_size
|
||||
})
|
||||
paging.value.complete(lists)
|
||||
} catch (e) {
|
||||
console.log('报错=>', e)
|
||||
//TODO handle the exception
|
||||
paging.value.complete(false)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<!-- #ifndef H5 -->
|
||||
<u-sticky
|
||||
h5-nav-height="0"
|
||||
bg-color="transparent"
|
||||
>
|
||||
<u-navbar
|
||||
:is-back="false"
|
||||
:is-fixed="false"
|
||||
title="资讯"
|
||||
:border-bottom="false"
|
||||
:title-bold="true"
|
||||
:title-color="$theme.navColor"
|
||||
:background="{ background: $theme.navBgColor }"
|
||||
>
|
||||
</u-navbar>
|
||||
</u-sticky>
|
||||
<!-- #endif -->
|
||||
<view class="news">
|
||||
<!-- 搜索 -->
|
||||
<navigator class="news-search px-[24rpx] py-[14rpx] bg-white" url="/pages/search/search">
|
||||
<u-search placeholder="请输入关键词搜索" disabled :show-action="false"></u-search>
|
||||
</navigator>
|
||||
|
||||
<!-- 内容 -->
|
||||
<tabs
|
||||
:current="current"
|
||||
@change="handleChange"
|
||||
height="80"
|
||||
bar-width="60"
|
||||
:barStyle="{ bottom: '0' }"
|
||||
>
|
||||
<tab v-for="(item, i) in tabList" :key="i" :name="item.name">
|
||||
<view class="news-list pt-[20rpx]">
|
||||
<news-list :cid="item.id" :i="i" :index="current"></news-list>
|
||||
</view>
|
||||
</tab>
|
||||
</tabs>
|
||||
<tabbar />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { onLoad, onShow, onReady } from '@dcloudio/uni-app'
|
||||
import NewsList from './component/news-list.vue'
|
||||
import { getArticleCate } from '@/api/news'
|
||||
|
||||
const tabList = ref<any>([])
|
||||
const current = ref<number>(0)
|
||||
|
||||
const handleChange = (index: number) => {
|
||||
console.log(index)
|
||||
current.value = Number(index)
|
||||
}
|
||||
|
||||
const getData = async () => {
|
||||
const data = await getArticleCate()
|
||||
tabList.value = [{ name: '全部', id: '' }].concat(data)
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
getData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.news {
|
||||
&-search {
|
||||
margin-bottom: 2rpx;
|
||||
}
|
||||
|
||||
&-list {
|
||||
height: calc(100vh - 272rpx - env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<view class="news-detail bg-white">
|
||||
<!-- 标题信心 -->
|
||||
<view class="news-detail-header py-[20rpx] px-[30rpx]">
|
||||
<view class="text-3xl font-medium">{{ newsData.title }}</view>
|
||||
<view class="flex mt-[20rpx] text-xs">
|
||||
<view class="mr-[40rpx]" v-if="newsData.author">作者: {{ newsData.author }}</view>
|
||||
<view class="text-muted mr-[40rpx] flex-1">{{ newsData.create_time }}</view>
|
||||
<view class="flex items-center text-muted flex-none">
|
||||
<image
|
||||
src="/static/images/icon/icon_visit.png"
|
||||
class="w-[30rpx] h-[30rpx]"
|
||||
></image>
|
||||
<view class="ml-[10rpx]">{{ newsData.click }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 咨询内容 -->
|
||||
<view class="news-detail-section bg-white p-[24rpx]">
|
||||
<!-- 摘要 -->
|
||||
<view class="summary p-[20rpx] text-base" v-if="newsData.abstract">
|
||||
<text class="font-medium">摘要: </text> {{ newsData.abstract }}
|
||||
</view>
|
||||
<!-- 内容 -->
|
||||
<view class="mt-[20rpx]">
|
||||
<u-parse :html="newsData.content"></u-parse>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel-btn flex items-center px-[34rpx]" @click="handleAddCollect(newsData.id)">
|
||||
<u-icon
|
||||
:name="newsData.collect ? 'star-fill' : 'star'"
|
||||
size="40"
|
||||
:color="newsData.collect ? '#F7BA47' : '#333'"
|
||||
></u-icon>
|
||||
<text class="ml-[10rpx]">收藏</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getArticleDetail, addCollect, cancelCollect } from '@/api/news'
|
||||
|
||||
const newsData = ref<any>({})
|
||||
let newsId = ''
|
||||
|
||||
const getData = async (id) => {
|
||||
newsData.value = await getArticleDetail({ id })
|
||||
}
|
||||
|
||||
const handleAddCollect = async (id: number) => {
|
||||
try {
|
||||
if (newsData.value.collect) {
|
||||
await cancelCollect({ id })
|
||||
uni.$u.toast('已取消收藏')
|
||||
} else {
|
||||
await addCollect({ id })
|
||||
uni.$u.toast('收藏成功')
|
||||
}
|
||||
getData(newsId)
|
||||
} catch (e) {
|
||||
//TODO handle the exception
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((options: any) => {
|
||||
newsId = options.id
|
||||
getData(newsId)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.news-detail {
|
||||
height: 100%;
|
||||
|
||||
&-header {
|
||||
border-bottom: 2rpx solid #f8f8f8;
|
||||
}
|
||||
|
||||
&-section {
|
||||
.summary {
|
||||
border-radius: 12rpx;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-btn {
|
||||
position: fixed;
|
||||
right: 30rpx;
|
||||
height: 80rpx;
|
||||
bottom: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<!-- 页面状态 -->
|
||||
<page-status :status="status">
|
||||
<template #error>
|
||||
<u-empty text="订单不存在" mode="order"></u-empty>
|
||||
</template>
|
||||
<template #default>
|
||||
<view class="payment-result p-[20rpx]">
|
||||
<view class="result bg-white p-[20rpx] rounded-md">
|
||||
<view class="flex flex-col items-center my-[40rpx]">
|
||||
<!-- 支付状态图片 -->
|
||||
<u-image
|
||||
class="status-image"
|
||||
:src="paymentStatus['image']"
|
||||
width="100"
|
||||
height="100"
|
||||
shape="circle"
|
||||
/>
|
||||
<!-- 支付状态文字 -->
|
||||
<text class="text-2xl font-medium mt-[20rpx]">{{
|
||||
paymentStatus['text']
|
||||
}}
|
||||
</text>
|
||||
<view class="text-3xl font-medium mt-[20rpx]">
|
||||
¥ {{ orderInfo.order.order_amount }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 支付信息 -->
|
||||
<view class="result-info">
|
||||
<view class="result-info__item">
|
||||
<text>订单编号</text>
|
||||
<text>{{ orderInfo.order.order_sn }}</text>
|
||||
</view>
|
||||
<view class="result-info__item">
|
||||
<text>付款时间</text>
|
||||
<text>{{ orderInfo.order.pay_time }}</text>
|
||||
</view>
|
||||
<view class="result-info__item">
|
||||
<text>支付方式</text>
|
||||
<template v-if="orderInfo.pay_status">
|
||||
<text>{{ orderInfo.order.pay_way || '-' }}</text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text>未支付</text>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-[40rpx]">
|
||||
<view class="mb-[20rpx]">
|
||||
<u-button
|
||||
v-if="pageOptions.from == 'recharge'"
|
||||
type="primary"
|
||||
shape="circle"
|
||||
hover-class="none"
|
||||
@click="goOrder"
|
||||
>
|
||||
继续充值
|
||||
</u-button>
|
||||
</view>
|
||||
<view class="mb-[20rpx]">
|
||||
<u-button
|
||||
type="primary"
|
||||
plain
|
||||
shape="circle"
|
||||
hover-class="none"
|
||||
@click="goHome"
|
||||
>
|
||||
返回首页
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</page-status>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {getPayResult} from '@/api/pay'
|
||||
import {PageStatusEnum} from '@/enums/appEnums'
|
||||
import {onLoad} from '@dcloudio/uni-app'
|
||||
import {computed, reactive, ref} from 'vue'
|
||||
import {useRouter} from "uniapp-router-next";
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const mapStatus = {
|
||||
succeed: {
|
||||
text: '支付成功',
|
||||
image: '/static/images/payment/icon_succeed.png'
|
||||
},
|
||||
waiting: {
|
||||
text: '等待支付',
|
||||
image: '/static/images/payment/icon_waiting.png'
|
||||
}
|
||||
}
|
||||
const status = ref(PageStatusEnum['LOADING'])
|
||||
const pageOptions = ref({
|
||||
id: '',
|
||||
from: ''
|
||||
})
|
||||
const orderInfo = reactive<any>({
|
||||
order: {}
|
||||
})
|
||||
const paymentStatus = computed(() => {
|
||||
const status = !!orderInfo.pay_status
|
||||
return mapStatus[status ? 'succeed' : 'waiting']
|
||||
})
|
||||
|
||||
const initPageData = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
getPayResult({
|
||||
order_id: pageOptions.value.id,
|
||||
from: pageOptions.value.from
|
||||
})
|
||||
.then((data) => {
|
||||
Object.assign(orderInfo, data)
|
||||
resolve(data)
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const goHome = () => {
|
||||
router.reLaunch('/pages/index/index')
|
||||
}
|
||||
|
||||
const goOrder = () => {
|
||||
switch (pageOptions.value.from) {
|
||||
case 'recharge':
|
||||
router.navigateBack()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(async (options: any) => {
|
||||
try {
|
||||
if (!options.id) throw new Error('订单不存在')
|
||||
pageOptions.value = options
|
||||
await initPageData()
|
||||
status.value = PageStatusEnum['NORMAL']
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
status.value = PageStatusEnum['ERROR']
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.result-info {
|
||||
.result-info__item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<view
|
||||
class="register bg-white min-h-full flex flex-col items-center px-[40rpx] pt-[40rpx] box-border"
|
||||
>
|
||||
<view class="w-full">
|
||||
<view class="text-2xl font-medium mb-[60rpx]">注册新账号</view>
|
||||
|
||||
<view
|
||||
class="px-[18rpx] border border-solid border-lightc border-light rounded-[10rpx] h-[100rpx] items-center flex"
|
||||
>
|
||||
<u-input
|
||||
class="flex-1"
|
||||
v-model="formData.account"
|
||||
:border="false"
|
||||
placeholder="请输入账号"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="px-[18rpx] border border-solid border-lightc border-light rounded-[10rpx] h-[100rpx] items-center flex mt-[40rpx]"
|
||||
>
|
||||
<u-input
|
||||
class="flex-1"
|
||||
type="password"
|
||||
v-model="formData.password"
|
||||
placeholder="请输入密码"
|
||||
:border="false"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
class="px-[18rpx] border border-solid border-lightc border-light rounded-[10rpx] h-[100rpx] items-center flex mt-[40rpx]"
|
||||
>
|
||||
<u-input
|
||||
class="flex-1"
|
||||
type="password"
|
||||
v-model="formData.password_confirm"
|
||||
placeholder="请再次输入密码"
|
||||
:border="false"
|
||||
/>
|
||||
</view>
|
||||
<view class="mt-[40rpx]" v-if="isOpenAgreement">
|
||||
<u-checkbox v-model="isCheckAgreement" shape="circle">
|
||||
<view class="text-xs flex">
|
||||
已阅读并同意
|
||||
<view @click.stop>
|
||||
<router-navigate
|
||||
class="text-primary"
|
||||
hover-class="none"
|
||||
to="/pages/agreement/agreement?type=service"
|
||||
>
|
||||
《服务协议》
|
||||
</router-navigate>
|
||||
</view>
|
||||
|
||||
和
|
||||
<view @click.stop>
|
||||
<router-navigate
|
||||
class="text-primary"
|
||||
hover-class="none"
|
||||
to="/pages/agreement/agreement?type=privacy"
|
||||
>
|
||||
《隐私协议》
|
||||
</router-navigate>
|
||||
</view>
|
||||
</view>
|
||||
</u-checkbox>
|
||||
</view>
|
||||
<view class="mt-[60rpx]">
|
||||
<u-button
|
||||
type="primary"
|
||||
hover-class="none"
|
||||
@click="accountRegister"
|
||||
:customStyle="{
|
||||
height: '100rpx',
|
||||
opacity:
|
||||
formData.account && formData.password && formData.password_confirm
|
||||
? '1'
|
||||
: '0.5'
|
||||
}"
|
||||
>
|
||||
注册
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 协议弹框 -->
|
||||
<u-modal
|
||||
v-model="showModel"
|
||||
show-cancel-button
|
||||
:show-title="false"
|
||||
@confirm=";(isCheckAgreement = true), (showModel = false)"
|
||||
@cancel="showModel = false"
|
||||
confirm-color="var(--color-primary)"
|
||||
>
|
||||
<view class="text-center px-[70rpx] py-[60rpx]">
|
||||
<view> 请先阅读并同意</view>
|
||||
<view class="flex justify-center">
|
||||
<router-navigate data-theme="" to="/pages/agreement/agreement?type=service">
|
||||
<view class="text-primary">《服务协议》</view>
|
||||
</router-navigate>
|
||||
和
|
||||
<router-navigate to="/pages/agreement/agreement?type=privacy">
|
||||
<view class="text-primary">《隐私协议》</view>
|
||||
</router-navigate>
|
||||
</view>
|
||||
</view>
|
||||
</u-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {register} from '@/api/account'
|
||||
import {useAppStore} from '@/stores/app'
|
||||
import {computed, reactive, ref} from 'vue'
|
||||
|
||||
const isCheckAgreement = ref(false)
|
||||
const appStore = useAppStore()
|
||||
const isOpenAgreement = computed(() => appStore.getLoginConfig.login_agreement == 1)
|
||||
const formData = reactive({
|
||||
account: '',
|
||||
password: '',
|
||||
password_confirm: ''
|
||||
})
|
||||
const showModel = ref(false)
|
||||
const accountRegister = async () => {
|
||||
if (!formData.account) return uni.$u.toast('请输入账号')
|
||||
if (!formData.password) return uni.$u.toast('请输入密码')
|
||||
if (!formData.password_confirm) return uni.$u.toast('请输入确认密码')
|
||||
if (!isCheckAgreement.value && isOpenAgreement.value) return (showModel.value = true)
|
||||
if (formData.password != formData.password_confirm) return uni.$u.toast('两次输入的密码不一致')
|
||||
await register(formData)
|
||||
// uni.navigateBack()
|
||||
setTimeout(function () {
|
||||
uni.navigateBack()
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
page {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<view class="suggest bg-white">
|
||||
<!-- 热门搜索 -->
|
||||
<view class="hot" v-if="hot_search.status == 1 && searchData.length">
|
||||
<view class="font-medium pl-[24rpx] pt-[26rpx] pb-[6rpx] text-lg">热门搜索</view>
|
||||
|
||||
<view class="w-full px-[24rpx]">
|
||||
<block v-for="(hotItem, index) in searchData" :key="index">
|
||||
<view
|
||||
class="keyword truncate max-w-full"
|
||||
@click="handleHistoreSearch(hotItem.name)"
|
||||
>
|
||||
{{ hotItem.name }}
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="mx-[24rpx] my-[40rpx] border-b border-solid border-0 border-light"
|
||||
v-if="hot_search.status == 1 && searchData.length && his_search.length"
|
||||
></view>
|
||||
|
||||
<!-- 历史搜索 -->
|
||||
<view class="history" v-if="his_search.length">
|
||||
<view class="flex justify-between px-[24rpx] pb-[6rpx] pt-[26rpx]">
|
||||
<view class="text-lg font-medium">历史搜索</view>
|
||||
<view class="text-xs text-muted" @click="() => emit('clear')">清空</view>
|
||||
</view>
|
||||
|
||||
<view class="w-full px-[24rpx]">
|
||||
<block v-for="(hisItem, index) in his_search" :key="index">
|
||||
<view class="keyword truncate" @click="handleHistoreSearch(hisItem)">{{
|
||||
hisItem
|
||||
}}</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'search', value: string): void
|
||||
(event: 'clear', value: void): void
|
||||
}>()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
hot_search?: {
|
||||
data: any[]
|
||||
status: number
|
||||
}
|
||||
his_search?: string[]
|
||||
}>(),
|
||||
{
|
||||
hot_search: () => ({
|
||||
data: [],
|
||||
status: 0
|
||||
}),
|
||||
his_search: () => []
|
||||
}
|
||||
)
|
||||
|
||||
const searchData = computed(() => {
|
||||
return props.hot_search.data.filter((item) => item.name)
|
||||
})
|
||||
|
||||
const handleHistoreSearch = (text: string) => {
|
||||
emit('search', text)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.suggest {
|
||||
height: 100%;
|
||||
.keyword {
|
||||
display: inline-block;
|
||||
margin: 24rpx 16rpx 0 0;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 26rpx;
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<view class="search">
|
||||
<!-- 搜索框 -->
|
||||
<view class="px-[24rpx] py-[14rpx] bg-white">
|
||||
<u-search
|
||||
v-model="keyword"
|
||||
placeholder="请输入关键词搜索"
|
||||
height="72"
|
||||
@search="handleSearch"
|
||||
@custom="handleSearch"
|
||||
@clear="search.searching = false"
|
||||
></u-search>
|
||||
</view>
|
||||
|
||||
<!-- 搜索 -->
|
||||
<view class="search-content">
|
||||
<!-- -->
|
||||
<suggest
|
||||
v-show="!search.searching"
|
||||
@search="handleSearch"
|
||||
@clear="handleClear"
|
||||
:hot_search="search.hot_search"
|
||||
:his_search="search.his_search"
|
||||
></suggest>
|
||||
|
||||
<!-- -->
|
||||
<view class="search-content-s pt-[20rpx]" v-show="search.searching">
|
||||
<z-paging
|
||||
ref="paging"
|
||||
v-model="search.result"
|
||||
@query="queryList"
|
||||
:fixed="false"
|
||||
height="100%"
|
||||
>
|
||||
<block v-for="item in search.result" :key="item.id">
|
||||
<news-card :item="item" :newsId="item.id"></news-card>
|
||||
</block>
|
||||
</z-paging>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, shallowRef } from 'vue'
|
||||
import Suggest from './component/suggest.vue'
|
||||
import { HISTORY } from '@/enums/constantEnums'
|
||||
import { getHotSearch } from '@/api/shop'
|
||||
import cache from '@/utils/cache'
|
||||
import { getArticleList } from '@/api/news'
|
||||
|
||||
interface Search {
|
||||
hot_search: {
|
||||
data: any[]
|
||||
status: number
|
||||
}
|
||||
his_search: string[]
|
||||
result: any
|
||||
searching: boolean
|
||||
}
|
||||
|
||||
const search = reactive<Search>({
|
||||
hot_search: {
|
||||
data: [],
|
||||
status: 1
|
||||
},
|
||||
his_search: [],
|
||||
result: [],
|
||||
searching: false
|
||||
})
|
||||
const keyword = ref<string>('')
|
||||
const paging = shallowRef()
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
keyword.value = value
|
||||
if (keyword.value) {
|
||||
if (!search.his_search.includes(keyword.value)) {
|
||||
search.his_search.unshift(keyword.value)
|
||||
cache.set(HISTORY, search.his_search)
|
||||
}
|
||||
}
|
||||
paging.value.reload()
|
||||
search.searching = true
|
||||
}
|
||||
|
||||
const getHotSearchFunc = async () => {
|
||||
try {
|
||||
search.hot_search = await getHotSearch()
|
||||
} catch (e) {
|
||||
//TODO handle the exception
|
||||
console.log('获取热门搜索失败=>', e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClear = async (): Promise<void> => {
|
||||
const resModel: any = await uni.showModal({
|
||||
title: '温馨提示',
|
||||
content: '是否清空历史记录?'
|
||||
})
|
||||
if (resModel.confirm) {
|
||||
cache.set(HISTORY, '')
|
||||
search.his_search = []
|
||||
}
|
||||
}
|
||||
|
||||
const queryList = async (page_no: number, page_size: number) => {
|
||||
try {
|
||||
const { lists } = await getArticleList({
|
||||
keyword: keyword.value,
|
||||
page_no,
|
||||
page_size
|
||||
})
|
||||
paging.value.complete(lists)
|
||||
} catch (e) {
|
||||
console.log('报错=>', e)
|
||||
//TODO handle the exception
|
||||
paging.value.complete(false)
|
||||
}
|
||||
}
|
||||
|
||||
getHotSearchFunc()
|
||||
search.his_search = cache.get(HISTORY) || []
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search {
|
||||
&-content {
|
||||
height: calc(100vh - 46px - env(safe-area-inset-bottom));
|
||||
&-s {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<view class="user">
|
||||
<view v-for="(item, index) in state.pages" :key="index">
|
||||
<template v-if="item.name == 'user-info'">
|
||||
<w-user-info
|
||||
:pageMeta="state.meta"
|
||||
: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>
|
||||
<tabbar />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<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<{
|
||||
meta: any[]
|
||||
pages: any[]
|
||||
}>({
|
||||
meta: [],
|
||||
pages: []
|
||||
})
|
||||
const getData = async () => {
|
||||
const data = await getDecorate({ id: 2 })
|
||||
state.meta = JSON.parse(data.meta)
|
||||
state.pages = JSON.parse(data.data)
|
||||
uni.setNavigationBarTitle({
|
||||
title: state.meta[0].content.title
|
||||
})
|
||||
}
|
||||
const userStore = useUserStore()
|
||||
const { userInfo, isLogin } = storeToRefs(userStore)
|
||||
onShow(() => {
|
||||
userStore.getUser()
|
||||
})
|
||||
getData()
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,365 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<!-- Main Start -->
|
||||
<!-- 头部修改头像 -->
|
||||
<view class="header bg-white pt-[30rpx]">
|
||||
<view class="flex justify-center pb-5">
|
||||
<avatar-upload
|
||||
:modelValue="userInfo?.avatar"
|
||||
file-key="url"
|
||||
:round="true"
|
||||
@update:modelValue="handleAvatarChange"
|
||||
>
|
||||
</avatar-upload>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户ID -->
|
||||
<view
|
||||
class="item text-nr flex justify-between"
|
||||
@click=";(showUserName = true), (newUsername = userInfo?.username)"
|
||||
>
|
||||
<view class="label">账号</view>
|
||||
<view class="content">{{ userInfo?.account }}</view>
|
||||
<u-icon name="arrow-right" size="22" color="#666"></u-icon>
|
||||
</view>
|
||||
|
||||
<!-- 昵称 -->
|
||||
<view
|
||||
class="item text-nr flex justify-between"
|
||||
@click=";(showNickName = true), (newNickname = userInfo?.nickname)"
|
||||
>
|
||||
<view class="label">昵称</view>
|
||||
<view class="content">{{ userInfo?.nickname }}</view>
|
||||
<u-icon name="arrow-right" size="22" color="#666"></u-icon>
|
||||
</view>
|
||||
|
||||
<!-- 性别 -->
|
||||
<view class="item text-nr flex justify-between" @click="changeSex">
|
||||
<view class="label">性别</view>
|
||||
<view class="content">{{ userInfo?.sex }}</view>
|
||||
<u-icon name="arrow-right" size="22" color="#666"></u-icon>
|
||||
</view>
|
||||
|
||||
<!-- 手机号 -->
|
||||
<view class="item text-nr flex justify-between">
|
||||
<view class="label">手机号</view>
|
||||
<view class="content">{{
|
||||
userInfo?.mobile == '' ? '未绑定手机号' : userInfo?.mobile
|
||||
}}</view>
|
||||
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<u-button
|
||||
open-type="getPhoneNumber"
|
||||
@getphonenumber="getPhoneNumber"
|
||||
type="primary"
|
||||
shape="circle"
|
||||
size="mini"
|
||||
:plain="true"
|
||||
>
|
||||
{{ userInfo?.mobile == '' ? '绑定手机号' : '更换手机号' }}
|
||||
</u-button>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<u-button
|
||||
@click="showMobilePop = true"
|
||||
size="mini"
|
||||
type="primary"
|
||||
shape="circle"
|
||||
:plain="true"
|
||||
>
|
||||
{{ userInfo?.mobile == '' ? '绑定手机号' : '更换手机号' }}
|
||||
</u-button>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
|
||||
<!-- 注册时间 -->
|
||||
<view class="item text-nr flex justify-between">
|
||||
<view class="label">注册时间</view>
|
||||
<view class="content">{{ userInfo?.create_time }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 昵称修改组件 -->
|
||||
<u-popup
|
||||
v-model="showNickName"
|
||||
:closeable="true"
|
||||
mode="center"
|
||||
:maskCloseAble="false"
|
||||
border-radius="20"
|
||||
>
|
||||
<view class="px-[50rpx] py-[40rpx] bg-white" style="width: 85vw">
|
||||
<form @submit="changeNameConfirm">
|
||||
<view class="mb-[70rpx] text-xl text-center">修改昵称</view>
|
||||
<u-form-item borderBottom>
|
||||
<input
|
||||
class="nr h-[60rpx] w-full"
|
||||
:value="userInfo.nickname"
|
||||
name="nickname"
|
||||
type="nickname"
|
||||
placeholder="请输入昵称"
|
||||
/>
|
||||
</u-form-item>
|
||||
<view class="mt-[80rpx]">
|
||||
<button
|
||||
class="bg-primary text-white w-full h-[80rpx] !text-lg !leading-[80rpx] rounded-full"
|
||||
form-type="submit"
|
||||
size="mini"
|
||||
hover-class="none"
|
||||
>
|
||||
确定
|
||||
</button>
|
||||
</view>
|
||||
</form>
|
||||
</view>
|
||||
</u-popup>
|
||||
|
||||
<!-- 账号修改组件 -->
|
||||
<u-popup v-model="showUserName" :closeable="true" mode="center" border-radius="20">
|
||||
<view class="px-[50rpx] py-[40rpx] bg-white" style="width: 85vw">
|
||||
<view class="mb-[70rpx] text-xl text-center">修改账号</view>
|
||||
<u-form-item borderBottom>
|
||||
<u-input
|
||||
class="flex-1"
|
||||
v-model="newUsername"
|
||||
placeholder="请输入账号"
|
||||
:border="false"
|
||||
/>
|
||||
</u-form-item>
|
||||
<view class="mt-[80rpx]">
|
||||
<u-button @click="changeUserNameConfirm" type="primary" shape="circle">
|
||||
确定
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
|
||||
<!-- 性别修改组件 -->
|
||||
<u-picker
|
||||
mode="selector"
|
||||
v-model="showPicker"
|
||||
confirm-color="#4173FF"
|
||||
:default-selector="[0]"
|
||||
:range="sexList"
|
||||
@confirm="changeSexConfirm"
|
||||
/>
|
||||
|
||||
<!-- 账号修改组件 -->
|
||||
<u-popup v-model="showMobilePop" :closeable="true" mode="center" border-radius="20">
|
||||
<view class="px-[50rpx] py-[40rpx] bg-white" style="width: 85vw">
|
||||
<view class="mb-[70rpx] text-xl text-center">{{ userInfo?.mobile == '' ? '绑定手机号' : '更换手机号' }}</view>
|
||||
<u-form-item borderBottom>
|
||||
<u-input
|
||||
class="flex-1"
|
||||
v-model="newMobile"
|
||||
placeholder="请输入新的手机号码"
|
||||
:border="false"
|
||||
/>
|
||||
</u-form-item>
|
||||
<u-form-item borderBottom>
|
||||
<u-input
|
||||
class="flex-1"
|
||||
v-model="mobileCode"
|
||||
placeholder="请输入验证码"
|
||||
:border="false"
|
||||
/>
|
||||
<view
|
||||
class="border-l border-solid border-0 border-light pl-3 text-muted leading-4 ml-3 w-[180rpx]"
|
||||
@click="sendSms"
|
||||
>
|
||||
<u-verification-code
|
||||
ref="uCodeRef"
|
||||
:seconds="60"
|
||||
@change="codeChange"
|
||||
change-text="x秒"
|
||||
/>
|
||||
{{ codeTips }}
|
||||
</view>
|
||||
</u-form-item>
|
||||
<view class="mt-[80rpx]">
|
||||
<u-button @click="changeCodeMobile" type="primary" shape="circle"> 确定 </u-button>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef } from 'vue'
|
||||
import { onShow, onUnload } from '@dcloudio/uni-app'
|
||||
import { getUserInfo, userEdit, userBindMobile, userMnpMobile } from '@/api/user'
|
||||
import { smsSend } from '@/api/app'
|
||||
import { FieldType, SMSEnum } from '@/enums/appEnums'
|
||||
|
||||
// 用户信息
|
||||
const userInfo = ref<any>({})
|
||||
// 用户信息的枚举
|
||||
const fieldType = ref(FieldType.NONE)
|
||||
//选择性别数据
|
||||
const sexList = ref<Array<string> | null>(['男', '女'])
|
||||
|
||||
//显示昵称弹窗
|
||||
const showNickName = ref<boolean | null>(false)
|
||||
//显示账户弹窗
|
||||
const showUserName = ref<boolean | null>(false)
|
||||
//显示性别选择弹窗
|
||||
const showPicker = ref<boolean | null>(false)
|
||||
// 显示手机号验证码调整弹窗 非小程序才需要
|
||||
const showMobilePop = ref<boolean | null>(false)
|
||||
|
||||
//新昵称
|
||||
const newNickname = ref<string>('')
|
||||
//新账号
|
||||
const newUsername = ref<string>('')
|
||||
//新的手机号码
|
||||
const newMobile = ref<string>('')
|
||||
|
||||
//修改手机验证码
|
||||
const mobileCode = ref<string>('')
|
||||
const codeTips = ref('')
|
||||
const uCodeRef = shallowRef()
|
||||
|
||||
// 获取用户信息
|
||||
const getUser = async (): Promise<void> => {
|
||||
userInfo.value = await getUserInfo()
|
||||
}
|
||||
|
||||
// 获取验证码显示字段
|
||||
const codeChange = (text: string) => {
|
||||
codeTips.value = text
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
const sendSms = async () => {
|
||||
if (!newMobile.value) return uni.$u.toast('请输入新的手机号码')
|
||||
if (uCodeRef.value?.canGetCode) {
|
||||
await smsSend({
|
||||
scene: userInfo.value.mobile ? SMSEnum.CHANGE_MOBILE : SMSEnum.BIND_MOBILE,
|
||||
mobile: newMobile.value
|
||||
})
|
||||
uni.$u.toast('发送成功')
|
||||
uCodeRef.value?.start()
|
||||
}
|
||||
}
|
||||
|
||||
const handleAvatarChange = (value) => {
|
||||
fieldType.value = FieldType.AVATAR
|
||||
setUserInfoFun(value)
|
||||
}
|
||||
|
||||
// 验证码修改手机号-非微信小程序
|
||||
const changeCodeMobile = async () => {
|
||||
await userBindMobile({
|
||||
type: userInfo.value.mobile ? 'change' : 'bind',
|
||||
mobile: newMobile.value,
|
||||
code: mobileCode.value
|
||||
})
|
||||
uni.$u.toast('操作成功')
|
||||
showMobilePop.value = false
|
||||
getUser()
|
||||
}
|
||||
|
||||
// 修改用户信息
|
||||
const setUserInfoFun = async (value: string): Promise<void> => {
|
||||
await userEdit({
|
||||
field: fieldType.value,
|
||||
value: value
|
||||
})
|
||||
uni.$u.toast('操作成功')
|
||||
getUser()
|
||||
}
|
||||
|
||||
// 显示修改用户性别弹窗
|
||||
const changeSex = () => {
|
||||
showPicker.value = true
|
||||
fieldType.value = FieldType.SEX
|
||||
}
|
||||
|
||||
// 修改用户性别
|
||||
const changeSexConfirm = (value) => {
|
||||
setUserInfoFun(value[0] + 1)
|
||||
showPicker.value = false
|
||||
}
|
||||
|
||||
// 修改用户账号
|
||||
const changeUserNameConfirm = () => {
|
||||
if (newUsername.value == '') return uni.$u.toast('账号不能为空')
|
||||
if (newUsername.value.length > 10) return uni.$u.toast('账号长度不得超过十位数')
|
||||
|
||||
fieldType.value = FieldType.USERNAME
|
||||
setUserInfoFun(newUsername.value)
|
||||
showUserName.value = false
|
||||
}
|
||||
|
||||
// 修改用户昵称
|
||||
const changeNameConfirm = async (e: any) => {
|
||||
newNickname.value = e.detail.value.nickname
|
||||
if (newNickname.value == '') return uni.$u.toast('昵称不能为空')
|
||||
if (newNickname.value.length > 10) return uni.$u.toast('昵称长度不得超过十位数')
|
||||
fieldType.value = FieldType.NICKNAME
|
||||
await setUserInfoFun(newNickname.value)
|
||||
|
||||
showNickName.value = false
|
||||
}
|
||||
|
||||
// 微信小程序 绑定||修改用户手机号
|
||||
const getPhoneNumber = async (e): Promise<void> => {
|
||||
const { encryptedData, iv, code } = e.detail
|
||||
const data = {
|
||||
code,
|
||||
encrypted_data: encryptedData,
|
||||
iv
|
||||
}
|
||||
if (encryptedData) {
|
||||
await userMnpMobile({
|
||||
...data
|
||||
})
|
||||
uni.$u.toast('操作成功')
|
||||
getUser()
|
||||
}
|
||||
}
|
||||
|
||||
const goPage = (url: string) => {
|
||||
uni.navigateTo({
|
||||
url: url
|
||||
})
|
||||
}
|
||||
|
||||
onShow(async () => {
|
||||
getUser()
|
||||
})
|
||||
|
||||
onUnload(() => {})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.header {
|
||||
width: 100%;
|
||||
|
||||
image {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
margin-top: 2rpx;
|
||||
padding: 30rpx;
|
||||
background-color: #ffffff;
|
||||
|
||||
.label {
|
||||
width: 150rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<page-meta :page-style="$theme.pageStyle">
|
||||
<!-- #ifndef H5 -->
|
||||
<navigation-bar
|
||||
:front-color="$theme.navColor"
|
||||
:background-color="$theme.navBgColor"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<view class="user-set">
|
||||
<navigator :url="`/pages/user_data/user_data`">
|
||||
<view class="item flex bg-white mt-[20rpx]">
|
||||
<u-avatar :src="userInfo.avatar" shape="square" :size="100"></u-avatar>
|
||||
<view class="ml-[20rpx] flex flex-1 justify-between items-center">
|
||||
<view>
|
||||
<view class="mb-[15rpx] text-xl font-medium">{{ userInfo.nickname }}</view>
|
||||
<view class="text-content text-xs">账号:{{ userInfo.account }}</view>
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="#666"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</navigator>
|
||||
<view
|
||||
class="item bg-white mt-[20rpx] btn-border flex flex-1 justify-between"
|
||||
@click="handlePwd"
|
||||
>
|
||||
<view class="">登录密码</view>
|
||||
<u-icon name="arrow-right" color="#666"></u-icon>
|
||||
</view>
|
||||
<!-- #ifdef H5 || MP-WEIXIN -->
|
||||
<view
|
||||
v-if="isWeixin"
|
||||
class="item bg-white flex flex-1 justify-between"
|
||||
@click="bindWechatLock"
|
||||
>
|
||||
<view class="">绑定微信</view>
|
||||
<view class="flex justify-between">
|
||||
<view class="text-muted mr-[20rpx]">
|
||||
{{ userInfo.is_auth ? '已绑定' : '未绑定' }}
|
||||
</view>
|
||||
<u-icon v-if="userInfo.is_auth == 0" name="arrow-right" color="#666"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<navigator :url="`/pages/agreement/agreement?type=${AgreementEnum.PRIVACY}`">
|
||||
<view class="item bg-white mt-[20rpx] btn-border flex flex-1 justify-between">
|
||||
<view class="">隐私政策</view>
|
||||
<u-icon name="arrow-right" color="#666"></u-icon>
|
||||
</view>
|
||||
</navigator>
|
||||
<navigator :url="`/pages/agreement/agreement?type=${AgreementEnum.SERVICE}`">
|
||||
<view class="item bg-white btn-border flex flex-1 justify-between">
|
||||
<view class="">服务协议</view>
|
||||
<u-icon name="arrow-right" color="#666"></u-icon>
|
||||
</view>
|
||||
</navigator>
|
||||
<navigator url="/pages/as_us/as_us">
|
||||
<view class="item bg-white flex flex-1 justify-between">
|
||||
<view class="">关于我们</view>
|
||||
<view class="flex justify-between">
|
||||
<view class="text-muted mr-[20rpx]">
|
||||
{{ appStore.config.version }}
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="#666"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</navigator>
|
||||
|
||||
<view class="mt-[60rpx] mx-[26rpx]">
|
||||
<u-button type="primary" shape="circle" @click="showLogout = true"> 退出登录</u-button>
|
||||
</view>
|
||||
|
||||
<u-action-sheet
|
||||
:list="list"
|
||||
v-model="show"
|
||||
@click="handleClick"
|
||||
:safe-area-inset-bottom="true"
|
||||
></u-action-sheet>
|
||||
|
||||
<u-popup
|
||||
class="pay-popup"
|
||||
v-model="showLogout"
|
||||
round
|
||||
mode="center"
|
||||
borderRadius="10"
|
||||
:maskCloseAble="false"
|
||||
>
|
||||
<view class="content bg-white w-[560rpx] p-[40rpx]">
|
||||
<view class="text-2xl font-medium text-center"> 温馨提示</view>
|
||||
<view class="pt-[30rpx] pb-[40rpx]">
|
||||
<view> 是否清除当前登录信息,退出登录?</view>
|
||||
</view>
|
||||
<view class="flex">
|
||||
<view class="flex-1 mr-[20rpx]">
|
||||
<u-button
|
||||
shape="circle"
|
||||
type="primary"
|
||||
plain
|
||||
size="medium"
|
||||
hover-class="none"
|
||||
:customStyle="{ width: '100%' }"
|
||||
@click="showLogout = false"
|
||||
>
|
||||
取消
|
||||
</u-button>
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<u-button
|
||||
shape="circle"
|
||||
type="primary"
|
||||
size="medium"
|
||||
hover-class="none"
|
||||
:customStyle="{ width: '100%' }"
|
||||
@click="logoutHandle"
|
||||
>
|
||||
确认
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {onLoad, onShow} from '@dcloudio/uni-app'
|
||||
import {computed, ref} from 'vue'
|
||||
import {useAppStore} from '@/stores/app'
|
||||
import {useUserStore} from '@/stores/user'
|
||||
import {AgreementEnum} from '@/enums/agreementEnums'
|
||||
import {isWeixinClient} from '@/utils/client'
|
||||
import {mnpAuthBind, oaAuthBind} from '@/api/account'
|
||||
import {useLockFn} from '@/hooks/useLockFn'
|
||||
import {useRouter} from "uniapp-router-next";
|
||||
// #ifdef H5
|
||||
import wechatOa from '@/utils/wechat'
|
||||
// #endif
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const userStore = useUserStore()
|
||||
const userInfo = computed(() => userStore.userInfo)
|
||||
|
||||
const list = ref([
|
||||
{
|
||||
text: '修改密码'
|
||||
},
|
||||
{
|
||||
text: '忘记密码'
|
||||
}
|
||||
])
|
||||
|
||||
const isWeixin = ref(true)
|
||||
// #ifdef H5
|
||||
isWeixin.value = isWeixinClient()
|
||||
// #endif
|
||||
|
||||
const show = ref(false)
|
||||
const showLogout = ref(false)
|
||||
|
||||
// 修改/忘记密码
|
||||
const handleClick = (index: number) => {
|
||||
switch (index) {
|
||||
case 0:
|
||||
router.navigateTo('/pages/change_password/change_password')
|
||||
break
|
||||
case 1:
|
||||
router.navigateTo('/pages/forget_pwd/forget_pwd')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handlePwd = () => {
|
||||
if (!userInfo.value.has_password)
|
||||
return router.navigateTo('/pages/change_password/change_password?type=set')
|
||||
show.value = true
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const logoutHandle = () => {
|
||||
userStore.logout()
|
||||
router.redirectTo('/pages/login/login')
|
||||
}
|
||||
|
||||
const bindWechat = async () => {
|
||||
if (userInfo.value.is_auth) return
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: '请稍后...'
|
||||
})
|
||||
// #ifdef MP-WEIXIN
|
||||
const {code}: any = await uni.login({
|
||||
provider: 'weixin'
|
||||
})
|
||||
await mnpAuthBind({
|
||||
code: code
|
||||
})
|
||||
//#endif
|
||||
// #ifdef H5
|
||||
if (isWeixin.value) {
|
||||
wechatOa.getUrl()
|
||||
}
|
||||
// #endif
|
||||
await userStore.getUser()
|
||||
uni.hideLoading()
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
uni.$u.toast(e)
|
||||
}
|
||||
}
|
||||
const {lockFn: bindWechatLock} = useLockFn(bindWechat)
|
||||
|
||||
onShow(() => {
|
||||
userStore.getUser()
|
||||
})
|
||||
|
||||
onLoad(async (options) => {
|
||||
// #ifdef H5
|
||||
const {code} = options
|
||||
if (!isWeixin.value) return
|
||||
if (code) {
|
||||
uni.showLoading({
|
||||
title: '请稍后...'
|
||||
})
|
||||
try {
|
||||
await oaAuthBind({code})
|
||||
await userStore.getUser()
|
||||
} catch (error) {
|
||||
}
|
||||
//用于清空code
|
||||
router.redirectTo('/pages/user_set/user_set')
|
||||
}
|
||||
|
||||
// #endif
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-set {
|
||||
.item {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.btn-border {
|
||||
border-bottom: 2rpx solid #f8f8f8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<web-view :src="url" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const url = ref('')
|
||||
|
||||
onLoad((options) => {
|
||||
url.value = decodeURIComponent(options.url!)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { isFunction } from '@vue/shared'
|
||||
import { App } from 'vue'
|
||||
const modules = import.meta.globEager('./modules/**/*.ts')
|
||||
|
||||
export default {
|
||||
install: (app: App) => {
|
||||
for (const module of Object.values(modules)) {
|
||||
const fun = module.default
|
||||
isFunction(fun) && fun(app)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { App } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
const pinia = createPinia()
|
||||
export default (app: App) => {
|
||||
app.use(pinia)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { App } from 'vue'
|
||||
import uView from '@/uni_modules/vk-uview-ui'
|
||||
|
||||
export default (app: App) => {
|
||||
// 使用 uView UI
|
||||
app.use(uView)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// url中携带参数vconsole == vconsoleMd5时打开Vconsole,可以用于手机浏览器中调试
|
||||
// 如/mobile/pages/user/user?vconsole=47b1e3a9d33e6064e58cc4796c708447,此时在个人中心页面打开vconsole
|
||||
|
||||
const vconsoleMd5 = '47b1e3a9d33e6064e58cc4796c708447'
|
||||
export default async () => {
|
||||
// #ifdef H5
|
||||
const url = new URL(location.href)
|
||||
const searchParams = new URLSearchParams(url.search)
|
||||
const vconsole = searchParams.get('vconsole')
|
||||
if (vconsole == vconsoleMd5) {
|
||||
const module: any = await import('vconsole')
|
||||
const Vconsole = module.default
|
||||
const vConsole = new Vconsole()
|
||||
return vConsole
|
||||
}
|
||||
|
||||
// #endif
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import routes from 'uni-router-routes'
|
||||
import { createRouter } from 'uniapp-router-next'
|
||||
|
||||
import { ClientEnum } from '@/enums/appEnums'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { client } from '@/utils/client'
|
||||
// #ifdef H5
|
||||
import wechatOa from '@/utils/wechat'
|
||||
// #endif
|
||||
import cache from '@/utils/cache'
|
||||
import { BACK_URL } from '@/enums/constantEnums'
|
||||
|
||||
const router = createRouter({
|
||||
routes: [
|
||||
...routes,
|
||||
{
|
||||
path: '*',
|
||||
redirect() {
|
||||
return {
|
||||
name: '404'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
debug: import.meta.env.DEV,
|
||||
//@ts-ignore
|
||||
platform: process.env.UNI_PLATFORM,
|
||||
h5: {}
|
||||
})
|
||||
|
||||
//存储登陆前的页面
|
||||
let isFirstEach = true
|
||||
router.beforeEach(async (to, from) => {
|
||||
//保存第一次进来时的页面路径(需要登陆才能访问的页面)
|
||||
if (isFirstEach) {
|
||||
const userStore = useUserStore()
|
||||
if (!userStore.isLogin && !to.meta.white) {
|
||||
cache.set(BACK_URL, to.fullPath)
|
||||
}
|
||||
isFirstEach = false
|
||||
}
|
||||
})
|
||||
router.afterEach((to, from) => {
|
||||
const userStore = useUserStore()
|
||||
if (!userStore.isLogin && !to.meta.white) {
|
||||
cache.set(BACK_URL, to.fullPath)
|
||||
}
|
||||
})
|
||||
|
||||
// 登录拦截
|
||||
router.beforeEach(async (to, from) => {
|
||||
const userStore = useUserStore();
|
||||
if (!userStore.isLogin && to.meta.auth) {
|
||||
return '/pages/login/login'
|
||||
}
|
||||
})
|
||||
|
||||
// #ifdef H5
|
||||
//用于收集微信公众号的授权的code,并清除路径上微信带的参数
|
||||
router.beforeEach(async (to, form) => {
|
||||
const { code, state, scene } = to.query
|
||||
|
||||
if (code && state && scene) {
|
||||
wechatOa.setAuthData({
|
||||
code,
|
||||
scene
|
||||
})
|
||||
//收集完删除路径上的参数
|
||||
delete to.query.code
|
||||
delete to.query.state
|
||||
return {
|
||||
path: to.path,
|
||||
force: true,
|
||||
navType: 'reLaunch',
|
||||
query: to.query
|
||||
}
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
router.afterEach((to, from) => {
|
||||
setTimeout(async () => {
|
||||
if (client == ClientEnum.OA_WEIXIN && !to.meta.webview) {
|
||||
// jssdk配置
|
||||
await wechatOa.config()
|
||||
}
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
export default router
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 679 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 972 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 744 B |
|
After Width: | Height: | Size: 971 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,31 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { getConfig } from '@/api/app'
|
||||
|
||||
interface AppSate {
|
||||
config: Record<string, any>
|
||||
}
|
||||
export const useAppStore = defineStore({
|
||||
id: 'appStore',
|
||||
state: (): AppSate => ({
|
||||
config: {
|
||||
domain: ''
|
||||
}
|
||||
}),
|
||||
getters: {
|
||||
getWebsiteConfig: (state) => state.config.website || {},
|
||||
getLoginConfig: (state) => state.config.login || {},
|
||||
getTabbarConfig: (state) => state.config.tabbar || [],
|
||||
getStyleConfig: (state) => state.config.style || {},
|
||||
getH5Config: (state) => state.config.webPage || {},
|
||||
getCopyrightConfig: (state) => state.config.copyright || [],
|
||||
},
|
||||
actions: {
|
||||
getImageUrl(url: string) {
|
||||
return url.indexOf('http') ? `${this.config.domain}${url}` : url
|
||||
},
|
||||
async getConfig() {
|
||||
const data = await getConfig()
|
||||
this.config = data
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
import { getDecorate } from '@/api/shop'
|
||||
import { generateVars } from '@/utils/theme'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
interface ThemeStore {
|
||||
primaryColor: string
|
||||
minorColor: string
|
||||
btnColor: string
|
||||
navColor: string
|
||||
navBgColor: string
|
||||
vars: string
|
||||
}
|
||||
export const useThemeStore = defineStore({
|
||||
id: 'themeStore',
|
||||
state: (): ThemeStore => ({
|
||||
primaryColor: '',
|
||||
minorColor: '',
|
||||
btnColor: 'white',
|
||||
navColor: '#000000',
|
||||
navBgColor: '#ffffff',
|
||||
vars: ''
|
||||
}),
|
||||
actions: {
|
||||
async getTheme() {
|
||||
const data = await getDecorate({
|
||||
id: 5
|
||||
})
|
||||
const {
|
||||
themeColor1,
|
||||
themeColor2,
|
||||
buttonColor,
|
||||
navigationBarColor,
|
||||
topTextColor
|
||||
} = JSON.parse(data.data)
|
||||
this.primaryColor = themeColor1
|
||||
this.minorColor = themeColor2
|
||||
this.btnColor = buttonColor
|
||||
this.navColor = topTextColor === 'white' ? '#ffffff' : '#000000'
|
||||
this.navBgColor = navigationBarColor || themeColor1
|
||||
this.vars = generateVars(
|
||||
{
|
||||
primary: themeColor1
|
||||
},
|
||||
{
|
||||
'--color-minor': themeColor2,
|
||||
'--color-btn-text': buttonColor
|
||||
}
|
||||
)
|
||||
},
|
||||
setTheme(color: string) {
|
||||
this.primaryColor = color
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
import { getUserCenter } from '@/api/user'
|
||||
import { TOKEN_KEY } from '@/enums/constantEnums'
|
||||
import cache from '@/utils/cache'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
interface UserSate {
|
||||
userInfo: Record<string, any>
|
||||
token: string | null
|
||||
temToken: string | null
|
||||
}
|
||||
export const useUserStore = defineStore({
|
||||
id: 'userStore',
|
||||
state: (): UserSate => ({
|
||||
userInfo: {},
|
||||
token: cache.get(TOKEN_KEY) || null,
|
||||
temToken: null
|
||||
}),
|
||||
getters: {
|
||||
isLogin: (state) => !!state.token
|
||||
},
|
||||
actions: {
|
||||
async getUser() {
|
||||
const data = await getUserCenter({
|
||||
token: this.token || this.temToken
|
||||
})
|
||||
this.userInfo = data
|
||||
},
|
||||
login(token: string) {
|
||||
this.token = token
|
||||
cache.set(TOKEN_KEY, token)
|
||||
},
|
||||
logout() {
|
||||
this.token = ''
|
||||
this.userInfo = {}
|
||||
cache.remove(TOKEN_KEY)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
@import './tailwind.css';
|
||||
@import './public.scss';
|
||||
@import '../uni_modules/vk-uview-ui/index.scss';
|
||||
@@ -0,0 +1,16 @@
|
||||
page {
|
||||
background-color: $u-bg-color;
|
||||
font-size: 28rpx;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-family: theme('fontFamily.sans')
|
||||
//font-family: PingFang SC;
|
||||
}
|
||||
|
||||
uni-modal {
|
||||
z-index: 999999 !important;
|
||||
}
|
||||
|
||||
button::after {
|
||||
border: initial;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/* #ifdef H5 */
|
||||
@tailwind base;
|
||||
/* #endif */
|
||||
|
||||
@tailwind utilities;
|
||||
@@ -0,0 +1,41 @@
|
||||
// 替换uview颜色变量
|
||||
$u-main-color: theme("colors.main");
|
||||
$u-content-color: theme("colors.content");
|
||||
$u-tips-color: theme("colors.muted");
|
||||
$u-light-color: theme("colors.light");
|
||||
$u-border-color: theme("colors.light");
|
||||
$u-bg-color: theme("colors.page");
|
||||
$u-disabled-color: theme("colors.disabled");
|
||||
$u-minor-color: theme("colors[minor]");
|
||||
$u-type-primary: theme("colors.primary.DEFAULT");
|
||||
$u-type-primary-disabled: theme("colors.primary[light-3]");
|
||||
$u-type-primary-dark: theme("colors.primary[dark-2]");
|
||||
$u-type-primary-light: theme("colors.primary[light-9]");
|
||||
|
||||
$u-type-warning: theme("colors.warning.DEFAULT");
|
||||
$u-type-warning-disabled: theme("colors.warning[light-3]");
|
||||
$u-type-warning-dark: theme("colors.warning[dark-2]");
|
||||
$u-type-warning-light: theme("colors.warning[light-9]");
|
||||
|
||||
$u-type-success: theme("colors.success.DEFAULT");
|
||||
$u-type-success-disabled: theme("colors.success[light-3]");
|
||||
$u-type-success-dark: theme("colors.success[dark-2]");
|
||||
$u-type-success-light: theme("colors.success[light-9]");
|
||||
|
||||
$u-type-error: theme("colors.error.DEFAULT");
|
||||
$u-type-error-disabled: theme("colors.error[light-3]");
|
||||
$u-type-error-dark: theme("colors.error[dark-2]");
|
||||
$u-type-error-light: theme("colors.error[light-9]");
|
||||
|
||||
$u-type-info: theme("colors.info.DEFAULT");
|
||||
$u-type-info-disabled: theme("colors.info[light-3]");
|
||||
$u-type-info-dark: theme("colors.info[dark-2]");
|
||||
$u-type-info-light: theme("colors.info[light-9]");
|
||||
|
||||
$u-form-item-height: 60rpx;
|
||||
$u-form-item-border-color: theme("colors.light");
|
||||
|
||||
$-color-white: theme("colors.white");
|
||||
$-color-black: theme("colors.black");
|
||||
|
||||
$u-color-btn-text: theme("colors[btn-text]");
|
||||
@@ -0,0 +1,118 @@
|
||||
## 1.3.12(2022-08-30)
|
||||
* 【优化】`u-keyboard` 组件内部细节。
|
||||
## 1.3.11(2022-08-30)
|
||||
* 【修复】`u-subsection` 组件的 `list` 属性不支持动态修改的问题。
|
||||
## 1.3.10(2022-07-30)
|
||||
* 【优化】上传组件部分细节
|
||||
## 1.3.9(2022-07-07)
|
||||
* 【更新】省市区数据源
|
||||
* 【优化】`u-subsection` 组件支持在右上角显示数字角标
|
||||
```html
|
||||
<template>
|
||||
<u-subsection :list="list"></u-subsection>
|
||||
</template>
|
||||
```
|
||||
|
||||
```js
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
list: [
|
||||
{
|
||||
name: '待发货',
|
||||
num: 10
|
||||
},
|
||||
{
|
||||
name: '待付款',
|
||||
num: 5
|
||||
},
|
||||
{
|
||||
name: '待评价',
|
||||
num: 15
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
## 1.3.8(2022-06-13)
|
||||
* 【优化】组件 `u-icon`,使之更方便的兼容第三方icon(满足规则自动计算customPrefix)
|
||||
**规则如下:**
|
||||
* 当 `name` 中包含 `-icon-` 字符串时
|
||||
* 如 `vk-icon-goods`,则组件的 `customPrefix` 属性自动识别为 `vk-icon` ,`name`属性 自动识别为 `goods`
|
||||
* 如 `vk-2-icon-goods-list`,则组件的 `customPrefix` 属性自动识别为 `vk-2-icon` ,`name`属性 自动识别为 `goods-list`
|
||||
## 1.3.7(2022-06-10)
|
||||
* 【优化】组件 `u-action-sheet` `u-calendar` `u-dropdown-item` `u-field` `u-input` `u-keyboard` `u-modal` `u-radio-group` `u-rate` `u-search` `u-slider` `u-switch` `u-tabbar` `u-waterfall` 在 `vue3` 模式下的细节问题。
|
||||
## 1.3.6(2022-06-10)
|
||||
* 【优化】组件 `u-action-sheet` `u-calendar` `u-dropdown-item` `u-field` `u-input` `u-keyboard` `u-modal` `u-radio-group` `u-rate` `u-search` `u-slider` `u-switch` `u-tabbar` `u-waterfall` 在 `vue3` 模式下的细节问题。
|
||||
## 1.3.5(2022-05-28)
|
||||
* 【优化】组件 `u-mask` `u-popup` `u-select` `u-modal` `u-keyboard` `u-calendar` `u-action-sheet` `u-picker` 均新增 `blur` 属性,可用于设置弹出遮罩的模糊度,默认为0(不模糊)
|
||||
* 
|
||||
## 1.3.4(2022-05-03)
|
||||
* 【修复】`u-tabs` 组件细节问题。
|
||||
## 1.1.4(2022-03-22)
|
||||
* 【修复】`u-field` 组件 `arrowDirection` 属性无效的问题。
|
||||
## 1.1.3(2022-03-21)
|
||||
* 【优化】部分细节。
|
||||
## 1.1.2(2022-03-21)
|
||||
* 【优化】部分细节。
|
||||
## 1.1.1(2022-03-17)
|
||||
* 【优化】部分细节。
|
||||
## 1.1.0(2022-03-12)
|
||||
* 【重要】`u-picker` 组件新增 `regionDiscern` 方法 智能识别省市区街道地址
|
||||
如将字符串 `浙江省杭州市西湖区希望路1333弄是啊我庭12号楼1203` 中识别为
|
||||
```json
|
||||
{
|
||||
"province": {
|
||||
"code": "330000",
|
||||
"name": "浙江省"
|
||||
},
|
||||
"city": {
|
||||
"code": "330100",
|
||||
"name": "杭州市"
|
||||
},
|
||||
"area": {
|
||||
"code": "330106",
|
||||
"name": "西湖区"
|
||||
},
|
||||
"address": "龙井路1号",
|
||||
"formatted_address": "浙江省杭州市西湖区龙井路1号"
|
||||
}
|
||||
```
|
||||
而组件的 `addressDiscern` 方法还可以识别收货信息,如 `张三 13888888888 上海市嘉定区希望路1333弄是啊我庭12号楼1203` 中识别姓名、手机号、地址(支持多种格式)
|
||||
## 1.0.13(2022-03-12)
|
||||
* 【优化】部分细节。
|
||||
## 1.0.12(2022-03-09)
|
||||
* 【修复】`u-radio-group` 在 vue3 模式下,设置默认值可能会无效的问题。
|
||||
## 1.0.11(2022-03-07)
|
||||
* 【优化】部分细节。
|
||||
## 1.0.10(2022-03-05)
|
||||
* 【修复】`u-radio` 中的值相等的判断 == 改为 ===
|
||||
* 【优化】部分注释的错别字。
|
||||
## 1.0.9(2022-03-03)
|
||||
* 【修复】`u-parse` 在 vue3模式下编译到app无法正常显示的问题。
|
||||
## 1.0.8(2022-02-26)
|
||||
* 【优化】`u-form` 组件新增2个属性 `inputAlign` 和 `clearable` 用于统一设置表单内所有 `u-input` 组件的对应属性默认值
|
||||
* 【优化】更新城市数据源信息
|
||||
## 1.0.7(2022-02-25)
|
||||
* 【重要】`u-picker` 组件新增 `addressDiscern` 方法 智能识别收货信息
|
||||
|
||||
如在 `张三 13888888888 上海市嘉定区希望路1333弄是啊我庭12号楼1203` 中识别姓名、手机号、地址(支持多种格式)
|
||||
即使这样的字符串也能识别 `!!!!~~~$张三~~~上海市嘉定区希望路1333弄是啊我庭12号楼1203【【【【13888888888】`
|
||||
## 1.0.6(2022-02-24)
|
||||
* 【优化】`u-form-item` 组件的 `prop` 属性支持 a.b 形式
|
||||
## 1.0.5(2022-01-11)
|
||||
* 【修复】`u-sticky` 组件 在微信小程序中无法正常吸顶的问题
|
||||
## 1.0.4(2021-12-31)
|
||||
* 【优化】`u-dropdown-item` 组件 0和"" 无法区分的问题。
|
||||
* 【修复】`u-modal` 在Vue3版本中使用了mask-close-able属性无效的问题
|
||||
## 1.0.3(2021-12-20)
|
||||
【优化】u-icon在微信小程序下可能会显示null字符串的问题
|
||||
## 1.0.2(2021-12-09)
|
||||
* 1、【优化】`u-button` 组件新增 `timerId` 属性
|
||||
* 之前的效果是:所有按钮一定时间内只能点击1次(`共用计算时间`)导致点击按钮A后无法马上点击按钮B
|
||||
* 优化的效果是:每个按钮一定时间内只能点击1次(`分开计算时间`)且支持设置相同的 timerId 来达到指定按钮 `共用计算时间`
|
||||
## 1.0.1(2021-11-22)
|
||||
* 修复 u-parse 组件在微信小程序上的显示问题。
|
||||
## 1.0.0(2021-11-18)
|
||||
uView Vue3.0 横空出世,继承uView1.0意志,再战江湖,风云再起!by vk 2021-11-18
|
||||
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<u-popup
|
||||
:blur="blur"
|
||||
mode="bottom"
|
||||
:border-radius="borderRadius"
|
||||
:popup="false"
|
||||
v-model="popupValue"
|
||||
:maskCloseAble="maskCloseAble"
|
||||
length="auto"
|
||||
:safeAreaInsetBottom="safeAreaInsetBottom"
|
||||
@close="popupClose"
|
||||
:z-index="uZIndex"
|
||||
>
|
||||
<view class="u-tips u-border-bottom" v-if="tips.text" :style="[tipsStyle]">
|
||||
<text>{{ tips.text }}</text>
|
||||
</view>
|
||||
<block v-for="(item, index) in list" :key="index">
|
||||
<view
|
||||
@touchmove.stop.prevent
|
||||
@tap="itemClick(index)"
|
||||
:style="[itemStyle(index)]"
|
||||
class="u-action-sheet-item u-line-1"
|
||||
:class="[index < list.length - 1 ? 'u-border-bottom' : '']"
|
||||
:hover-stay-time="150"
|
||||
>
|
||||
<text>{{ item[labelName] }}</text>
|
||||
<text class="u-action-sheet-item__subtext u-line-1" v-if="item.subText">
|
||||
{{ item.subText }}
|
||||
</text>
|
||||
</view>
|
||||
</block>
|
||||
<view class="u-gab" v-if="cancelBtn"></view>
|
||||
<view
|
||||
@touchmove.stop.prevent
|
||||
class="u-actionsheet-cancel u-action-sheet-item"
|
||||
hover-class="u-hover-class"
|
||||
:hover-stay-time="150"
|
||||
v-if="cancelBtn"
|
||||
@tap="close"
|
||||
>
|
||||
{{ cancelText }}
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* actionSheet 操作菜单
|
||||
* @description 本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI,配置更加灵活,所有平台都表现一致。
|
||||
* @tutorial https://www.uviewui.com/components/actionSheet.html
|
||||
* @property {Array<Object>} list 按钮的文字数组,见官方文档示例
|
||||
* @property {Object} tips 顶部的提示文字,见官方文档示例
|
||||
* @property {String} cancel-text 取消按钮的提示文字
|
||||
* @property {Boolean} cancel-btn 是否显示底部的取消按钮(默认true)
|
||||
* @property {Number String} border-radius 弹出部分顶部左右的圆角值,单位rpx(默认0)
|
||||
* @property {Boolean} mask-close-able 点击遮罩是否可以关闭(默认true)
|
||||
* @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
|
||||
* @property {Number String} z-index z-index值(默认1075)
|
||||
* @property {String} cancel-text 取消按钮的提示文字
|
||||
* @event {Function} click 点击ActionSheet列表项时触发
|
||||
* @event {Function} close 点击取消按钮时触发
|
||||
* @example <u-action-sheet :list="list" @click="click" v-model="show"></u-action-sheet>
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: "u-action-sheet",
|
||||
emits: ["update:modelValue", "input", "click", "close"],
|
||||
props: {
|
||||
// 通过双向绑定控制组件的弹出与收起
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 点击遮罩是否可以关闭actionsheet
|
||||
maskCloseAble: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 按钮的文字数组,可以自定义颜色和字体大小,字体单位为rpx
|
||||
list: {
|
||||
type: Array,
|
||||
default() {
|
||||
// 如下
|
||||
// return [{
|
||||
// text: '确定',
|
||||
// color: '',
|
||||
// fontSize: ''
|
||||
// }]
|
||||
return [];
|
||||
}
|
||||
},
|
||||
// 顶部的提示文字
|
||||
tips: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
text: "",
|
||||
color: "",
|
||||
fontSize: "26"
|
||||
};
|
||||
}
|
||||
},
|
||||
// 底部的取消按钮
|
||||
cancelBtn: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
|
||||
safeAreaInsetBottom: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 弹出的顶部圆角值
|
||||
borderRadius: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
},
|
||||
// 弹出的z-index值
|
||||
zIndex: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
},
|
||||
// 取消按钮的文字提示
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: "取消"
|
||||
},
|
||||
// 自定义label属性名
|
||||
labelName: {
|
||||
type: String,
|
||||
default: "text"
|
||||
},
|
||||
// 遮罩的模糊度
|
||||
blur: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
valueCom() {
|
||||
// #ifndef VUE3
|
||||
return this.value;
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE3
|
||||
return this.modelValue;
|
||||
// #endif
|
||||
},
|
||||
// 顶部提示的样式
|
||||
tipsStyle() {
|
||||
let style = {};
|
||||
if (this.tips.color) style.color = this.tips.color;
|
||||
if (this.tips.fontSize) style.fontSize = this.tips.fontSize + "rpx";
|
||||
return style;
|
||||
},
|
||||
// 操作项目的样式
|
||||
itemStyle() {
|
||||
return index => {
|
||||
let style = {};
|
||||
if (this.list[index].color) style.color = this.list[index].color;
|
||||
if (this.list[index].fontSize) style.fontSize = this.list[index].fontSize + "rpx";
|
||||
// 选项被禁用的样式
|
||||
if (this.list[index].disabled) style.color = "#c0c4cc";
|
||||
return style;
|
||||
};
|
||||
},
|
||||
uZIndex() {
|
||||
// 如果用户有传递z-index值,优先使用
|
||||
return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
popupValue: false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
valueCom(v1, v2) {
|
||||
this.popupValue = v1;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 点击取消按钮
|
||||
close() {
|
||||
// 发送input事件,并不会作用于父组件,而是要设置组件内部通过props传递的value参数
|
||||
// 这是一个vue发送事件的特殊用法
|
||||
this.popupClose();
|
||||
this.$emit("close");
|
||||
},
|
||||
// 弹窗关闭
|
||||
popupClose() {
|
||||
this.$emit("input", false);
|
||||
this.$emit("update:modelValue", false);
|
||||
},
|
||||
// 点击某一个item
|
||||
itemClick(index) {
|
||||
// disabled的项禁止点击
|
||||
if (this.list[index].disabled) return;
|
||||
this.$emit("click", index);
|
||||
this.$emit("input", false);
|
||||
this.$emit("update:modelValue", false);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-tips {
|
||||
font-size: 26rpx;
|
||||
text-align: center;
|
||||
padding: 34rpx 0;
|
||||
line-height: 1.5;
|
||||
color: $u-tips-color;
|
||||
}
|
||||
|
||||
.u-action-sheet-item {
|
||||
@include vue-flex;
|
||||
line-height: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 32rpx;
|
||||
padding: 34rpx 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.u-action-sheet-item__subtext {
|
||||
font-size: 24rpx;
|
||||
color: $u-tips-color;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.u-gab {
|
||||
height: 12rpx;
|
||||
background-color: rgb(234, 234, 236);
|
||||
}
|
||||
|
||||
.u-actionsheet-cancel {
|
||||
color: $u-main-color;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,257 @@
|
||||
<template>
|
||||
<view class="u-alert-tips" v-if="show" :class="[
|
||||
!show ? 'u-close-alert-tips': '',
|
||||
type ? 'u-alert-tips--bg--' + type + '-light' : '',
|
||||
type ? 'u-alert-tips--border--' + type + '-disabled' : '',
|
||||
]" :style="{
|
||||
backgroundColor: bgColor,
|
||||
borderColor: borderColor
|
||||
}">
|
||||
<view class="u-icon-wrap">
|
||||
<u-icon v-if="showIcon" :name="uIcon" :size="description ? 40 : 32" class="u-icon" :color="uIconType" :custom-style="iconStyle"></u-icon>
|
||||
</view>
|
||||
<view class="u-alert-content" @tap.stop="click">
|
||||
<view class="u-alert-title" :style="[uTitleStyle]">
|
||||
{{title}}
|
||||
</view>
|
||||
<view v-if="description" class="u-alert-desc" :style="[descStyle]">
|
||||
{{description}}
|
||||
</view>
|
||||
</view>
|
||||
<view class="u-icon-wrap">
|
||||
<u-icon @click="close" v-if="closeAble && !closeText" hoverClass="u-type-error-hover-color" name="close" color="#c0c4cc"
|
||||
:size="22" class="u-close-icon" :style="{
|
||||
top: description ? '18rpx' : '24rpx'
|
||||
}"></u-icon>
|
||||
</view>
|
||||
<text v-if="closeAble && closeText" class="u-close-text" :style="{
|
||||
top: description ? '18rpx' : '24rpx'
|
||||
}">{{closeText}}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* alertTips 警告提示
|
||||
* @description 警告提示,展现需要关注的信息
|
||||
* @tutorial https://uviewui.com/components/alertTips.html
|
||||
* @property {String} title 显示的标题文字
|
||||
* @property {String} description 辅助性文字,颜色比title浅一点,字号也小一点,可选
|
||||
* @property {String} type 关闭按钮(默认为叉号icon图标)
|
||||
* @property {String} icon 图标名称
|
||||
* @property {Object} icon-style 图标的样式,对象形式
|
||||
* @property {Object} title-style 标题的样式,对象形式
|
||||
* @property {Object} desc-style 描述的样式,对象形式
|
||||
* @property {String} close-able 用文字替代关闭图标,close-able为true时有效
|
||||
* @property {Boolean} show-icon 是否显示左边的辅助图标
|
||||
* @property {Boolean} show 显示或隐藏组件
|
||||
* @event {Function} click 点击组件时触发
|
||||
* @event {Function} close 点击关闭按钮时触发
|
||||
*/
|
||||
export default {
|
||||
name: 'u-alert-tips',
|
||||
emits: ["click", "close"],
|
||||
props: {
|
||||
// 显示文字
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 主题,success/warning/info/error
|
||||
type: {
|
||||
type: String,
|
||||
default: 'warning'
|
||||
},
|
||||
// 辅助性文字
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否可关闭
|
||||
closeAble: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 关闭按钮自定义文本
|
||||
closeText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否显示图标
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 文字颜色,如果定义了color值,icon会失效
|
||||
color: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 背景颜色
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 边框颜色
|
||||
borderColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否显示
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 左边显示的icon
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// icon的样式
|
||||
iconStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 标题的样式
|
||||
titleStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 描述文字的样式
|
||||
descStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
uTitleStyle() {
|
||||
let style = {};
|
||||
// 如果有描述文字的话,标题进行加粗
|
||||
style.fontWeight = this.description ? 500 : 'normal';
|
||||
// 将用户传入样式对象和style合并,传入的优先级比style高,同属性会被覆盖
|
||||
return this.$u.deepMerge(style, this.titleStyle);
|
||||
},
|
||||
uIcon() {
|
||||
// 如果有设置icon名称就使用,否则根据type主题,推定一个默认的图标
|
||||
return this.icon ? this.icon : this.$u.type2icon(this.type);
|
||||
},
|
||||
uIconType() {
|
||||
// 如果有设置图标的样式,优先使用,没有的话,则用type的样式
|
||||
return Object.keys(this.iconStyle).length ? '' : this.type;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 点击内容
|
||||
click() {
|
||||
this.$emit('click');
|
||||
},
|
||||
// 点击关闭按钮
|
||||
close() {
|
||||
this.$emit('close');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-alert-tips {
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 30rpx;
|
||||
border-radius: 8rpx;
|
||||
position: relative;
|
||||
transition: all 0.3s linear;
|
||||
border: 1px solid #fff;
|
||||
|
||||
&--bg--primary-light {
|
||||
background-color: $u-type-primary-light;
|
||||
}
|
||||
|
||||
&--bg--info-light {
|
||||
background-color: $u-type-info-light;
|
||||
}
|
||||
|
||||
&--bg--success-light {
|
||||
background-color: $u-type-success-light;
|
||||
}
|
||||
|
||||
&--bg--warning-light {
|
||||
background-color: $u-type-warning-light;
|
||||
}
|
||||
|
||||
&--bg--error-light {
|
||||
background-color: $u-type-error-light;
|
||||
}
|
||||
|
||||
&--border--primary-disabled {
|
||||
border-color: $u-type-primary-disabled;
|
||||
}
|
||||
|
||||
&--border--success-disabled {
|
||||
border-color: $u-type-success-disabled;
|
||||
}
|
||||
|
||||
&--border--error-disabled {
|
||||
border-color: $u-type-error-disabled;
|
||||
}
|
||||
|
||||
&--border--warning-disabled {
|
||||
border-color: $u-type-warning-disabled;
|
||||
}
|
||||
|
||||
&--border--info-disabled {
|
||||
border-color: $u-type-info-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
.u-close-alert-tips {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.u-icon {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.u-alert-title {
|
||||
font-size: 28rpx;
|
||||
color: $u-main-color;
|
||||
}
|
||||
|
||||
.u-alert-desc {
|
||||
font-size: 26rpx;
|
||||
text-align: left;
|
||||
color: $u-content-color;
|
||||
}
|
||||
|
||||
.u-close-icon {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
}
|
||||
|
||||
.u-close-hover {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.u-close-text {
|
||||
font-size: 24rpx;
|
||||
color: $u-tips-color;
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,290 @@
|
||||
<template>
|
||||
<view class="content">
|
||||
<view class="cropper-wrapper" :style="{ height: cropperOpt.height + 'px' }">
|
||||
<canvas
|
||||
class="cropper"
|
||||
:disable-scroll="true"
|
||||
@touchstart="touchStart"
|
||||
@touchmove="touchMove"
|
||||
@touchend="touchEnd"
|
||||
:style="{ width: cropperOpt.width, height: cropperOpt.height, backgroundColor: 'rgba(0, 0, 0, 0.8)' }"
|
||||
canvas-id="cropper"
|
||||
id="cropper"
|
||||
></canvas>
|
||||
<canvas
|
||||
class="cropper"
|
||||
:disable-scroll="true"
|
||||
:style="{
|
||||
position: 'fixed',
|
||||
top: `-${cropperOpt.width * cropperOpt.pixelRatio}px`,
|
||||
left: `-${cropperOpt.height * cropperOpt.pixelRatio}px`,
|
||||
width: `${cropperOpt.width * cropperOpt.pixelRatio}px`,
|
||||
height: `${cropperOpt.height * cropperOpt.pixelRatio}`
|
||||
}"
|
||||
canvas-id="targetId"
|
||||
id="targetId"
|
||||
></canvas>
|
||||
</view>
|
||||
<view class="cropper-buttons safe-area-padding" :style="{ height: bottomNavHeight + 'px' }">
|
||||
<!-- #ifdef H5 -->
|
||||
<view class="upload" @tap="uploadTap">选择图片</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 -->
|
||||
<view class="upload" @tap="uploadTap">重新选择</view>
|
||||
<!-- #endif -->
|
||||
<view class="getCropperImage" @tap="getCropperImage(false)">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WeCropper from './weCropper.js';
|
||||
export default {
|
||||
props: {
|
||||
// 裁剪矩形框的样式,其中可包含的属性为lineWidth-边框宽度(单位rpx),color: 边框颜色,
|
||||
// mask-遮罩颜色,一般设置为一个rgba的透明度,如"rgba(0, 0, 0, 0.35)"
|
||||
boundStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
lineWidth: 4,
|
||||
borderColor: 'rgb(245, 245, 245)',
|
||||
mask: 'rgba(0, 0, 0, 0.35)'
|
||||
};
|
||||
}
|
||||
}
|
||||
// // 裁剪框宽度,单位rpx
|
||||
// rectWidth: {
|
||||
// type: [String, Number],
|
||||
// default: 400
|
||||
// },
|
||||
// // 裁剪框高度,单位rpx
|
||||
// rectHeight: {
|
||||
// type: [String, Number],
|
||||
// default: 400
|
||||
// },
|
||||
// // 输出图片宽度,单位rpx
|
||||
// destWidth: {
|
||||
// type: [String, Number],
|
||||
// default: 400
|
||||
// },
|
||||
// // 输出图片高度,单位rpx
|
||||
// destHeight: {
|
||||
// type: [String, Number],
|
||||
// default: 400
|
||||
// },
|
||||
// // 输出的图片类型,如果发现裁剪的图片很大,可能是因为设置为了"png",改成"jpg"即可
|
||||
// fileType: {
|
||||
// type: String,
|
||||
// default: 'jpg',
|
||||
// },
|
||||
// // 生成的图片质量
|
||||
// // H5上无效,目前不考虑使用此参数
|
||||
// quality: {
|
||||
// type: [Number, String],
|
||||
// default: 1
|
||||
// }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 底部导航的高度
|
||||
bottomNavHeight: 50,
|
||||
originWidth: 200,
|
||||
width: 0,
|
||||
height: 0,
|
||||
cropperOpt: {
|
||||
id: 'cropper',
|
||||
targetId: 'targetCropper',
|
||||
pixelRatio: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
scale: 2.5,
|
||||
zoom: 8,
|
||||
cut: {
|
||||
x: (this.width - this.originWidth) / 2,
|
||||
y: (this.height - this.originWidth) / 2,
|
||||
width: this.originWidth,
|
||||
height: this.originWidth
|
||||
},
|
||||
boundStyle: {
|
||||
lineWidth: uni.upx2px(this.boundStyle.lineWidth),
|
||||
mask: this.boundStyle.mask,
|
||||
color: this.boundStyle.borderColor
|
||||
}
|
||||
},
|
||||
// 裁剪框和输出图片的尺寸,高度默认等于宽度
|
||||
// 输出图片宽度,单位px
|
||||
destWidth: 200,
|
||||
// 裁剪框宽度,单位px
|
||||
rectWidth: 200,
|
||||
// 输出的图片类型,如果'png'类型发现裁剪的图片太大,改成"jpg"即可
|
||||
fileType: 'jpg',
|
||||
src: '', // 选择的图片路径,用于在点击确定时,判断是否选择了图片
|
||||
};
|
||||
},
|
||||
onLoad(option) {
|
||||
let rectInfo = uni.getSystemInfoSync();
|
||||
this.width = rectInfo.windowWidth;
|
||||
this.height = rectInfo.windowHeight - this.bottomNavHeight;
|
||||
this.cropperOpt.width = this.width;
|
||||
this.cropperOpt.height = this.height;
|
||||
this.cropperOpt.pixelRatio = rectInfo.pixelRatio;
|
||||
|
||||
if (option.destWidth) this.destWidth = option.destWidth;
|
||||
if (option.rectWidth) {
|
||||
let rectWidth = Number(option.rectWidth);
|
||||
this.cropperOpt.cut = {
|
||||
x: (this.width - rectWidth) / 2,
|
||||
y: (this.height - rectWidth) / 2,
|
||||
width: rectWidth,
|
||||
height: rectWidth
|
||||
};
|
||||
}
|
||||
this.rectWidth = option.rectWidth;
|
||||
if (option.fileType) this.fileType = option.fileType;
|
||||
// 初始化
|
||||
this.cropper = new WeCropper(this.cropperOpt)
|
||||
.on('ready', ctx => {
|
||||
// wecropper is ready for work!
|
||||
})
|
||||
.on('beforeImageLoad', ctx => {
|
||||
// before picture loaded, i can do something
|
||||
})
|
||||
.on('imageLoad', ctx => {
|
||||
// picture loaded
|
||||
})
|
||||
.on('beforeDraw', (ctx, instance) => {
|
||||
// before canvas draw,i can do something
|
||||
});
|
||||
// 设置导航栏样式,以免用户在page.json中没有设置为黑色背景
|
||||
uni.setNavigationBarColor({
|
||||
frontColor: '#ffffff',
|
||||
backgroundColor: '#000000'
|
||||
});
|
||||
uni.chooseImage({
|
||||
count: 1, // 默认9
|
||||
sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有
|
||||
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
|
||||
success: res => {
|
||||
this.src = res.tempFilePaths[0];
|
||||
// 获取裁剪图片资源后,给data添加src属性及其值
|
||||
this.cropper.pushOrign(this.src);
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
touchStart(e) {
|
||||
this.cropper.touchStart(e);
|
||||
},
|
||||
touchMove(e) {
|
||||
this.cropper.touchMove(e);
|
||||
},
|
||||
touchEnd(e) {
|
||||
this.cropper.touchEnd(e);
|
||||
},
|
||||
getCropperImage(isPre = false) {
|
||||
if(!this.src) return this.$u.toast('请先选择图片再裁剪');
|
||||
|
||||
let cropper_opt = {
|
||||
destHeight: Number(this.destWidth), // uni.canvasToTempFilePath要求这些参数为数值
|
||||
destWidth: Number(this.destWidth),
|
||||
fileType: this.fileType
|
||||
};
|
||||
this.cropper.getCropperImage(cropper_opt, (path, err) => {
|
||||
if (err) {
|
||||
uni.showModal({
|
||||
title: '温馨提示',
|
||||
content: err.message
|
||||
});
|
||||
} else {
|
||||
if (isPre) {
|
||||
uni.previewImage({
|
||||
current: '', // 当前显示图片的 http 链接
|
||||
urls: [path] // 需要预览的图片 http 链接列表
|
||||
});
|
||||
} else {
|
||||
uni.$emit('uAvatarCropper', path);
|
||||
this.$u.route({
|
||||
type: 'back'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
uploadTap() {
|
||||
const self = this;
|
||||
uni.chooseImage({
|
||||
count: 1, // 默认9
|
||||
sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
|
||||
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
|
||||
success: (res) => {
|
||||
self.src = res.tempFilePaths[0];
|
||||
// 获取裁剪图片资源后,给data添加src属性及其值
|
||||
|
||||
self.cropper.pushOrign(this.src);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.content {
|
||||
background: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.cropper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.cropper-buttons {
|
||||
background-color: #000000;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.cropper-wrapper {
|
||||
position: relative;
|
||||
@include vue-flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.cropper-buttons {
|
||||
width: 100vw;
|
||||
@include vue-flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.cropper-buttons .upload,
|
||||
.cropper-buttons .getCropperImage {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cropper-buttons .upload {
|
||||
text-align: left;
|
||||
padding-left: 50rpx;
|
||||
}
|
||||
|
||||
.cropper-buttons .getCropperImage {
|
||||
text-align: right;
|
||||
padding-right: 50rpx;
|
||||
}
|
||||
</style>
|
||||