This commit is contained in:
Your Name
2026-03-01 14:17:59 +08:00
parent 65aa12f146
commit a07e844c47
6071 changed files with 777651 additions and 0 deletions
@@ -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>
+322
View File
@@ -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:订单idfrom:订单来源
*/
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>
+126
View File
@@ -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>
+84
View File
@@ -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>
+55
View File
@@ -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
}
}
+437
View File
@@ -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>
+75
View File
@@ -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>