ihzero 2024-04-21 02:36:57 +08:00
parent 1a0c2a75fb
commit f91386d39d
76 changed files with 25648 additions and 4950 deletions

0
.env 100644
View File

5
.env.development 100644
View File

@ -0,0 +1,5 @@
VITE_COMMON_API_PREFIX = /api
VITE_COMMON_API_URL = http://store-manage.hmily.club

3
.env.production 100644
View File

@ -0,0 +1,3 @@
VITE_COMMON_API_PREFIX = /api
VITE_COMMON_API_URL = http://store-manage.hmily.club

3
.env.test 100644
View File

@ -0,0 +1,3 @@
VITE_COMMON_API_PREFIX = /api
VITE_COMMON_API_URL = http://store-manage.hmily.club

View File

@ -56,6 +56,7 @@
"@dcloudio/uni-mp-xhs": "3.0.0-3090920231225001",
"@dcloudio/uni-quickapp-webview": "3.0.0-3090920231225001",
"@qiun/ucharts": "^2.5.0-20230101",
"luch-request": "^3.1.1",
"pinia": "2.0.33",
"pinia-plugin-persistedstate": "^3.2.1",
"vue": "^3.2.45",

View File

@ -0,0 +1,52 @@
<template>
<view
class="flex items-center h-92rpx pl-base pr-15rpx rounded-19rpx bg-white"
:class="[{ 'card-shadow': shadow }]"
@click="$emit('onClick')"
>
<view
class="text-[#333333] font-400 text-27rpx"
:style="[
{ width: `${getPx(titleWidth)}px`, fontSize: `${getPx(textSize)}px` },
]"
>{{ title }}</view
>
<view class="flex-1">
<slot></slot>
</view>
<view class="h-full flex-center">
<slot name="right-icon">
<image
v-if="isLink"
class="w-26rpx h-26rpx"
src="@/static/images/me_icon_more_def.svg"
></image>
</slot>
</view>
</view>
</template>
<script setup>
import { getPx } from '@climblee/uv-ui/libs/function/index.js'
const props = defineProps({
title: {
type: String,
},
isLink: {
type: Boolean,
default: true,
},
shadow: {
type: Boolean,
default: true,
},
titleWidth: {
type: String,
default: '1130rpx',
},
textSize: {
type: String,
default: '27rpx',
},
})
</script>
<style scoped lang="scss"></style>

View File

@ -1,20 +1,77 @@
<template>
<uv-navbar
:title="title"
:leftIcon="leftIcon"
:fixed="fixed"
:bgColor="bgColor"
:titleStyle="titleStyle"
fixed
placeholder
:isBack="isBack"
:leftIcon="leftIcon"
:title="title"
:titleStyle="{...titleStyle}"
:placeholder="placeholder"
@leftClick="onLeftClick"
z-index="1000"
>
<template #right>
<slot name="right"></slot>
</template>
<template #left>
<slot name="left">
<image
v-if="isBack && !leftIcon"
class="w-54rpx h-54rpx"
src="/static/images/icon_back_def.svg"
></image>
</slot>
</template>
<template #center>
<slot name="center"></slot>
</template>
<template #right>
<view :style="[{ paddingRight: addUnit(paddingRight) }]">
<slot name="right"></slot>
</view>
</template>
</uv-navbar>
</template>
<script setup>
let menuButtonInfo = {}
// #ifdef MP-WEIXIN || MP-BAIDU || MP-TOUTIAO || MP-QQ
menuButtonInfo = uni.getMenuButtonBoundingClientRect()
// #endif
import { computed } from 'vue'
import { addUnit, sys } from '@climblee/uv-ui/libs/function/index.js'
const props = defineProps({
fixed: {
type: Boolean,
default: true,
},
isBack: {
type: Boolean,
default: true,
},
autoBack: {
type: Boolean,
default: true,
},
placeholder: {
type: Boolean,
default: true,
},
bgColor: {
type: String,
default: '#ee2c37',
},
imgMode: {
type: String,
default: 'aspectFill',
},
safeAreaInsetTop: {
type: Boolean,
default: true,
},
height: {
type: [String, Number],
default: '44px',
},
title: {
type: String,
default: '',
@ -23,17 +80,40 @@ const props = defineProps({
type: String,
default: '',
},
bgColor: {
type: String,
default: '#ee2c37',
},
titleStyle: {
type: Object,
default: () => ({ color: '#fff' }),
default: () => {
return {
color: '#fff',
}
},
},
isBack: {
type: Boolean,
default: true,
}
})
const emit = defineEmits(['leftClick'])
const paddingRight = computed(() => {
let rightButtonWidth = 0
rightButtonWidth =
sys().windowWidth - (menuButtonInfo?.left ?? sys().windowWidth)
return `${rightButtonWidth}px`
})
const onLeftClick = () => {
emit('leftClick')
if (props.autoBack) {
// #ifdef H5
let canNavBack = getCurrentPages()
if (canNavBack && canNavBack.length > 1) {
uni.navigateBack({
delta: 1,
})
} else {
history.back()
}
// #endif
// #ifndef H5
uni.navigateBack()
// #endif
}
}
</script>

View File

@ -0,0 +1,66 @@
<template>
<view>
<view class="flex justify-end text-white" @click="onClick">
<view>{{ valueFormat }}</view>
<uv-icon color="white" size="16rpx" name="arrow-down-fill"></uv-icon>
</view>
<uv-datetime-picker
ref="datetimePicker"
v-model="value"
:mode="mode"
@confirm="confirm"
:maxDate="Number(new Date())"
:minDate="Number(new Date(2020, 0, 1))"
>
</uv-datetime-picker>
</view>
</template>
<script setup>
import { ref, watch, watchEffect, computed } from 'vue'
import { timeFormat } from '@climblee/uv-ui/libs/function/index'
const props = defineProps({
modelValue: {
type: [String, Number],
default: null,
},
mode: {
type: String,
default: 'year-month',
},
formatStr: {
type: String,
default: 'yyyy年mm月',
},
})
const datetimePicker = ref(null)
const value = ref(props.modelValue || Number(new Date()))
const valueFormat = computed(() => {
return timeFormat(value.value, props.formatStr)
})
const emit = defineEmits(['update:modelValue','confirm'])
const confirm = (e) => {
value.value = e.value
emit('confirm', e)
}
const onClick = () => {
datetimePicker.value.open()
}
watchEffect(() => {
value.value = props.modelValue || Number(new Date())
})
watch(
() => value.value,
(val) => {
emit('update:modelValue', val)
}
)
</script>

View File

@ -1,15 +1,24 @@
<template>
<view class="flex items-center justify-between">
<view class="title">{{ title }}</view>
<slot name="right"></slot>
<view class="title" :style="{ fontSize: `${getPx(textSize)}px` }">{{
title
}}</view>
<view>
<slot name="right"></slot>
</view>
</view>
</template>
<script setup>
import { getPx } from '@climblee/uv-ui/libs/function/index.js'
const props = defineProps({
title: {
type: String,
default: '',
},
textSize: {
type: String,
default: '27rpx',
},
})
</script>
<style lang="scss">

View File

@ -0,0 +1,21 @@
export const useGlobSetting = () => {
const ENV = import.meta.env
const { VITE_COMMON_API_PREFIX, VITE_COMMON_API_URL } = ENV
let apiUrl = ''
// #ifdef MP-WEIXIN || APP-PLUS
apiUrl += VITE_COMMON_API_URL
// #endif
// #ifdef H5
apiUrl += VITE_COMMON_API_PREFIX
// #endif
return {
urlPrefix: VITE_COMMON_API_PREFIX,
api: VITE_COMMON_API_URL,
apiUrl: apiUrl
}
}

View File

@ -2,16 +2,12 @@ import {
createSSRApp
} from "vue";
import App from "./App.vue";
import uvUI from '@climblee/uv-ui'
import 'virtual:uno.css'
import { setupStore } from '@/store';
export function createApp() {
const app = createSSRApp(App);
app.use(uvUI);
setupStore(app);
return {
app,
};

View File

@ -1,5 +1,11 @@
{
"pages": [
{
"path": "pages/login/index",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/home/index",
"style": {
@ -40,6 +46,11 @@
"style": {
"navigationBarTitleText": "业绩数据"
}
},{
"path": "upload/index",
"style": {
"navigationBarTitleText": "上传数据"
}
}
]
},
@ -57,6 +68,89 @@
"style": {
"navigationBarTitleText": "修改信息"
}
},
{
"path": "detail",
"style": {
"navigationBarTitleText": "员工详情"
}
}
]
},
{
"root": "pages/setting",
"pages": [
{
"path": "index",
"style": {
"navigationBarTitleText": "设置"
}
},
{
"path": "password",
"style": {
"navigationBarTitleText": "修改密码"
}
},
{
"path": "complain",
"style": {
"navigationBarTitleText": "举报投诉"
}
},
{
"path": "suggestion",
"style": {
"navigationBarTitleText": "意见箱"
}
}
]
},
{
"root": "pages/task",
"pages": [
{
"path": "index",
"style": {
"navigationBarTitleText": "任务列表"
}
},
{
"path": "submit",
"style": {
"navigationBarTitleText": "任务提交"
}
},
{
"path": "detail",
"style": {
"navigationBarTitleText": "任务详情"
}
}
]
},{
"root": "pages/expense-account",
"pages": [
{
"path": "index",
"style": {
"navigationBarTitleText": "报销管理"
}
},{
"path": "submit",
"style": {
"navigationBarTitleText": "报销提交"
}
}
]
},{
"root": "pages/work",
"pages": [
{
"path": "list",
"style": {
"navigationBarTitleText": "升职申请"
}
}
]
}

View File

@ -1,10 +1,10 @@
<template>
<view class="card">
<view class="flex justify-between">
<view class="font-600">2024年2月</view>
<view class="font-600">{{ data.month }}</view>
<view class="space-x-20rpx">
<text>提成</text>
<text class="text-primary">1231</text>
<text class="text-primary">{{ data.commission }}</text>
</view>
</view>
<view class="flex mt-20rpx">
@ -12,17 +12,26 @@
<view class="flex-1 grid grid-cols-3 text-gray-500">
<view class="text-right">
<text>日常</text>
<text>154</text>
<text>{{ data.daily_expenses }}</text>
</view>
<view class="text-right">
<text>员工</text>
<text>154</text>
<text>{{ data.employee_expenses }}</text>
</view>
<view class="text-right">
<text>其他</text>
<text>154</text>
<text>{{ data.other_expenses }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
const props = defineProps({
data: {
type: Object,
default: () => {},
},
})
</script>
<style lang="scss" scoped></style>

View File

@ -1,15 +1,50 @@
<template>
<view>
<CuNavbar title="提成数据" isBack></CuNavbar>
<view class="space-y-20rpx mt-30rpx">
<Item></Item>
<Item></Item>
<Item></Item>
</view>
<mescroll-body @init="mescrollInit" @down="downCallback" @up="upCallback">
<view class="space-y-20rpx px-base mt-20rpx">
<view v-for="(item, i) in list" :key="i">
<Item :data="item"></Item>
</view>
</view>
</mescroll-body>
</view>
</template>
<script setup>
import CuNavbar from '@/components/cu-navbar/index'
import Item from './components/item.vue'
import { onLoad } from '@dcloudio/uni-app'
import { http } from '@/utils/request'
import { ref } from 'vue'
import { onPageScroll, onReachBottom } from '@dcloudio/uni-app'
import useMescroll from '@/uni_modules/mescroll-uni/hooks/useMescroll.js'
const { mescrollInit, downCallback, getMescroll } = useMescroll(
onPageScroll,
onReachBottom
)
const list = ref([])
const upCallback = async (mescroll) => {
const { size, num } = mescroll
try {
const resData = await http.get('/account/store-master-commissions', {
params: {
per_page: size,
page: num,
},
})
const curPageData = resData.data || []
if (num === 1) list.value = []
list.value = list.value.concat(curPageData)
mescroll.endSuccess(curPageData.length)
} catch (error) {
mescroll.endErr() // ,
}
}
</script>

View File

@ -2,39 +2,48 @@
<view>
<CuNavbar title="业绩数据" isBack></CuNavbar>
<view class="bg-primary bg-opacity-80 p-base">
<view class="flex justify-end text-white">
<view>2024年3月</view>
<uv-icon color="white" size="16rpx" name="arrow-down-fill"></uv-icon>
</view>
<DateTime v-model="currentDate" @confirm="dateConfirm" />
<view class="flex items-center py-base text-white">
<view class="flex-1 text-center">
<view class="text-32rpx">当前业绩</view>
<view class="mt-10rpx">4923</view>
<view class="mt-10rpx">{{ countData.actual_performance }}</view>
</view>
<view>/</view>
<view class="flex-1 text-center">
<view class="text-32rpx">目标业绩</view>
<view class="mt-10rpx">4923</view>
<view class="mt-10rpx">{{ countData.expected_performance }}</view>
</view>
</view>
</view>
<view class="card">
<uv-tabs
:activeStyle="{ color: '#ee2c37' }"
:lineColor="'#ee2c37'"
:scrollable="false"
:list="[{ name: '业绩目标' }, { name: '完成业绩' }]"
></uv-tabs>
</view>
<view class="mt-20rpx card">
<TitleComp title="2024"></TitleComp>
<view v-for="item in 3">
<view class="flex items-center h-84rpx">
<view class="w-110rpx text-primary">4</view>
<view class="flex-1">0/150000</view>
<view>未开始</view>
<uv-sticky bgColor="#fff" zIndex="20">
<view class="">
<uv-tabs
:activeStyle="{ color: '#ee2c37' }"
:lineColor="'#ee2c37'"
:scrollable="false"
:list="tabList"
:current="currentTab"
@change="tabChange"
></uv-tabs>
</view>
</uv-sticky>
<view class="">
<view v-for="ob in 10" :key="ob">
<uv-sticky bgColor="#f5f5f5" zIndex="10" offsetTop="44" >
<view class="h-80rpx flex items-center px-base">
<TitleComp title="2024"></TitleComp>
</view>
</uv-sticky>
<view class="card">
<view v-for="(item, i) in 3" :key="i">
<view class="flex items-center h-84rpx">
<view class="w-110rpx text-primary">4</view>
<view class="flex-1">0/150000</view>
<view>未开始</view>
</view>
<uv-line></uv-line>
</view>
</view>
<uv-line></uv-line>
</view>
</view>
</view>
@ -42,4 +51,57 @@
<script setup>
import CuNavbar from '@/components/cu-navbar/index'
import TitleComp from '@/components/title-comp/index'
import DateTime from '@/components/date-time/index'
import { http } from '@/utils/request'
import { onLoad } from '@dcloudio/uni-app'
import { timeFormat } from '@climblee/uv-ui/libs/function/index'
import { ref, reactive, computed } from 'vue'
const list = ref([])
const tabList = [
{ name: '业绩目标', value: 'future' },
{ name: '完成业绩', value: 'history' },
]
const currentTab = ref(0)
const historyDate = uni.getStorageSync('historyDate')
const currentDate = ref(historyDate || Number(new Date()))
const countData = reactive({
actual_performance: 0,
expected_performance: 0,
})
const currentTabObj = computed(() => tabList[currentTab.value])
onLoad(() => {
getCount()
getList()
})
const dateConfirm = (e) => {
uni.setStorageSync('historyDate', e.value)
currentDate.value = e.value
getCount()
}
const getCount = async () => {
const resData = await http.get('/account/store-performance', {
params: {
month: timeFormat(currentDate.value, 'yyyy-mm'),
},
})
Object.assign(countData, resData)
}
const tabChange = (e) => {
currentTab.value = e.index
getList()
}
const getList = async () => {
const resData = await http.get('/account/store-performance-tasks', {
params: {
filter: currentTabObj.value.value,
},
})
list.value = resData
}
</script>

View File

@ -0,0 +1,108 @@
<template>
<view class="px-base">
<CuNavbar title="数据上报"></CuNavbar>
<view class="mt-30rpx">
<uv-form
labelWidth="140rpx"
labelPosition="left"
:borderBottom="false"
:model="form"
:rules="rules"
errorType="toast"
ref="formRef"
>
<view class="space-y-15rpx">
<view class="card-shadow bg-white rounded-19rpx px-base">
<uv-form-item label="日期">
<view class="w-full">
{{ form.date }}
</view>
</uv-form-item>
</view>
<view> </view>
<view class="card-shadow bg-white rounded-19rpx px-base">
<uv-form-item>
<TitleComp title="电彩" textSize="32rpx"></TitleComp>
</uv-form-item>
<uv-form-item label="销售" borderBottom>
<uv-input
border="none"
placeholder="请输入电彩销售金额"
></uv-input>
</uv-form-item>
<uv-form-item label="兑奖">
<uv-input
border="none"
placeholder="请输入电彩兑奖金额"
></uv-input>
</uv-form-item>
</view>
<view class="card-shadow bg-white rounded-19rpx px-base">
<uv-form-item>
<TitleComp title="汇总情况" textSize="32rpx"></TitleComp>
</uv-form-item>
<uv-form-item label="销售合计" borderBottom>
<uv-input
border="none"
placeholder="请输入总帐销售金额"
></uv-input>
</uv-form-item>
<uv-form-item label="兑奖合计" borderBottom>
<uv-input
border="none"
placeholder="请输入总帐兑奖金额"
></uv-input>
</uv-form-item>
<uv-form-item label="新增客户" borderBottom>
<uv-input
border="none"
placeholder="请输入微信新增人数"
></uv-input>
</uv-form-item>
<uv-form-item label="交账金额" borderBottom>
<uv-input border="none" placeholder="请输入交账金额"></uv-input>
</uv-form-item>
<view class="text-primary text-xs mt-10rpx pb-base"
>*请确保填写的交账金额正确无误</view
>
</view>
<view class="card-shadow bg-white rounded-19rpx px-base pb-base">
<uv-form-item>
<view class="w-full">
<TitleComp title="时段报表照片" textSize="32rpx">
<template #right>
<view class="text-hex-999999"> 0/9 </view>
</template>
</TitleComp>
</view>
</uv-form-item>
<uv-form-item>
<uv-upload></uv-upload>
</uv-form-item>
<view class="text-primary text-xs">
*竞彩时段报表照片玩法时段报表照片每日账本上传需亲笔签字销量本上传
</view>
</view>
</view>
</uv-form>
</view>
<uv-datetime-picker ref="datetimePicker" v-model="form.date" mode="date">
</uv-datetime-picker>
</view>
</template>
<script setup>
import CuNavbar from '@/components/cu-navbar/index'
import TitleComp from '@/components/title-comp/index'
import { ref, reactive } from 'vue'
const formRef = ref(null)
const datetimePicker = ref(null)
const form = reactive({
date: '2022-01-01',
})
const rules = reactive({})
</script>

View File

@ -0,0 +1,60 @@
<template>
<view>
<CuNavbar title="报销管理">
<template #right>
<view @click="goPath('/pages/expense-account/submit')" class="text-24rpx text-white">申请</view>
</template>
</CuNavbar>
<uv-sticky bgColor="#fff">
<uv-tabs
:activeStyle="{ color: '#ee2c37' }"
:scrollable="false"
lineColor="#ee2c37"
:list="tabList"
></uv-tabs>
</uv-sticky>
<view class="px-base space-y-20rpx mt-30rpx">
<view
v-for="item in 4"
class="card-shadow bg-white rounded-19rpx p-base space-y-10rpx"
>
<view class="flex items-center justify-between">
<view class="text-30rpx"> 报销申请 </view>
<view class="text-24rpx text-hex-999999">待完成</view>
</view>
<view class="text-24rpx text-hex-999999 flex">
<view class="w-140rpx">报销金额</view>
<view class="text-primary">12313</view>
</view>
<view class="text-24rpx text-hex-999999 flex">
<view class="w-140rpx">报销时间</view>
<view class="text-hex-333">2022-01-01</view>
</view>
<view class="text-24rpx text-hex-999999">
<view class="">
<text class="w-140rpx inline-block">报销原因:</text>
<text class="text-hex-333 leading-27rpx">报销原因报销原因报销原因报销原因报销原因报销原因报销原因报销原因报销原因报销原因</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import CuNavbar from '@/components/cu-navbar/index'
import { ref } from 'vue'
const tabList = ref([
{
name: '我的报销',
},
{
name: '报销审核',
},
])
const goPath = (url) => {
uni.navigateTo({
url
})
}
</script>

View File

@ -0,0 +1,120 @@
<template>
<view class="px-base">
<CuNavbar title="报销申请"> </CuNavbar>
<view class="space-y-base mt-base">
<view class="card-shadow bg-white rounded-19rpx px-base">
<uv-form
labelWidth="160rpx"
:model="form"
:rules="rules"
errorType="toast"
ref="formRef"
labelPosition="left"
>
<uv-form-item
label="报销分类"
required
:borderBottom="true"
prop="content"
>
<uv-input
border="none"
placeholder="请输入报销分类"
v-model="form.content"
></uv-input>
</uv-form-item>
<uv-form-item
:borderBottom="true"
label="报销金额"
required
prop="content"
>
<uv-input
border="none"
placeholder="请输入报销金额"
v-model="form.content"
></uv-input>
</uv-form-item>
<uv-form-item
label="报销原因"
required
prop="content"
:borderBottom="true"
labelPosition="top"
>
<uv-textarea
border="none"
v-model="form.content"
placeholder="请输入报销原因"
></uv-textarea>
</uv-form-item>
<uv-form-item
label="报销凭证"
labelPosition="top"
prop="photos"
required
>
<view class="w-full mt-15rpx">
<uv-upload></uv-upload>
</view>
</uv-form-item>
</uv-form>
</view>
</view>
<view class="mt-100rpx">
<uv-button type="primary" @click="submit"></uv-button>
</view>
<uv-modal
ref="modalRef"
title="提示"
content="确定提交投诉?"
@confirm="changePassword"
:showCancelButton="true"
></uv-modal>
</view>
</template>
<script setup>
import CuNavbar from '@/components/cu-navbar/index'
import Cell from '@/components/cell/index'
import { ref, reactive } from 'vue'
import { http } from '@/utils/request'
const modalRef = ref(null)
const formRef = ref(null)
const form = reactive({
content: '',
photos: [],
})
const rules = reactive({
content: [
{ required: true, message: '请输入投诉内容' },
{ min: 20, message: '至少20字' },
{ max: 200, message: '最多200字' },
],
photos: {
type: 'array',
},
})
const submit = () => {
formRef.value.validate().then((res) => {
modalRef.value.open()
})
}
const changePassword = () => {
// http
// .post('/auth/profile', {
// password: form.password,
// password_confirmation: form.password2,
// })
// .then((ress) => {
// uni.showToast({
// title: '',
// duration: 2000,
// icon: 'none',
// })
// formRef.value.resetFields()
// })
}
</script>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,193 @@
<template>
<view>
<uv-sticky bgColor="#fff">
<view class="flex-center h-88rpx">
<view class="flex-center flex-1" @click="selectMenu({ name: 'shore' })">
<view>全部区域</view>
<uv-icon
class="ml-10rpx"
size="20rpx"
name="arrow-down-fill"
></uv-icon>
</view>
<view class="flex-center flex-1" @click="selectMenu({ name: 'store' })">
<view>全部区域</view>
<uv-icon
class="ml-10rpx"
size="20rpx"
name="arrow-down-fill"
></uv-icon>
</view>
</view>
</uv-sticky>
<uv-picker
ref="shoreRef"
keyName="name"
@change="shoreChange"
:columns="shoreList"
@confirm="shoreConfirm"
></uv-picker>
<uv-picker
ref="storeRef"
keyName="address"
@change="shoreChange"
:columns="storeList"
@confirm="shoreConfirm"
></uv-picker>
</view>
</template>
<script>
import { http } from '@/utils/request'
import data from './da.json'
export default {
onPageScroll() {
//
// this.$refs.dropDown.init()
},
computed: {
dropItem(name) {
return (name) => {
return {
label: name,
value: name,
}
}
},
//
currentDropItem() {
return this[this.activeName]
},
},
data() {
return {
shoreList: [],
storeList: [],
// value
defaultValue: [0, 'all'],
//
result: [],
activeName: 'shore',
shore: {
label: '全部区域',
activeIndex: [0, 0],
color: '#333',
activeColor: '#2878ff',
},
store: {
label: '全部门店',
activeIndex: 0,
color: '#333',
activeColor: '#2878ff',
},
cityData: data,
}
},
created() {
this.init()
},
methods: {
async init() {
const province = this.cityData.province
this.shoreList = [
province,
this.getCityByProvince(province[this.shore.activeIndex[1]].code),
]
},
change(e) {
console.log('弹窗打开状态:', e)
},
/**
* 点击每个筛选项回调
* @param {Object} e { name, active, type } = e
*/
selectMenu(e) {
const { name } = e
this.activeName = name
if (name === 'shore') {
const active = this.shore.activeIndex
const list = this.getCityByProvince(this.shoreList[0][active[0]].code)
this.shoreList[1] = list
this.$refs.shoreRef.setColumnValues(1, list)
this.$refs.shoreRef.setIndexs(active, true)
this.$refs.shoreRef.open()
}
if (name === 'store') {
this.$refs.storeRef.open()
}
},
/**
* 点击菜单回调处理
* @param {Object} item 选中项 { label,value } = e
*/
clickItem(e) {},
shoreChange(e) {
const { columnIndex, index } = e
if (columnIndex == 0) {
const item = this.shoreList[columnIndex][index]
this.shoreList[1] = this.getCityByProvince(item.code)
this.$refs.shoreRef.setColumnValues(
1,
this.getCityByProvince(item.code)
)
}
},
shoreConfirm(e) {
this.shore.activeIndex = e.indexs
const item = {
name: 'shore',
}
const cityIndex = this.shore.activeIndex[1]
const index = cityIndex == 0 ? 0 : 1
const index2 = e.indexs[index]
},
getCityByProvince(province) {
return [
{
name: '全部',
code: 'all',
},
].concat(this.cityData.city[province])
},
getStoreByCity(city) {
const res = http.get('/auth/stores', {
params: {
city: city,
},
})
this.storeList = [
{
id: 1,
title: '1',
master_id: 1,
category_id: 'store_category_1_1',
business_id: 'store_business_1',
level_id: 'store_level_2',
region: {
city: '天津市市辖区',
code: 120100,
street: null,
cityCode: 120100,
district: null,
province: '天津市',
districtCode: 0,
provinceCode: 120000,
},
address: '回龙观(地铁站)',
lon: '116.34266369754',
lat: '40.076418413591',
profit_ratio: 0,
profit_money: '0.00',
business_status: 1,
created_at: '2024-04-03 17:17:02',
updated_at: '2024-04-03 17:17:02',
business_status_text: '开业',
business_status_color: 'success',
},
]
},
},
}
</script>

View File

@ -1,35 +1,7 @@
<template>
<view>
<view>
<uv-drop-down
ref="dropDown"
sign="dropDown_1"
text-active-color="#3c9cff"
:extra-icon="{ name: 'arrow-down-fill', color: '#666', size: '26rpx' }"
:extra-active-icon="{
name: 'arrow-up-fill',
color: '#3c9cff',
size: '26rpx',
}"
:defaultValue="['all', 'all']"
:custom-style="{ padding: '0 30rpx' }"
@click="selectMenu"
>
<uv-drop-down-item
name="area"
type="2"
:label="dropDownData.area.label"
:value="dropDownData.area.value"
>
</uv-drop-down-item>
<uv-drop-down-item
name="store"
type="2"
:label="dropDownData.store.label"
:value="dropDownData.store.value"
>
</uv-drop-down-item>
</uv-drop-down>
<StoreDropDown></StoreDropDown>
</view>
<view class="bg-primary p-base text-center text-white relative">
@ -56,21 +28,21 @@
<view class="h-80rpx leading-80rpx px-base">近30天趋势数据</view>
</view>
<view>
<uv-tabs :activeStyle="{color:'#ee2c37'}" lineColor="#ee2c37" :list="list" @click="onTabClick" :scrollable="false"></uv-tabs>
<uv-tabs
:activeStyle="{ color: '#ee2c37' }"
lineColor="#ee2c37"
:list="list"
@click="onTabClick"
:scrollable="false"
></uv-tabs>
<ChartComp></ChartComp>
</view>
<uv-picker
ref="dropDownPickerRef"
:columns="currentDropItem"
keyName="label"
@confirm="clickItem"
></uv-picker>
</view>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import ChartComp from './components/chart.vue'
import StoreDropDown from '@/pages/home/components/store-drop-down/index.vue'
const list = ref([
{
@ -81,99 +53,6 @@ const list = ref([
},
])
const dropDownData = reactive({
area: {
label: '全部区域',
value: 'all',
activeIndex: 0,
color: '#333',
activeColor: '#2878ff',
child: [
{
label: '全部区域',
value: 'all',
},
{
label: '重庆',
value: 'cq',
},
{
label: '北京',
value: 'bj',
},
],
},
store: {
label: '全部门店',
value: 'all',
activeIndex: 0,
color: '#333',
activeColor: '#2878ff',
child: [
{
label: '全部门店',
value: 'all',
},
{
label: '门店1',
value: 'new',
},
{
label: '门店2',
value: 'money',
},
],
},
})
const result = ref([])
const dropDownPickerRef = ref(null)
const activeName = ref('area')
const currentDropItem = computed(() => {
return [dropDownData[activeName.value].child]
})
const selectMenu = (e) => {
const { name, active, type } = e
activeName.value = name
const find = result.value.find((item) => item.name == activeName.value)
if (find) {
const findIndex = dropDownData[activeName.value].child.findIndex(
(item) => item.label == find.label && item.value == find.value
)
dropDownData[activeName.value].activeIndex = findIndex
} else {
dropDownData[activeName.value].activeIndex = 0
}
dropDownPickerRef.value.open()
console.log(dropDownData)
}
const clickItem = (e) => {
const { value: cc } = e
const { label, value } = cc[0]
const findIndex = result.value.findIndex(
(item) => item.name == activeName.value
)
if (findIndex > -1) {
result.value[findIndex] = {
name: activeName.value,
label: label,
value: value,
}
} else {
result.value.push({
name: activeName.value,
label: label,
value: value,
})
}
console.log(result.value)
}
const onTabClick = (e) => {
console.log(e)
}

View File

@ -0,0 +1,20 @@
<template>
<view class="w-full flex flex-col">
<view class="mt-20vh">
<view class="b-1px b-solid h-120rpx"></view>
</view>
<view class="text-35rpx text-hex-333333 font-600 mt-80rpx">体彩管理系统</view>
<view class="text-27rpx text-hex-333333">欢迎登录</view>
<view class="flex-1 flex flex-col justify-end mt-0rpx">
<LoginForm></LoginForm>
</view>
</view>
</template>
<script setup>
import LoginForm from './LoginForm.vue'
</script>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,65 @@
<template>
<view class="">
<uv-form>
<uv-form-item>
<view class="h-92rpx bg-white rounded-full w-full flex-center">
<uv-input
v-model="form.username"
shape="circle"
maxlength="11"
placeholder="请输入帐号"
type="text"
border="none1"
fontSize="27rpx"
>
</uv-input>
</view>
</uv-form-item>
<uv-form-item>
<view class="h-92rpx bg-white rounded-full w-full flex-center">
<uv-input
v-model="form.password"
shape="circle"
placeholder="请输入密码"
type="password"
border="none1"
fontSize="27rpx"
>
</uv-input>
</view>
</uv-form-item>
</uv-form>
<view class="mt-115rpx">
<uv-button block type="primary" shape="circle" @click="handleClick"
>登录</uv-button
>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { useUserStore } from '@/store/modules/user'
const userStore = useUserStore()
const form = ref({
username: 'admin',
password: 'admin',
})
const handleClick = async () => {
try {
const { username, password } = form.value
await userStore.login({
username,
password,
})
uni.switchTab({
url: '/pages/home/index',
})
} catch (e) {
console.log(e)
}
}
</script>

View File

@ -0,0 +1,53 @@
<template>
<view>
<view class="mt-115rpx">
<uv-button block type="primary_btn1" shape="circle" open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber">微信一键登录
</uv-button>
<view class="mt-31rpx">
<cu-checkbox v-model="checkbox">
<view class="flex items-center flex-wrap text-23rpx leading-33rpx text-hex-333333">
<view class="text-hex-999999 float-left">
登录即代表同意
</view>
<text>用户协议隐私政策</text>
<text class="text-hex-999999">
</text>
<text>第三方SDK类服务商说明</text>
</view>
</cu-checkbox>
</view>
</view>
</view>
</template>
<script setup>
import CuCheckbox from "@/components/CuCheckbox/cu-checkbox.vue";
import {ref} from 'vue'
import {useUserStore} from "@/store/modules/user";
const userStore = useUserStore()
const checkbox = ref(false)
const getPhoneNumber = (e) => {
const {appId, version} = uni.getAccountInfoSync().miniProgram
userStore.wchatLogin({
appId,
version,
type: 'getPhoneNumber',
code: e.code
})
uni.switchTab({
url: '/pages/tabbar/home',
})
}
</script>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,42 @@
<template>
<Cupopup mode="bottom" v-model:visible="ruleShow" title="请阅读并同意以下条款" @cancel="handleClick('cancel')"
@confirm="handleClick('confirm')">
<view @click="ruleClick">
登录即代表同意用户协议隐私政策 登录即代表同意用户协议隐私政策第三方SDK类服务商说明
</view>
</Cupopup>
</template>
<script setup>
import Cupopup from "@/components/CuPopup/index.vue";
import {ref, watch, watchEffect} from "vue";
const props = defineProps({
visible: {
type: Boolean,
default: false,
}
})
const ruleShow = ref(props.visible)
const emit = defineEmits(['update:visible', 'onClick','ruleClick'])
const handleClick = (e) => {
emit('onClick', e)
}
const ruleClick = () => {
emit('ruleClick')
}
watchEffect(() => {
ruleShow.value = props.visible
})
watch(() => ruleShow.value, (val) => {
emit('update:visible', val)
})
</script>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,53 @@
<template>
<Cupopup mode="bottom" v-model:visible="ruleShow" title="用户服务协议及隐私政策" @cancel="handleClick('cancel')" @confirm="handleClick('confirm')">
<view class="space-y-20rpx">
<view class="indent-46rpx">
小程序用户协议和隐私政策是小程序开发者为保障用户合法权益和保护用户隐私而制
</view>
<view>定的两个重要文件下面分别介绍一下小程序用户协议和隐私政策的内容和要点</view>
<view>用户协议 用户协议是小程序开发者与用户之间达成的协议 规定了用户使用小程序的
条件权利和义务等内容用户在使用小程序之前需要同意用户协议否则无法使用小程序
</view>
<view> 一般来说小程序用户协议包括以下几个方面的内容</view>
<view>1. 用户注册和账号管理规定包括用户注册账号安全和管理等规定</view>
<view>2. 使用规范和限制 包括用户在小程序上发布内容的规范禁止发布的内容和行为等规定</view>
<view>3. 用户权利和义务包括用户在使用小程序时享有的权利和应承担的义务等规定</view>
<view>4. 免责声明和责任限制包括小程序开发者对小程序服务的免责声明和责任限制等规定</view>
<view>5. 争议解决方式包括双方在发生争议时解决方式的规定</view>
<view>6. 隐私政策 隐私政策是小程序开发者为保护用户隐私而制定的政策 规定了小程序开发
者在收集使用存储保护用户个人信息时应遵守的原则和措施用户在使用小程序 需要同意隐私政策 否则无法使用小程序
一般来
</view>
</view>
</Cupopup>
</template>
<script setup>
import Cupopup from '@/components/CuPopup/index.vue'
import {ref, watch, watchEffect} from 'vue'
const props = defineProps({
visible: {
type: Boolean,
default: false,
}
})
const ruleShow = ref(props.visible)
const emit = defineEmits(['update:visible', 'onClick'])
const handleClick = (e) => {
emit('onClick', e)
}
watchEffect(() => {
ruleShow.value = props.visible
})
watch(() => ruleShow.value, (val) => {
emit('update:visible', val)
})
</script>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,11 @@
<template>
<view class="px-106rpx min-h-screen">
<Layout></Layout>
</view>
</template>
<script setup>
import Layout from './Layout.vue'
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
</script>

View File

@ -1,6 +1,12 @@
<template>
<view>
<CuNavbar title="我的"></CuNavbar>
<CuNavbar :isBack="false" title="我的">
<template #right>
<view class="h-full flex-center">
<image @click="goPath('/pages/setting/index')" class="w-32rpx h-32rpx" src="/static/images/setting.svg"></image>
</view>
</template>
</CuNavbar>
<view class="px-base space-y-15rpx mt-30rpx">
<view class="card" v-for="(item, i) in opList" :key="i">
<TitleComp :title="item.title"></TitleComp>
@ -46,17 +52,17 @@ const opList = [
{
icon: 'lock',
title: '我的任务',
url: '',
url: '/pages/task/index',
},
{
icon: 'home-fill',
title: '报销管理',
url: '',
url: '/pages/expense-account/index',
},
{
icon: 'map-fill',
title: '升职申请',
url: '',
url: '/pages/work/list',
},
{
icon: 'grid-fill',
@ -101,4 +107,11 @@ const opList = [
],
},
]
const goPath = (url) => {
uni.navigateTo({
url
})
}
</script>

View File

@ -0,0 +1,150 @@
<template>
<view class="px-base">
<CuNavbar title="举报投诉"> </CuNavbar>
<view class="space-y-base mt-base">
<view class="card-shadow bg-white rounded-19rpx px-base">
<uv-form
labelWidth="160rpx"
:borderBottom="false"
:model="form"
:rules="rules"
errorType="toast"
ref="formRef"
labelPosition="top"
>
<uv-form-item label="投诉内容" prop="content">
<uv-textarea
maxlength="200"
:customStyle="{ padding: 0 ,minHeight: '200rpx'}"
count
placeholder="可描述具体投诉内容至少20字"
:border="`none`"
v-model="form.content"
>
</uv-textarea>
</uv-form-item>
<uv-line color="#f5f5f5"></uv-line>
<uv-form-item label="" prop="photos">
<view class="w-full">
<view class="flex justify-between text-15px">
<view>证明材料选填</view>
<view class="text-hex-999999 text-12px pr-9px">{{form.photos.length}}/9</view>
</view>
<view class="mt-10rpx">
<uv-upload
:maxCount="9"
multiple
:fileList="form.photos"
@afterRead="afterRead"
@delete="deletePic"
name="photos"
></uv-upload>
</view>
</view>
</uv-form-item>
</uv-form>
</view>
</view>
<view class="mt-100rpx">
<uv-button type="primary" @click="submit"></uv-button>
</view>
<uv-modal
ref="modalRef"
title="提示"
content="确定提交投诉?"
@confirm="onSubmit"
:showCancelButton="true"
></uv-modal>
</view>
</template>
<script setup>
import CuNavbar from '@/components/cu-navbar/index'
import Cell from '@/components/cell/index'
import { ref, reactive } from 'vue'
import { http } from '@/utils/request'
const modalRef = ref(null)
const formRef = ref(null)
const fileList = ref([])
const form = reactive({
content: '',
photos: [],
})
const rules = reactive({
content: [
{ required: true, message: '可描述具体投诉内容至少20字' },
{ min: 20, message: '至少20字' },
{ max: 200, message: '最多200字' },
],
})
const submit = () => {
formRef.value.validate().then((res) => {
modalRef.value.open()
})
}
const afterRead = async (event) => {
let lists = [].concat(event.file)
let fileListLen = form[event.name].length
lists.map((item) => {
form[event.name].push({
...item,
status: 'uploading',
message: '上传中',
})
})
for (let i = 0; i < lists.length; i++) {
// console.log(lists[i]);
const result = await uploadFilePromise(lists[i].url)
let item = form[event.name][fileListLen]
form[event.name].splice(
fileListLen,
1,
Object.assign(item, {
status: 'success',
message: '',
url: result,
})
)
fileListLen++
}
}
const deletePic = (event) => {
form[event.name].splice(event.index, 1)
}
const uploadFilePromise = (url) => {
return new Promise((resolve, reject) => {
http
.upload('/fileupload', {
filePath: url,
name: 'file',
})
.then((res) => {
resolve(res.url)
})
.catch((err) => {
reject(err)
})
})
}
const onSubmit = () => {
http
.post('/complaints', {
content: form.content,
photos: form.photos.map((item) => item.url),
anonymous: true
})
.then((ress) => {
uni.showToast({
title: '提交成功',
duration: 2000,
icon: 'none',
})
formRef.value.resetFields()
})
}
</script>

View File

@ -0,0 +1,35 @@
<template>
<view class="px-base">
<CuNavbar title="设置"></CuNavbar>
<view class="space-y-base mt-base">
<view class="card-shadow bg-white rounded-19rpx">
<Cell title="修改密码" :shadow="false" @onClick="goPath('/pages/setting/password')"></Cell>
<uv-line color="#f5f5f5"></uv-line>
<Cell title="举报投诉" :shadow="false" @click="goPath('/pages/setting/complain')"></Cell>
<uv-line color="#f5f5f5"></uv-line>
<Cell title="意见箱" :shadow="false" @click="goPath('/pages/setting/suggestion')"></Cell>
<uv-line color="#f5f5f5"></uv-line>
</view>
<view>
<Cell title="当前版本" :isLink="false">
<view class="flex justify-end text-hex-999999 text-24rpx px-base">
v1.0.0
</view>
</Cell>
</view>
</view>
<view class="mt-100rpx">
<uv-button block type="primary">退出登录</uv-button>
</view>
</view>
</template>
<script setup>
import CuNavbar from '@/components/cu-navbar/index'
import Cell from '@/components/cell/index'
const goPath = (url) => {
uni.navigateTo({
url
})
}
</script>

View File

@ -0,0 +1,98 @@
<template>
<view class="px-base">
<CuNavbar title="修改密码">
<!-- <template #right>
<view @click="submit" class="text-white text-24rpx">保存</view>
</template> -->
</CuNavbar>
<view class="space-y-base mt-base">
<view class="card-shadow bg-white rounded-19rpx px-base">
<uv-form
labelWidth="160rpx"
labelPosition="left"
:borderBottom="false"
:model="form"
:rules="rules"
errorType="toast"
ref="formRef"
>
<uv-form-item label="新登录密码" prop="password">
<uv-input
border="none"
input-align="right"
type="password"
v-model="form.password"
placeholder="请输入新登录密码"
></uv-input>
</uv-form-item>
<uv-line color="#f5f5f5"></uv-line>
<uv-form-item label="新登录密码" prop="password2">
<uv-input
border="none"
v-model="form.password2"
input-align="right"
type="password"
placeholder="请输入新登录密码"
></uv-input>
</uv-form-item>
</uv-form>
</view>
</view>
<view class="mt-100rpx">
<uv-button type="primary" @click="submit"></uv-button>
</view>
<uv-modal
ref="modalRef"
title="提示"
content="确定修改密码?"
@confirm="changePassword"
:showCancelButton="true"
></uv-modal>
</view>
</template>
<script setup>
import CuNavbar from '@/components/cu-navbar/index'
import Cell from '@/components/cell/index'
import { ref, reactive } from 'vue'
import { http } from '@/utils/request'
const modalRef = ref(null)
const formRef = ref(null)
const form = reactive({
password: '',
password2: '',
})
const rules = reactive({
password: [{ required: true, message: '请输入新登录密码' }],
password2: [
{ required: true, message: '请输入新登录密码' },
{
validator: (rule, value) => {
return value === form.password
},
message: '两次密码不一致',
},
],
})
const submit = () => {
formRef.value.validate().then((res) => {
modalRef.value.open()
})
}
const changePassword = () => {
http
.post('/auth/profile', {
password: form.password,
password_confirmation: form.password2,
})
.then((ress) => {
uni.showToast({
title: '修改成功',
duration: 2000,
icon: 'none',
})
formRef.value.resetFields()
})
}
</script>

View File

@ -0,0 +1,80 @@
<template>
<view class="px-base">
<CuNavbar title="意见箱"> </CuNavbar>
<view class="space-y-base mt-base">
<view class="card-shadow bg-white rounded-19rpx px-base">
<uv-form
labelWidth="160rpx"
:borderBottom="false"
:model="form"
:rules="rules"
errorType="toast"
ref="formRef"
labelPosition="top"
>
<uv-form-item label="意见内容" prop="content">
<uv-textarea
maxlength="200"
:customStyle="{ padding: 0 }"
count
placeholder="可描述具体意见内容至少20字"
border="none"
v-model="form.content"
>
</uv-textarea>
</uv-form-item>
</uv-form>
</view>
</view>
<view class="mt-100rpx">
<uv-button type="primary" @click="submit"></uv-button>
</view>
<uv-modal
ref="modalRef"
title="提示"
content="确定提交意见?"
@confirm="onSubmit"
:showCancelButton="true"
></uv-modal>
</view>
</template>
<script setup>
import CuNavbar from '@/components/cu-navbar/index'
import Cell from '@/components/cell/index'
import { ref, reactive } from 'vue'
import { http } from '@/utils/request'
const modalRef = ref(null)
const formRef = ref(null)
const form = reactive({
content: '',
})
const rules = reactive({
content: [
{ required: true, message: '可描述具体意见内容至少20字' },
{ min: 20, message: '至少20字' },
{ max: 200, message: '最多200字' },
],
})
const submit = () => {
formRef.value.validate().then((res) => {
modalRef.value.open()
})
}
const onSubmit = () => {
http
.post('/feedback', {
content: form.content,
})
.then((ress) => {
uni.showToast({
title: '提交成功',
duration: 2000,
icon: 'none',
})
formRef.value.resetFields()
})
}
</script>

View File

@ -0,0 +1,55 @@
<template>
<view class="px-base">
<CuNavbar title="任务详情"></CuNavbar>
<view
class="mt-30rpx card-shadow bg-white rounded-19rpx px-base text-[#333333] text-27rpx"
>
<view class="py-20rpx flex items-center justify-between">
<view>申请人</view>
<view class="text-hex-999999">测试人</view>
</view>
<uv-line></uv-line>
<view class="py-20rpx flex items-center justify-between">
<view>所属门店</view>
<view class="text-hex-999999">具体门店名称</view>
</view>
<uv-line></uv-line>
<view class="py-20rpx flex items-center justify-between">
<view>电话号码</view>
<view class="text-hex-999999">具体门店名称</view>
</view>
<uv-line></uv-line>
<view class="py-20rpx flex items-center justify-between">
<view>申请时间</view>
<view class="text-hex-999999">具体门店名称</view>
</view>
<uv-line></uv-line>
<view class="py-20rpx">
<view>申请范围</view>
<view class="text-hex-999999 mt-20rpx">具体门店名称</view>
</view>
<uv-line></uv-line>
<view class="py-20rpx">
<view>清洁结果</view>
<view class="text-hex-999999 mt-20rpx">
<view class="bg-gray-50 b-solid w-130rpx h-130rpx"></view>
</view>
</view>
</view>
<view class="h-100rpx">
<view
class="fixed bottom-0 left-0 right-0 h-120rpx bg-white flex items-center px-base space-x-30rpx"
>
<view class="flex-1">
<uv-button color="#999999" shape="circle" plain block> 拒绝 </uv-button>
</view>
<view class="flex-1">
<uv-button type="primary" shape="circle" block> 通过 </uv-button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import CuNavbar from '@/components/cu-navbar/index'
</script>

View File

@ -0,0 +1,34 @@
<template>
<view>
<CuNavbar title="我的任务"></CuNavbar>
<uv-sticky bgColor="#fff">
<uv-tabs
:activeStyle="{ color: '#ee2c37' }"
:scrollable="false"
lineColor="#ee2c37"
:list="tabList"
></uv-tabs>
</uv-sticky>
<view class="px-base space-y-20rpx mt-30rpx">
<view v-for="item in 4" class="card-shadow bg-white rounded-19rpx p-base space-y-10rpx">
<view class="text-30rpx">月度清洁任务</view>
<view class="text-24rpx text-hex-999999">
任务时间2022年03月01号 - 2022年03月31号
</view>
<view class="text-24rpx text-hex-999999">待完成</view>
</view>
</view>
</view>
</template>
<script setup>
import CuNavbar from '@/components/cu-navbar/index'
import { ref } from 'vue'
const tabList = ref([
{
name: '任务列表',
},
{
name: '任务审核',
},
])
</script>

View File

@ -0,0 +1,93 @@
<template>
<view class="px-base">
<CuNavbar title="任务提交"> </CuNavbar>
<view class="space-y-base mt-base">
<view class="card-shadow bg-white rounded-19rpx px-base">
<uv-form
labelWidth="160rpx"
:borderBottom="false"
:model="form"
:rules="rules"
errorType="toast"
ref="formRef"
labelPosition="top"
>
<uv-form-item label="清洁范围" required prop="content">
<uv-input
border="bottom"
placeholder="请输入清洁范围"
v-model="form.content"
></uv-input>
</uv-form-item>
<uv-line color="#f5f5f5"></uv-line>
<uv-form-item label="" prop="photos" required>
<view class="w-full">
<view class="flex justify-between text-15px">
<view>清洁结果</view>
<view class="text-hex-999999 text-12px pr-9px">0/9</view>
</view>
<view class="mt-10rpx">
<uv-upload></uv-upload>
</view>
</view>
</uv-form-item>
</uv-form>
</view>
</view>
<view class="mt-100rpx">
<uv-button type="primary" @click="submit"></uv-button>
</view>
<uv-modal
ref="modalRef"
title="提示"
content="确定提交投诉?"
@confirm="changePassword"
:showCancelButton="true"
></uv-modal>
</view>
</template>
<script setup>
import CuNavbar from '@/components/cu-navbar/index'
import Cell from '@/components/cell/index'
import { ref, reactive } from 'vue'
import { http } from '@/utils/request'
const modalRef = ref(null)
const formRef = ref(null)
const form = reactive({
content: '',
photos: [],
})
const rules = reactive({
content: [
{ required: true, message: '请输入投诉内容' },
{ min: 20, message: '至少20字' },
{ max: 200, message: '最多200字' },
],
photos: {
type: 'array',
},
})
const submit = () => {
formRef.value.validate().then((res) => {
modalRef.value.open()
})
}
const changePassword = () => {
// http
// .post('/auth/profile', {
// password: form.password,
// password_confirmation: form.password2,
// })
// .then((ress) => {
// uni.showToast({
// title: '',
// duration: 2000,
// icon: 'none',
// })
// formRef.value.resetFields()
// })
}
</script>

View File

@ -0,0 +1,6 @@
<template>
<view>
<CuNavbar title="上传图片"></CuNavbar>
<uv-upload></uv-upload>
</view>
</template>

View File

@ -0,0 +1,91 @@
<template>
<view class="p-base">
<CuNavbar title="员工详情">
<template #right>
<uv-icon color="white" @click="open" name="more-dot-fill"></uv-icon>
</template>
</CuNavbar>
<view class="mt-20rpx card-shadow px-base text-14px">
<view class="flex justify-between items-center min-h-88rpx">
<view class="text-hex-999">头像</view>
<view class="w-80rpx h-80rpx rounded-full overflow-hidden">
<image class="w-full h-full" :src="detail.avatar"></image>
</view>
</view>
<uv-line color="#f5f5f5"></uv-line>
<view class="flex justify-between items-center min-h-88rpx">
<view class="text-hex-999">姓名</view>
<view class="">{{ detail.name }}</view>
</view>
<uv-line color="#f5f5f5"></uv-line>
<view class="flex justify-between items-center min-h-88rpx">
<view class="text-hex-999">手机号</view>
<view class="">{{ detail.phone }}</view>
</view>
<uv-line color="#f5f5f5"></uv-line>
<view class="flex justify-between items-center min-h-88rpx">
<view class="text-hex-999">门店</view>
<view class="" v-if="detail.store">{{ detail.store.address }}</view>
<view class="" v-else></view>
</view>
<uv-line color="#f5f5f5"></uv-line>
</view>
<uv-action-sheet
ref="actionSheet"
:actions="actionlist"
@select="select"
cancelText="取消"
>
</uv-action-sheet>
</view>
</template>
<script setup>
import { ref } from 'vue'
import CuNavbar from '@/components/cu-navbar/index'
import { http } from '@/utils/request'
import { onLoad, onShow } from '@dcloudio/uni-app'
const actionSheet = ref(null)
const id = ref(null)
const detail = ref({})
const actionlist = ref([
{
name: '编辑',
value: 'edit',
},
{
name: '离职',
value: 'quit',
},
{
name: '删除',
value: 'delete',
},
])
onLoad((options) => {
id.value = options.id
})
onShow(() => {
getDetail()
})
const getDetail = () => {
http.get(`/hr/employee/${id.value}`).then((res) => {
console.log(res)
detail.value = res
})
}
const open = () => {
actionSheet.value.open()
}
const select = (e) => {
const { value } = e
if (value === 'edit') {
uni.navigateTo({
url: `/pages/user/update?id=${id.value}`,
})
}
}
</script>

View File

@ -2,70 +2,75 @@
<view>
<CuNavbar title="员工管理">
<template #right>
<text @click="goPage('/pages/user/update')" class="text-white">添加</text>
<text @click="goPage('/pages/user/update')" class="text-white"
>添加</text
>
</template>
</CuNavbar>
<StoreDropDown></StoreDropDown>
<uv-drop-down
ref="dropDown"
sign="dropDown_1"
text-active-color="#3c9cff"
:extra-icon="{ name: 'arrow-down-fill', color: '#666', size: '26rpx' }"
:extra-active-icon="{
name: 'arrow-up-fill',
color: '#3c9cff',
size: '26rpx',
}"
:defaultValue="['all', 'all']"
:custom-style="{ padding: '0 30rpx' }"
@click="selectMenu"
>
<uv-drop-down-item
name="area"
type="2"
:label="dropDownData.area.label"
:value="dropDownData.area.value"
>
</uv-drop-down-item>
<uv-drop-down-item
name="store"
type="2"
:label="dropDownData.store.label"
:value="dropDownData.store.value"
>
</uv-drop-down-item>
</uv-drop-down>
<view class="mt-15rpx">
<uv-swipe-action>
<uv-swipe-action-item v-for="item in 3" :key="item" :options="options">
<view class="flex p-20rpx">
<view class="rounded-4rpx bg-true-gray-400 w-100rpx h-100rpx">
</view>
<view class="flex-1 ml-20rpx flex flex-col justify-between">
<view class="flex items-center text-sm">
<view>员工名称</view>
<uv-tags
class="ml-20rpx"
text="职位"
plain
size="mini"
type="primary"
></uv-tags>
<mescroll-body @init="mescrollInit" @down="downCallback" @up="upCallback">
<view class="mt-15rpx px-base">
<!-- <uv-swipe-action> -->
<view class="space-y-15rpx">
<view
class="rounded-19rpx card p-0 overflow-hidden"
v-for="(item, i) in list"
:key="i"
@click="goPage(`/pages/user/detail?id=${item.id}`)"
>
<!-- <uv-swipe-action-item :options="options"> -->
<view class="flex rounded-19rpx box-content card">
<view class="rounded-4rpx w-100rpx h-100rpx">
<image class="w-full h-full" :src="item.avatar"></image>
</view>
<view>
<view class="text-28rpx text-hex-999 text-xs">188888888</view>
<view
class="flex-1 ml-20rpx flex flex-col justify-between py-10rpx"
>
<view class="flex items-center text-sm">
<view>{{ item.name }}</view>
<template v-if="item.jobs">
<uv-tags
v-for="job in item.jobs"
class="ml-20rpx"
:text="job.name"
:key="job.id"
plain
size="mini"
type="primary"
></uv-tags>
</template>
</view>
<view class="flex justify-between">
<view class="text-28rpx text-hex-999 text-xs">{{
item.phone
}}</view>
<view v-if="item.store">{{ item.store.address }}</view>
</view>
</view>
</view>
<!-- </uv-swipe-action-item> -->
</view>
</uv-swipe-action-item>
</uv-swipe-action>
</view>
</view>
<!-- </uv-swipe-action> -->
</view>
</mescroll-body>
</view>
</template>
<script setup>
import CuNavbar from '@/components/cu-navbar/index'
import { http } from '@/utils/request'
import { computed, reactive, ref } from 'vue'
import StoreDropDown from '@/pages/home/components/store-drop-down/index.vue'
import { onPageScroll, onReachBottom } from '@dcloudio/uni-app'
import useMescroll from '@/uni_modules/mescroll-uni/hooks/useMescroll.js'
const { mescrollInit, downCallback, getMescroll } = useMescroll(
onPageScroll,
onReachBottom
)
const list = ref([])
const options = [
{
text: '删除',
@ -86,66 +91,24 @@ const options = [
},
},
]
const dropDownData = reactive({
area: {
label: '全部区域',
value: 'all',
activeIndex: 0,
color: '#333',
activeColor: '#2878ff',
child: [
{
label: '全部区域',
value: 'all',
},
{
label: '重庆',
value: 'cq',
},
{
label: '北京',
value: 'bj',
},
],
},
store: {
label: '全部门店',
value: 'all',
activeIndex: 0,
color: '#333',
activeColor: '#2878ff',
child: [
{
label: '全部门店',
value: 'all',
},
{
label: '门店1',
value: 'new',
},
{
label: '门店2',
value: 'money',
},
],
},
})
const result = ref([])
const upCallback = async (mescroll) => {
const { size, num } = mescroll
const activeName = ref('area')
const selectMenu = (e) => {
const { name, active, type } = e
activeName.value = name
const find = result.value.find((item) => item.name == activeName.value)
if (find) {
const findIndex = dropDownData[activeName.value].child.findIndex(
(item) => item.label == find.label && item.value == find.value
)
dropDownData[activeName.value].activeIndex = findIndex
} else {
dropDownData[activeName.value].activeIndex = 0
try {
const resData = await http.get('/hr/employee', {
params: {
per_page: size,
page: num,
},
})
const curPageData = resData.data || []
if (num === 1) list.value = []
list.value = list.value.concat(curPageData)
mescroll.endSuccess(curPageData.length)
} catch (error) {
console.log(error)
mescroll.endErr() // ,
}
}

View File

@ -1,45 +1,188 @@
<template>
<view>
<CuNavbar title="员工添加">
<template #right>
<view class="p-base">
<CuNavbar :title="title">
<!-- <template #right>
<view class="text-white">保存</view>
</template>
</template> -->
</CuNavbar>
<view class="px-base">
<uv-form labelPosition="left" ref="form" labelWidth="150rpx">
<uv-form-item required label="姓名" prop="userInfo.name" borderBottom>
<uv-input placeholder="请输入姓名" inputAlign="right" border="none">
</uv-input>
</uv-form-item>
<uv-form-item required label="手机号" prop="userInfo.name" borderBottom>
<uv-input placeholder="请输入手机号" inputAlign="right" border="none">
</uv-input>
</uv-form-item>
<uv-form-item
required
label="登录用户名"
prop="userInfo.name"
borderBottom
>
<view class="card-shadow px-base">
<uv-form
labelPosition="left"
:model="form"
:rules="rules"
ref="formRef"
errorType="toast"
labelWidth="150rpx"
>
<uv-form-item required label="姓名" prop="name">
<uv-input
placeholder="请输入登录用户名"
placeholder="请输入姓名"
inputAlign="right"
border="none"
:border="`none`"
v-model="form.name"
>
</uv-input>
</uv-form-item>
<uv-form-item :required="false" label="登录密码" prop="userInfo.name">
<uv-line color="#f5f5f5"></uv-line>
<uv-form-item required label="手机号" prop="phone">
<uv-input
placeholder="请输入手机号"
inputAlign="right"
:border="`none`"
type="number"
maxlength="11"
v-model="form.phone"
>
</uv-input>
</uv-form-item>
<uv-line color="#f5f5f5"></uv-line>
<uv-form-item required label="登录用户名" prop="name">
<uv-input
placeholder="请输入登录用户名"
inputAlign="right"
v-model="form.username"
:border="`none`"
>
</uv-input>
</uv-form-item>
<uv-line color="#f5f5f5"></uv-line>
<uv-form-item :required="!isEdit" label="登录密码" prop="password">
<uv-input
placeholder="请输入登录密码"
inputAlign="right"
border="none"
type="password"
v-model="form.password"
:border="`none`"
>
</uv-input>
</uv-form-item>
</uv-form>
</view>
<view class="mt-100rpx">
<uv-button type="primary" @click="submit"></uv-button>
</view>
<uv-modal
ref="modalRef"
title="提示"
content="确定提交员工信息?"
@confirm="onSubmit"
:showCancelButton="true"
></uv-modal>
</view>
</template>
<script setup>
import CuNavbar from '@/components/cu-navbar/index'
import { ref, reactive, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { mobile } from '@climblee/uv-ui/libs/function/test'
import { http } from '@/utils/request'
const formRef = ref(null)
const modalRef = ref(null)
const id = ref(0)
const loading = ref(false)
const form = reactive({
name: '',
phone: '',
username: '',
password: '',
confirm_password: '',
})
const rules = computed(() => {
return {
name: [{ required: true, message: '请输入姓名' }],
phone: [
{ required: true, message: '请输入手机号' },
{
validator: (rule, value) => {
return mobile(value)
},
message: '请输入正确的手机号',
},
],
username: [{ required: true, message: '请输入登录用户名' }],
password: [{ required: !isEdit.value, message: '请输入登录密码' }],
confirm_password: [
{ required: !isEdit.value, message: '请输入登录密码' },
{
validator: (rule, value) => {
return form.password === value
},
message: '两次密码不一致',
},
],
}
})
onLoad((options) => {
id.value = options.id
getDetail()
})
const isEdit = computed(() => !!id.value)
const title = computed(() => (isEdit.value ? '员工修改' : '员工添加'))
const submit = () => {
formRef.value.validate().then((res) => {
modalRef.value.open()
})
}
const onSubmit = () => {
if (isEdit.value) {
updateUser()
} else {
addUser()
}
}
const updateUser = async () => {
if (loading.value) {
return
}
loading.value = true
try {
await http.put(`/hr/employee/${id.value}`, {
...form,
})
uni.navigateBack()
formRef.value.resetFields()
uni.showToast({
title: '修改成功',
duration: 2000,
icon: 'none',
})
} catch (error) {
} finally {
loading.value = false
}
}
const addUser = async () => {
if (loading.value) {
return
}
loading.value = true
try {
await http.post('/hr/employee', {
...form,
})
formRef.value.resetFields()
uni.showToast({
title: '添加成功',
duration: 2000,
icon: 'none',
})
} catch (error) {
} finally {
loading.value = false
}
}
const getDetail = () => {
http.get(`/hr/employee/${id.value}`).then((res) => {
const info = {
name: res.name,
phone: res.phone,
username: res.username,
}
Object.assign(form, info)
})
}
</script>

View File

@ -0,0 +1,58 @@
<template>
<view>
<CuNavbar title="升职申请">
<template #right>
<view @click="goPath('/pages/expense-account/submit')" class="text-24rpx text-white">申请</view>
</template>
</CuNavbar>
<uv-sticky bgColor="#fff">
<uv-tabs
:activeStyle="{ color: '#ee2c37' }"
:scrollable="false"
lineColor="#ee2c37"
:list="tabList"
></uv-tabs>
</uv-sticky>
<view class="px-base space-y-20rpx mt-30rpx">
<view
v-for="item in 4"
class="card-shadow bg-white rounded-19rpx p-base space-y-10rpx"
>
<view class="flex items-center justify-between">
<view class="text-30rpx"> 升职申请 </view>
<view class="text-24rpx text-hex-999999">待完成</view>
</view>
<view class="text-24rpx text-hex-999999 flex">
<view class="w-140rpx">推荐人</view>
<view class="text-primary">12313</view>
</view>
<view class="text-24rpx text-hex-999999 flex">
<view class="w-140rpx">晋升职位</view>
<view class="text-hex-333">2022-01-01</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import CuNavbar from '@/components/cu-navbar/index'
import { ref } from 'vue'
const tabList = ref([
{
name: '申请列表',
},
{
name: '推荐列表',
},
{
name: '审核列表',
},
])
const goPath = (url) => {
uni.navigateTo({
url
})
}
</script>

View File

@ -8,3 +8,9 @@ page {
border-radius: 10px;
padding: 20rpx;
}
.card-shadow{
background: #ffffff;
box-shadow: 0px 0px 16px 0px rgba(0, 0, 0, 0.04);
border-radius: 20rpx;
}

View File

@ -10,7 +10,7 @@ $uv-border-color: #dadbde;
$uv-bg-color: #f3f4f6;
$uv-disabled-color: #c8c9cc;
$uv-primary: red;
$uv-primary: #ee2c37;
$uv-primary-dark: #398ade;
$uv-primary-disabled: #9acafc;
$uv-primary-light: #ecf5ff;
@ -40,4 +40,8 @@ $uv-info-light: #f4f4f5;
display: flex;
/* #endif */
flex-direction: $direction;
}
.uv-navbar--fixed{
z-index: 1000 !important;
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>home3_icon_back_def</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon" transform="translate(-48.000000, -215.000000)">
<g id="home3_icon_back_def" transform="translate(48.000000, 215.000000)">
<rect id="矩形" fill="#fff" fill-rule="nonzero" opacity="0" x="0" y="0" width="28" height="28"></rect>
<g id="back" transform="translate(13.500000, 14.500000) scale(-1, 1) translate(-13.500000, -14.500000) translate(6.000000, 8.000000)" stroke="#fff" stroke-linecap="round" stroke-width="2">
<path d="M5,10 L8.59692142,5.91374903 C9.91060843,4.42134644 12.1853935,4.27646884 13.6777961,5.59015586 C13.7456217,5.64985929 13.8111753,5.71209516 13.8743179,5.77673125 L18,10 L18,10" id="路径-5" stroke-linejoin="round" transform="translate(11.500000, 6.500000) rotate(-270.000000) translate(-11.500000, -6.500000) "></path>
<line x1="0" y1="6.5" x2="8" y2="6.5" id="直线-3"></line>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>me_icon_more_def</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon" transform="translate(-238.000000, -264.000000)">
<g id="me_icon_more_def" transform="translate(238.000000, 264.000000)">
<rect id="矩形" x="0" y="0" width="14" height="14"></rect>
<g id="编组-9" transform="translate(0.000000, 1.000000)" stroke="#333333" stroke-linecap="round">
<path d="M4.5,9.5 L7.97809825,5.21946514 C9.02292793,3.93358143 10.9123445,3.73816853 12.1982282,4.78299821 C12.3183328,4.88058781 12.4307062,4.98732445 12.5343422,5.10225239 L16.5,9.5 L16.5,9.5" id="路径-5" stroke-linejoin="round" transform="translate(10.500000, 6.000000) rotate(-270.000000) translate(-10.500000, -6.000000) "></path>
<line x1="0.5" y1="6" x2="7.5" y2="6" id="直线"></line>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1713446913072" class="icon" viewBox="0 0 1084 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4312" xmlns:xlink="http://www.w3.org/1999/xlink" width="211.71875" height="200"><path d="M1072.147851 406.226367c-6.331285-33.456782-26.762037-55.073399-52.047135-55.073399-0.323417 0-0.651455 0.003081-0.830105 0.009241l-4.655674 0c-73.124722 0-132.618162-59.491899-132.618162-132.618162 0-23.731152 11.447443-50.336101 11.546009-50.565574 13.104573-29.498767 3.023185-65.672257-23.427755-84.127081l-1.601687-1.127342-134.400039-74.661726-1.700252-0.745401c-8.753836-3.805547-18.334698-5.735272-28.479231-5.735272-20.789593 0-41.235746 8.344174-54.683758 22.306575-14.741683 15.216028-65.622973 58.649474-104.721083 58.649474-39.450789 0-90.633935-44.286652-105.438762-59.784516-13.518857-14.247316-34.128258-22.753199-55.127302-22.753199-9.945862 0-19.354234 1.861961-27.958682 5.531982l-1.746455 0.74078-139.141957 76.431283-1.643269 1.139662c-26.537186 18.437884-36.675557 54.579032-23.584845 84.062398 0.115506 0.264895 11.579891 26.725075 11.579891 50.634877 0 73.126262-59.491899 132.618162-132.618162 132.618162l-4.581749 0c-0.318797-0.00616-0.636055-0.01078-0.951772-0.01078-25.260456 0-45.672728 21.618157-52.002472 55.0811-0.462025 2.453354-11.313456 60.622322-11.313456 106.117939 0 45.494078 10.85143 103.659965 11.314996 106.119479 6.334365 33.458322 26.758957 55.076479 52.036353 55.076479 0.320337 0 0.651455-0.00616 0.842426-0.012321l4.655674 0c73.126262 0 132.618162 59.491899 132.618162 132.616622 0 23.760413-11.444363 50.333021-11.546009 50.565574-13.093793 29.474125-3.041666 65.646075 23.395414 84.151722l1.569346 1.093459 131.838879 73.726895 1.675611 0.7377c8.750757 3.84251 18.305437 5.790715 28.397607 5.790715 21.082208 0 41.676209-8.706094 55.0888-23.290689 18.724339-20.347588 69.527086-62.362616 107.04815-62.362616 40.625872 0 92.72537 47.100385 107.759669 63.583903 13.441852 14.831008 34.176001 23.689571 55.470741 23.695731l0.00616 0c9.895039 0 19.27877-1.883523 27.893999-5.598205l1.711034-0.73924 136.659342-75.531873 1.617088-1.128882c26.492523-18.456365 36.601633-54.600594 23.538642-84.016195-0.115506-0.267974-11.595291-27.082374-11.595291-50.67646 0-73.124722 59.49344-132.616622 132.618162-132.616622l4.517066-0.00154c0.300316 0.00616 0.599092 0.009241 0.899409 0.009241 25.331299-0.00154 45.785153-21.619697 52.107197-55.054918 0.112426-0.589852 11.325776-59.507301 11.325776-106.14104C1083.464388 466.640776 1072.609877 408.67356 1072.147851 406.226367zM377.486862 945.656142l-115.32764-64.487932c5.082277-13.052211 15.437801-43.51815 15.437801-75.017486 0-109.382917-84.176364-199.816642-192.587488-208.134635-2.647404-15.427021-8.873963-54.967133-8.873963-85.667166 0-30.65691 6.223479-70.232445 8.869343-85.671786 108.415744-8.311832 192.592108-98.745557 192.592108-208.134635 0-31.416171-10.300081-61.797405-15.371577-74.854236l122.721583-67.40331c0.003081 0 0.00462 0.00154 0.007701 0.00154 4.423121 4.518606 22.121764 22.080182 46.558275 39.493911 39.929754 28.46229 77.952885 42.894416 113.014434 42.894416 34.716571 0 72.437845-14.151831 112.115025-42.06431 24.282503-17.07953 41.896442-34.302288 46.308782-38.74543 0.009241-0.00154 0.018481-0.00462 0.026182-0.00616l118.301542 65.726159c-5.077657 13.055291-15.416239 43.499669-15.416239 74.958962 0 109.389077 84.174824 199.822802 192.590568 208.134635 2.645865 15.462442 8.872423 55.107281 8.872423 85.671786 0 30.687711-6.223479 70.241685-8.869343 85.673326C890.042174 606.334084 805.86427 696.767809 805.86427 806.158426c0 31.450053 10.317022 61.851309 15.393138 74.903519l-119.783103 66.198965c-5.168521-5.490399-22.603811-23.363073-46.740005-41.288109-40.701336-30.224145-79.662378-45.549521-115.800446-45.549521-35.79155 0-74.458435 15.038919-114.927219 44.694774C400.22004 922.554885 382.666163 940.255068 377.486862 945.656142zM731.271848 511.646647c0-105.803762-86.081448-191.88059-191.888289-191.88059-105.803762 0-191.88059 86.076827-191.88059 191.88059 0 105.803762 86.076827 191.882129 191.88059 191.882129C645.19194 703.528777 731.271848 617.450409 731.271848 511.646647zM539.383558 395.903184c63.825696 0 115.751164 51.922387 115.751164 115.743463 0 63.825696-51.925468 115.751164-115.751164 115.751164-63.821076 0-115.743463-51.925468-115.743463-115.751164C423.640095 447.824031 475.562482 395.903184 539.383558 395.903184z" fill="#ffffff" p-id="4313"></path></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1,23 @@
import { defineStore } from 'pinia';
import { store } from '@/store';
import { http } from "@/utils/request";
export const useDataStore = defineStore({
id: 'app-data',
state: () => ({
date: ''
}),
getters: {
getDateGetter(state) {
return state.date || Number(new Date())
}
},
actions: {
setDate(date) {
this.date = date
}
},
persist: {
paths: ['date']
}
})

View File

@ -0,0 +1,63 @@
import { defineStore } from 'pinia';
import { store } from '@/store';
import { http } from "@/utils/request";
export const useUserStore = defineStore({
id: 'app-user',
state: () => ({
userInfo: null,
token: null,
}),
getters: {
getUserInfo(state) {
return state.userInfo || {};
},
isLogin(state) {
return !!state.token;
}
},
actions: {
login(data) {
return new Promise((resolve, reject) => {
http.post('/auth/login', data).then(async (res) => {
this.token = res.token;
this.fetchUserInfo();
resolve(res);
}).catch(err => {
reject(err);
});
});
},
fetchUserInfo() {
return new Promise((resolve, reject) => {
http.get('/auth/profile').then(async (res) => {
this.userInfo = res
resolve(res);
}).catch(err => {
reject(err);
})
});
},
resetState() {
this.token = null;
this.userInfo = null;
},
async logout() {
return new Promise((resolve, reject) => {
http.delete('/auth/logout').then(async (res) => {
this.resetState();
}).catch(err => {
reject(err);
});
});
},
},
persist: true,
});
export function useUserStoreWithOut() {
return useUserStore(store);
}

View File

@ -0,0 +1,8 @@
## 1.3.82023-03-27
1. 新增useMescroll的hook, 支持vue3 script setup的写法
2. 新增vue3 script setup的示例 ( 根据vue2的示例,全部重写了一遍 )
3. mescroll-body 和 mescroll-uni 无需再写 ref="mescrollRef"
4. 解决mescroll-uni在页面渲染之后,无法动态设置height的问题
5. 解决renderjs在h5返回有时候无法正常滑动的问题
6. 修复小程序编辑器提示 Cannot read property 'nv_optDown' of undefined 的错误
-by 小瑾同学

View File

@ -0,0 +1,19 @@
.mescroll-body {
position: relative; /* 下拉刷新区域相对自身定位 */
height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/
overflow: hidden; /* 当有元素写在mescroll-body标签前面时,可遮住下拉刷新区域 */
box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
}
/* 使sticky生效: 父元素不能overflow:hidden或者overflow:auto属性 */
.mescroll-body.mescorll-sticky{
overflow: unset !important
}
/* 适配 iPhoneX */
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
.mescroll-safearea {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}

View File

@ -0,0 +1,400 @@
<template>
<view
class="mescroll-body mescroll-render-touch"
:class="{'mescorll-sticky': sticky}"
:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}"
@touchstart="wxsBiz.touchstartEvent"
@touchmove="wxsBiz.touchmoveEvent"
@touchend="wxsBiz.touchendEvent"
@touchcancel="wxsBiz.touchendEvent"
:change:prop="wxsBiz.propObserver"
:prop="wxsProp"
>
<!-- 状态栏 -->
<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp">
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
<view class="downwarp-content">
<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
<view class="downwarp-tip">{{downText}}</view>
</view>
</view>
<!-- 列表内容 -->
<slot></slot>
<!-- 空布局 -->
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-show="upLoadType===1">
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
</view>
<!-- 无数据 -->
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
</view>
</view>
<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
<!-- #ifdef H5 -->
<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
<!-- #endif -->
<!-- 适配iPhoneX -->
<view v-if="safearea" class="mescroll-safearea"></view>
<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
<!-- #endif -->
</view>
</template>
<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
<script src="../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
<!-- #endif -->
<!-- app, h5使用renderjs -->
<!-- #ifdef APP-PLUS || H5 -->
<script module="renderBiz" lang="renderjs">
import renderBiz from "../mescroll-uni/wxs/renderjs.js";
export default {
mixins: [renderBiz]
}
</script>
<!-- #endif -->
<script>
// mescroll-uni.js,
import MeScroll from "../mescroll-uni/mescroll-uni.js";
//
import GlobalOption from "../mescroll-uni/mescroll-uni-option.js";
//
import mescrollI18n from '../mescroll-uni/mescroll-i18n.js';
//
import MescrollTop from "../mescroll-uni/components/mescroll-top.vue";
// wxs(renderjs)mixins
import WxsMixin from "../mescroll-uni/wxs/mixins.js";
/**
* mescroll-body 基于page滚动的下拉刷新和上拉加载组件, 支持嵌套原生组件, 性能好
* @property {Object} down 下拉刷新的参数配置
* @property {Object} up 上拉加载的参数配置
* @property {Object} i18n 国际化的参数配置
* @property {String, Number} top 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
* @property {Boolean, String} topbar 偏移量top是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
* @property {String, Number} bottom 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
* @property {Boolean} safearea 偏移量bottom是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
* @property {Boolean} fixed 是否通过fixed固定mescroll的高度, 默认true
* @property {String, Number} height 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
* @property {Boolean} bottombar 底部是否偏移TabBar的高度 (仅在H5端的tab页生效)
* @property {Boolean} sticky 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法隐藏
* @event {Function} init 初始化完成的回调
* @event {Function} down 下拉刷新的回调
* @event {Function} up 上拉加载的回调
* @event {Function} emptyclick 点击empty配置的btnText按钮回调
* @event {Function} topclick 点击回到顶部的按钮回调
* @event {Function} scroll 滚动监听 (需在 up 配置 onScroll:true 才生效)
* @example <mescroll-body @init="mescrollInit" @down="downCallback" @up="upCallback"> ... </mescroll-body>
*/
export default {
name: 'mescroll-body',
mixins: [WxsMixin],
components: {
MescrollTop
},
props: {
down: Object,
up: Object,
i18n: Object,
top: [String, Number],
topbar: [Boolean, String],
bottom: [String, Number],
safearea: Boolean,
height: [String, Number],
bottombar:{
type: Boolean,
default: true
},
sticky: Boolean
},
data() {
return {
mescroll: {optDown:{},optUp:{}}, // mescroll
downHight: 0, //:
downRate: 0, // (inOffset: rate<1; outOffset: rate>=1)
downLoadType: 0, // : 0(loading), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
upLoadType: 0, // 0loading1loading2,END3,END
isShowEmpty: false, //
isShowToTop: false, //
windowHeight: 0, // 使
windowBottom: 0, // 使
statusBarHeight: 0 //
};
},
computed: {
// mescroll,windowHeight,使
minHeight(){
return this.toPx(this.height || '100%') + 'px'
},
// (px)
numTop() {
return this.toPx(this.top)
},
padTop() {
return this.numTop + 'px';
},
// (px)
numBottom() {
return this.toPx(this.bottom);
},
padBottom() {
return this.numBottom + 'px';
},
//
isDownReset() {
return this.downLoadType === 3 || this.downLoadType === 4;
},
//
transition() {
return this.isDownReset ? 'transform 300ms' : '';
},
translateY() {
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform使fixed,fixedmescroll
},
//
isDownLoading(){
return this.downLoadType === 3
},
//
downRotate(){
return 'rotate(' + 360 * this.downRate + 'deg)'
},
//
downText(){
if(!this.mescroll) return ""; //
switch (this.downLoadType){
case 1: return this.mescroll.optDown.textInOffset;
case 2: return this.mescroll.optDown.textOutOffset;
case 3: return this.mescroll.optDown.textLoading;
case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
default: return this.mescroll.optDown.textInOffset;
}
}
},
methods: {
//number,rpx,upx,px,% --> px
toPx(num) {
if (typeof num === 'string') {
if (num.indexOf('px') !== -1) {
if (num.indexOf('rpx') !== -1) {
// "10rpx"
num = num.replace('rpx', '');
} else if (num.indexOf('upx') !== -1) {
// "10upx"
num = num.replace('upx', '');
} else {
// "10px"
return Number(num.replace('px', ''));
}
} else if (num.indexOf('%') !== -1) {
// ,windowHeight,"10%"windowHeight10%
let rate = Number(num.replace('%', '')) / 100;
return this.windowHeight * rate;
}
}
return num ? uni.upx2px(Number(num)) : 0;
},
//
emptyClick() {
this.$emit('emptyclick', this.mescroll);
},
//
toTopClick() {
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); //
this.$emit('topclick', this.mescroll); //
}
},
// 使createdmescroll; mountedcssH5
created() {
let vm = this;
let diyOption = {
//
down: {
inOffset() {
vm.downLoadType = 1; // offset (mescroll,)
},
outOffset() {
vm.downLoadType = 2; // offset (mescroll,)
},
onMoving(mescroll, rate, downHight) {
// ,;
vm.downHight = downHight; // (mescroll,)
vm.downRate = rate; // (inOffset: rate<1; outOffset: rate>=1)
},
showLoading(mescroll, downHight) {
vm.downLoadType = 3; // (mescroll,)
vm.downHight = downHight; // (mescroll,)
},
beforeEndDownScroll(mescroll){
vm.downLoadType = 4;
return mescroll.optDown.beforeEndDelay //
},
endDownScroll() {
vm.downLoadType = 4; // (mescroll,)
vm.downHight = 0; // (mescroll,)
if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} //
vm.downResetTimer = setTimeout(()=>{ // ,0,inOffsettextInOffset
if(vm.downLoadType === 4) vm.downLoadType = 0
},300)
},
//
callback: function(mescroll) {
vm.$emit('down', mescroll);
}
},
//
up: {
//
showLoading() {
vm.upLoadType = 1;
},
//
showNoMore() {
vm.upLoadType = 2;
},
//
hideUpScroll(mescroll) {
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
},
//
empty: {
onShow(isShow) {
//
vm.isShowEmpty = isShow;
}
},
//
toTop: {
onShow(isShow) {
//
vm.isShowToTop = isShow;
}
},
//
callback: function(mescroll) {
vm.$emit('up', mescroll);
}
}
};
let i18nType = mescrollI18n.getType() //
let i18nOption = {type: i18nType} //
MeScroll.extend(i18nOption, vm.i18n) //
MeScroll.extend(i18nOption, GlobalOption.i18n) //
MeScroll.extend(diyOption, i18nOption[i18nType]); //
MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); //
let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // ,props
MeScroll.extend(myOption, diyOption); //
// MeScroll
vm.mescroll = new MeScroll(myOption, true); // true,body
//
vm.mescroll.i18n = i18nOption;
// initmescroll
vm.$emit('init', vm.mescroll);
//
const sys = uni.getSystemInfoSync();
if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
// 使downbottomOffset
vm.mescroll.setBodyHeight(sys.windowHeight);
// 使pagescroll,scrollTo
vm.mescroll.resetScrollTo((y, t) => {
if(typeof y === 'string'){
// view (ycss)
setTimeout(()=>{ // view; 使$nextTick
let selector;
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
selector = '#'+y // #. id
}else{
selector = y
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
if(y.indexOf('>>>')!=-1){ // ()
selector = y.split('>>>')[1].trim()
}
// #endif
}
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
if (rect) {
let top = rect.top
top += vm.mescroll.getScrollTop()
uni.pageScrollTo({
scrollTop: top,
duration: t
})
} else{
console.error(selector + ' does not exist');
}
}).exec()
},30)
} else{
// (y)
uni.pageScrollTo({
scrollTop: y,
duration: t
})
}
});
// up.toTop.safearea,vuesafearea
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
vm.mescroll.optUp.toTop.safearea = vm.safearea;
}
//
uni.$on("setMescrollGlobalOption", options=>{
if(!options) return;
let i18nType = options.i18n ? options.i18n.type : null
if(i18nType && vm.mescroll.i18n.type != i18nType){
vm.mescroll.i18n.type = i18nType
mescrollI18n.setType(i18nType)
MeScroll.extend(options, vm.mescroll.i18n[i18nType])
}
if(options.down){
let down = MeScroll.extend({}, options.down)
vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
}
if(options.up){
let up = MeScroll.extend({}, options.up)
vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
}
})
},
destroyed() {
//
uni.$off("setMescrollGlobalOption")
}
};
</script>
<style>
@import "../mescroll-body/mescroll-body.css";
@import "../mescroll-uni/components/mescroll-down.css";
@import "../mescroll-uni/components/mescroll-up.css";
</style>

View File

@ -0,0 +1,116 @@
<!--空布局:
遵循easycom规范, 可作为独立的组件, 不使用mescroll的页面也能使用:
<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty>
-->
<template>
<view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }" :style="{ 'z-index': option.zIndex, top: option.top }">
<view> <image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" /> </view>
<view v-if="tip" class="empty-tip">{{ tip }}</view>
<view v-if="btnText" class="empty-btn" @click="emptyClick">{{ btnText }}</view>
</view>
</template>
<script>
//
import GlobalOption from '../mescroll-uni/mescroll-uni-option.js';
//
import mescrollI18n from '../mescroll-uni/mescroll-i18n.js';
export default {
props: {
// empty: GlobalOption.up.empty
option: {
type: Object,
default() {
return {};
}
}
},
// 使computed,option
computed: {
//
icon() {
if (this.option.icon != null) { // 使,
return this.option.icon
} else{
let i18nType = mescrollI18n.getType() //
if (this.option.i18n) {
return this.option.i18n[i18nType].icon
} else{
return GlobalOption.i18n[i18nType].up.empty.icon || GlobalOption.up.empty.icon
}
}
},
//
tip() {
if (this.option.tip != null) { //
return this.option.tip
} else{
let i18nType = mescrollI18n.getType() //
if (this.option.i18n) {
return this.option.i18n[i18nType].tip
} else{
return GlobalOption.i18n[i18nType].up.empty.tip || GlobalOption.up.empty.tip
}
}
},
//
btnText() {
if (this.option.i18n) {
let i18nType = mescrollI18n.getType() //
return this.option.i18n[i18nType].btnText
} else{
return this.option.btnText
}
}
},
methods: {
//
emptyClick() {
this.$emit('emptyclick');
}
}
};
</script>
<style>
/* 无任何数据的空布局 */
.mescroll-empty {
box-sizing: border-box;
width: 100%;
padding: 100rpx 50rpx;
text-align: center;
}
.mescroll-empty.empty-fixed {
z-index: 99;
position: absolute; /*transform会使fixed失效,最终会降级为absolute */
top: 100rpx;
left: 0;
}
.mescroll-empty .empty-icon {
width: 280rpx;
height: 280rpx;
}
.mescroll-empty .empty-tip {
margin-top: 20rpx;
font-size: 24rpx;
color: gray;
}
.mescroll-empty .empty-btn {
display: inline-block;
margin-top: 40rpx;
min-width: 200rpx;
padding: 18rpx;
font-size: 28rpx;
border: 1rpx solid #e04b28;
border-radius: 60rpx;
color: #e04b28;
}
.mescroll-empty .empty-btn:active {
opacity: 0.75;
}
</style>

View File

@ -0,0 +1,55 @@
/* 下拉刷新区域 */
.mescroll-downwarp {
position: absolute;
top: -100%;
left: 0;
width: 100%;
height: 100%;
text-align: center;
}
/* 下拉刷新--内容区,定位于区域底部 */
.mescroll-downwarp .downwarp-content {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
min-height: 60rpx;
padding: 20rpx 0;
text-align: center;
}
/* 下拉刷新--提示文本 */
.mescroll-downwarp .downwarp-tip {
display: inline-block;
font-size: 28rpx;
vertical-align: middle;
margin-left: 16rpx;
/* color: gray; 已在style设置color,此处删去*/
}
/* 下拉刷新--旋转进度条 */
.mescroll-downwarp .downwarp-progress {
display: inline-block;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid gray;
border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
vertical-align: middle;
}
/* 旋转动画 */
.mescroll-downwarp .mescroll-rotate {
animation: mescrollDownRotate 0.6s linear infinite;
}
@keyframes mescrollDownRotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,47 @@
<!-- 下拉刷新区域 -->
<template>
<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
<view class="downwarp-content">
<view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mOption.textColor, 'transform':downRotate}"></view>
<view class="downwarp-tip">{{downText}}</view>
</view>
</view>
</template>
<script>
export default {
props: {
option: Object , // down
type: Number, // inOffset1 outOffset2 showLoading3 endDownScroll4
rate: Number // (inOffset: rate<1; outOffset: rate>=1)
},
computed: {
// ,propdefault
mOption(){
return this.option || {}
},
//
isDownLoading(){
return this.type === 3
},
//
downRotate(){
return 'rotate(' + 360 * this.rate + 'deg)'
},
//
downText(){
switch (this.type){
case 1: return this.mOption.textInOffset;
case 2: return this.mOption.textOutOffset;
case 3: return this.mOption.textLoading;
case 4: return this.mOption.textLoading;
default: return this.mOption.textInOffset;
}
}
}
};
</script>
<style>
@import "./mescroll-down.css";
</style>

View File

@ -0,0 +1,99 @@
<!-- 回到顶部的按钮 -->
<template>
<image
v-if="option.src"
class="mescroll-totop"
:class="[isShow ? 'mescroll-totop-in' : 'mescroll-totop-out', {'mescroll-totop-safearea': option.safearea}]"
:style="{'z-index':option.zIndex, 'left': left, 'right': right, 'bottom':addUnit(option.bottom), 'width':addUnit(option.width), 'border-radius':addUnit(option.radius)}"
:src="option.src"
mode="widthFix"
@click="toTopClick"
/>
</template>
<script>
export default {
props: {
// up.toTop
option: {
type: Object,
default(){
return {}
}
},
//
value: false, // vue2
modelValue: false // vue3
},
computed: {
//
left(){
return this.option.left ? this.addUnit(this.option.left) : 'auto';
},
// ()
right() {
return this.option.left ? 'auto' : this.addUnit(this.option.right);
},
//
isShow(){
// #ifdef VUE3
return this.modelValue
// #endif
// #ifdef VUE2
return this.value
// #endif
}
},
methods: {
addUnit(num){
if(!num) return 0;
if(typeof num === 'number') return num + 'rpx';
return num
},
toTopClick() {
// #ifdef VUE3
this.$emit("update:modelValue", false); // 使v-model vue3
// #endif
// #ifdef VUE2
this.$emit('input', false); // 使v-model vue2
// #endif
this.$emit('click'); //
}
}
};
</script>
<style>
/* 回到顶部的按钮 */
.mescroll-totop {
z-index: 9990;
position: fixed !important; /* 加上important避免编译到H5,在多mescroll中定位失效 */
right: 20rpx;
bottom: 120rpx;
width: 72rpx;
height: auto;
border-radius: 50%;
opacity: 0;
transition: opacity 0.5s; /* 过渡 */
margin-bottom: var(--window-bottom); /* css变量 */
}
/* 适配 iPhoneX */
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
.mescroll-totop-safearea {
margin-bottom: calc(var(--window-bottom) + constant(safe-area-inset-bottom)); /* window-bottom + 适配 iPhoneX */
margin-bottom: calc(var(--window-bottom) + env(safe-area-inset-bottom));
}
}
/* 显示 -- 淡入 */
.mescroll-totop-in {
opacity: 1;
}
/* 隐藏 -- 淡出且不接收事件*/
.mescroll-totop-out {
opacity: 0;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,47 @@
/* 上拉加载区域 */
.mescroll-upwarp {
box-sizing: border-box;
min-height: 110rpx;
padding: 30rpx 0;
text-align: center;
clear: both;
}
/*提示文本 */
.mescroll-upwarp .upwarp-tip,
.mescroll-upwarp .upwarp-nodata {
display: inline-block;
font-size: 28rpx;
vertical-align: middle;
/* color: gray; 已在style设置color,此处删去*/
}
.mescroll-upwarp .upwarp-tip {
margin-left: 16rpx;
}
/*旋转进度条 */
.mescroll-upwarp .upwarp-progress {
display: inline-block;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid gray;
border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
vertical-align: middle;
}
/* 旋转动画 */
.mescroll-upwarp .mescroll-rotate {
animation: mescrollUpRotate 0.6s linear infinite;
}
@keyframes mescrollUpRotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,39 @@
<!-- 上拉加载区域 -->
<template>
<view class="mescroll-upwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-show="isUpLoading">
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mOption.textColor}"></view>
<view class="upwarp-tip">{{ mOption.textLoading }}</view>
</view>
<!-- 无数据 -->
<view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view>
</view>
</template>
<script>
export default {
props: {
option: Object, // up
type: Number // 0loading1loading2
},
computed: {
// ,propdefault
mOption() {
return this.option || {};
},
//
isUpLoading() {
return this.type === 1;
},
//
isUpNoMore() {
return this.type === 2;
}
}
};
</script>
<style>
@import './mescroll-up.css';
</style>

View File

@ -0,0 +1,15 @@
// 国际化工具类
const mescrollI18n = {
// 默认语言
def: "zh",
// 获取当前语言类型
getType(){
return uni.getStorageSync("mescroll-i18n") || this.def
},
// 设置当前语言类型
setType(type){
uni.setStorageSync("mescroll-i18n", type)
}
}
export default mescrollI18n

View File

@ -0,0 +1,46 @@
// mescroll-body 和 mescroll-uni 通用
const MescrollMixin = {
data() {
return {
mescroll: null //mescroll实例对象
}
},
// 注册系统自带的下拉刷新 (配置down.native为true时生效, 还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
onPullDownRefresh(){
this.mescroll && this.mescroll.onPullDownRefresh();
},
// 注册列表滚动事件,用于判定在顶部可下拉刷新,在指定位置可显示隐藏回到顶部按钮 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
onPageScroll(e) {
this.mescroll && this.mescroll.onPageScroll(e);
},
// 注册滚动到底部的事件,用于上拉加载 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
onReachBottom() {
this.mescroll && this.mescroll.onReachBottom();
},
methods: {
// mescroll组件初始化的回调,可获取到mescroll对象
mescrollInit(mescroll) {
this.mescroll = mescroll;
},
// 下拉刷新的回调 (mixin默认resetUpScroll)
downCallback() {
if(this.mescroll.optUp.use){
this.mescroll.resetUpScroll()
}else{
setTimeout(()=>{
this.mescroll.endSuccess();
}, 500)
}
},
// 上拉加载的回调
upCallback() {
// mixin默认延时500自动结束加载
setTimeout(()=>{
this.mescroll.endErr();
}, 500)
}
}
}
export default MescrollMixin;

View File

@ -0,0 +1,64 @@
// 全局配置
// mescroll-body 和 mescroll-uni 通用
const GlobalOption = {
down: {
// 其他down的配置参数也可以写,这里只展示了常用的配置:
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
},
up: {
// 其他up的配置参数也可以写,这里只展示了常用的配置:
offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
toTop: {
// 回到顶部按钮,需配置src才显示
src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
},
empty: {
use: true, // 是否显示空布局
icon: "https://www.mescroll.com/img/mescroll-empty.png" // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
}
},
// 国际化配置
i18n: {
// 中文
zh: {
down: {
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
textLoading: '加载中 ...', // 加载中的提示文本
textSuccess: '加载成功', // 加载成功的文本
textErr: '加载失败', // 加载失败的文本
},
up: {
textLoading: '加载中 ...', // 加载中的提示文本
textNoMore: '-- END --', // 没有更多数据的提示文本
empty: {
tip: '~ 空空如也 ~' // 空提示
}
}
},
// 英文
en: {
down: {
textInOffset: 'drop down refresh',
textOutOffset: 'release updates',
textLoading: 'loading ...',
textSuccess: 'loaded successfully',
textErr: 'loading failed'
},
up: {
textLoading: 'loading ...',
textNoMore: '-- END --',
empty: {
tip: '~ absolutely empty ~'
}
}
}
}
}
export default GlobalOption

View File

@ -0,0 +1,36 @@
.mescroll-uni-warp{
height: 100%;
}
.mescroll-uni-content{
height: 100%;
}
.mescroll-uni {
position: relative;
width: 100%;
height: 100%;
min-height: 200rpx;
overflow-y: auto;
box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
}
/* 定位的方式固定高度 */
.mescroll-uni-fixed{
z-index: 1;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: auto; /* 使right生效 */
height: auto; /* 使bottom生效 */
}
/* 适配 iPhoneX */
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
.mescroll-safearea {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}

View File

@ -0,0 +1,799 @@
/* mescroll
* version 1.3.7
* 2021-04-12 wenju
* https://www.mescroll.com
*/
export default function MeScroll(options, isScrollBody) {
let me = this;
me.version = '1.3.7'; // mescroll版本号
me.options = options || {}; // 配置
me.isScrollBody = isScrollBody || false; // 滚动区域是否为原生页面滚动; 默认为scroll-view
me.isDownScrolling = false; // 是否在执行下拉刷新的回调
me.isUpScrolling = false; // 是否在执行上拉加载的回调
let hasDownCallback = me.options.down && me.options.down.callback; // 是否配置了down的callback
// 初始化下拉刷新
me.initDownScroll();
// 初始化上拉加载,则初始化
me.initUpScroll();
// 自动加载
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
// 自动触发下拉刷新 (只有配置了down的callback才自动触发下拉刷新)
if ((me.optDown.use || me.optDown.native) && me.optDown.auto && hasDownCallback) {
if (me.optDown.autoShowLoading) {
me.triggerDownScroll(); // 显示下拉进度,执行下拉回调
} else {
me.optDown.callback && me.optDown.callback(me); // 不显示下拉进度,直接执行下拉回调
}
}
// 自动触发上拉加载
if(!me.isUpAutoLoad){ // 部分小程序(头条小程序)emit是异步, 会导致isUpAutoLoad判断有误, 先延时确保先执行down的callback,再执行up的callback
setTimeout(function(){
me.optUp.use && me.optUp.auto && !me.isUpAutoLoad && me.triggerUpScroll();
},100)
}
}, 30); // 需让me.optDown.inited和me.optUp.inited先执行
}
/* 配置参数:下拉刷新 */
MeScroll.prototype.extendDownScroll = function(optDown) {
// 下拉刷新的配置
MeScroll.extend(optDown, {
use: true, // 是否启用下拉刷新; 默认true
auto: true, // 是否在初始化完毕之后自动执行下拉刷新的回调; 默认true
native: false, // 是否使用系统自带的下拉刷新; 默认false; 仅mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
autoShowLoading: false, // 如果设置auto=true(在初始化完毕之后自动执行下拉刷新的回调),那么是否显示下拉刷新的进度; 默认false
isLock: false, // 是否锁定下拉刷新,默认false;
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
startTop: 100, // scroll-view快速滚动到顶部时,此时的scroll-top可能大于0, 此值用于控制最大的误差
inOffsetRate: 1, // 在列表顶部,下拉的距离小于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
outOffsetRate: 0.2, // 在列表顶部,下拉的距离大于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
bottomOffset: 20, // 当手指touchmove位置在距离body底部20px范围内的时候结束上拉刷新,避免Webview嵌套导致touchend事件不执行
minAngle: 45, // 向下滑动最少偏移的角度,取值区间 [0,90];默认45度,即向下滑动的角度大于45度则触发下拉;而小于45度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
textLoading: '加载中 ...', // 加载中的提示文本
textSuccess: '加载成功', // 加载成功的文本
textErr: '加载失败', // 加载失败的文本
beforeEndDelay: 0, // 延时结束的时长 (显示加载成功/失败的时长, android小程序设置此项结束下拉会卡顿, 配置后请注意测试)
bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorTop)
textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
inited: null, // 下拉刷新初始化完毕的回调
inOffset: null, // 下拉的距离进入offset范围内那一刻的回调
outOffset: null, // 下拉的距离大于offset那一刻的回调
onMoving: null, // 下拉过程中的回调,滑动过程一直在执行; rate下拉区域当前高度与指定距离的比值(inOffset: rate<1; outOffset: rate>=1); downHight当前下拉区域的高度
beforeLoading: null, // 准备触发下拉刷新的回调: 如果return true,将不触发showLoading和callback回调; 常用来完全自定义下拉刷新, 参考案例【淘宝 v6.8.0】
showLoading: null, // 显示下拉刷新进度的回调
afterLoading: null, // 显示下拉刷新进度的回调之后,马上要执行的代码 (如: 在wxs中使用)
beforeEndDownScroll: null, // 准备结束下拉的回调. 返回结束下拉的延时执行时间,默认0ms; 常用于结束下拉之前再显示另外一小段动画,才去隐藏下拉刷新的场景, 参考案例【dotJump】
endDownScroll: null, // 结束下拉刷新的回调
afterEndDownScroll: null, // 结束下拉刷新的回调,马上要执行的代码 (如: 在wxs中使用)
callback: function(mescroll) {
// 下拉刷新的回调;默认重置上拉加载列表为第一页
mescroll.resetUpScroll();
}
})
}
/* 配置参数:上拉加载 */
MeScroll.prototype.extendUpScroll = function(optUp) {
// 上拉加载的配置
MeScroll.extend(optUp, {
use: true, // 是否启用上拉加载; 默认true
auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
isLock: false, // 是否锁定上拉加载,默认false;
isBoth: true, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认true,两者可同时触发;
callback: null, // 上拉加载的回调;function(page,mescroll){ }
page: {
num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
size: 10, // 每页数据的数量
time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
},
noMoreSize: 5, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
textLoading: '加载中 ...', // 加载中的提示文本
textNoMore: '-- END --', // 没有更多数据的提示文本
bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorBottom)
textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
inited: null, // 初始化完毕的回调
showLoading: null, // 显示加载中的回调
showNoMore: null, // 显示无更多数据的回调
hideUpScroll: null, // 隐藏上拉加载的回调
errDistance: 60, // endErr的时候需往上滑动一段距离,使其往下滑动时再次触发onReachBottom,仅mescroll-body生效
toTop: {
// 回到顶部按钮,需配置src才显示
src: null, // 图片路径,默认null (绝对路径或网络图)
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
duration: 300, // 回到顶部的动画时长,默认300ms (当值为0或300则使用系统自带回到顶部,更流畅; 其他值则通过step模拟,部分机型可能不够流畅,所以非特殊情况不建议修改此项)
btnClick: null, // 点击按钮的回调
onShow: null, // 是否显示的回调
zIndex: 9990, // fixed定位z-index值
left: null, // 到左边的距离, 默认null. 此项有值时,right不生效. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
right: 20, // 到右边的距离, 默认20 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
bottom: 120, // 到底部的距离, 默认120 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
safearea: false, // bottom的偏移量是否加上底部安全区的距离, 默认false, 需要适配iPhoneX时使用 (具体的界面如果不配置此项,则取本vue的safearea值)
width: 72, // 回到顶部图标的宽度, 默认72 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
radius: "50%" // 圆角, 默认"50%" (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
},
empty: {
use: true, // 是否显示空布局
icon: null, // 图标路径
tip: '~ 暂无相关数据 ~', // 提示
btnText: '', // 按钮
btnClick: null, // 点击按钮的回调
onShow: null, // 是否显示的回调
fixed: false, // 是否使用fixed定位,默认false; 配置fixed为true,以下的top和zIndex才生效 (transform会使fixed失效,最终会降级为absolute)
top: "100rpx", // fixed定位的top值 (完整的单位值,如 "10%"; "100rpx")
zIndex: 99 // fixed定位z-index值
},
onScroll: false // 是否监听滚动事件
})
}
/* 配置参数 */
MeScroll.extend = function(userOption, defaultOption) {
if (!userOption) return defaultOption;
for (let key in defaultOption) {
if (userOption[key] == null) {
let def = defaultOption[key];
if (def != null && typeof def === 'object') {
userOption[key] = MeScroll.extend({}, def); // 深度匹配
} else {
userOption[key] = def;
}
} else if (typeof userOption[key] === 'object') {
MeScroll.extend(userOption[key], defaultOption[key]); // 深度匹配
}
}
return userOption;
}
/* 简单判断是否配置了颜色 (非透明,非白色) */
MeScroll.prototype.hasColor = function(color) {
if(!color) return false;
let c = color.toLowerCase();
return c != "#fff" && c != "#ffffff" && c != "transparent" && c != "white"
}
/* -------初始化下拉刷新------- */
MeScroll.prototype.initDownScroll = function() {
let me = this;
// 配置参数
me.optDown = me.options.down || {};
if(!me.optDown.textColor && me.hasColor(me.optDown.bgColor)) me.optDown.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
me.extendDownScroll(me.optDown);
// 如果是mescroll-body且配置了native,则禁止自定义的下拉刷新
if(me.isScrollBody && me.optDown.native){
me.optDown.use = false
}else{
me.optDown.native = false // 仅mescroll-body支持,mescroll-uni不支持
}
me.downHight = 0; // 下拉区域的高度
// 在页面中加入下拉布局
if (me.optDown.use && me.optDown.inited) {
// 初始化完毕的回调
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
me.optDown.inited(me);
}, 0)
}
}
/* 列表touchstart事件 */
MeScroll.prototype.touchstartEvent = function(e) {
if (!this.optDown.use) return;
this.startPoint = this.getPoint(e); // 记录起点
this.startTop = this.getScrollTop(); // 记录此时的滚动条位置
this.startAngle = 0; // 初始角度
this.lastPoint = this.startPoint; // 重置上次move的点
this.maxTouchmoveY = this.getBodyHeight() - this.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
this.inTouchend = false; // 标记不是touchend
}
/* 列表touchmove事件 */
MeScroll.prototype.touchmoveEvent = function(e) {
if (!this.optDown.use) return;
let me = this;
let scrollTop = me.getScrollTop(); // 当前滚动条的距离
let curPoint = me.getPoint(e); // 当前点
let moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
// 向下拉 && 在顶部
// mescroll-body,直接判定在顶部即可
// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
if (moveY > 0 && (
(me.isScrollBody && scrollTop <= 0)
||
(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
)) {
// 可下拉的条件
if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
me.optUp.isBoth))) {
// 下拉的初始角度是否在配置的范围内
if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
if (me.startAngle < me.optDown.minAngle) return; // 如果小于配置的角度,则不往下执行下拉刷新
// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
me.inTouchend = true; // 标记执行touchend
me.touchendEvent(); // 提前触发touchend
return;
}
me.preventDefault(e); // 阻止默认事件
let diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
// 下拉距离 < 指定距离
if (me.downHight < me.optDown.offset) {
if (me.movetype !== 1) {
me.movetype = 1; // 加入标记,保证只执行一次
me.isDownEndSuccess = null; // 重置是否加载成功的状态 (wxs执行的是wxs.wxs)
me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
}
me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
// 指定距离 <= 下拉距离
} else {
if (me.movetype !== 2) {
me.movetype = 2; // 加入标记,保证只执行一次
me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
}
if (diff > 0) { // 向下拉
me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
} else { // 向上收
me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
}
}
me.downHight = Math.round(me.downHight) // 取整
let rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
}
}
me.lastPoint = curPoint; // 记录本次移动的点
}
/* 列表touchend事件 */
MeScroll.prototype.touchendEvent = function(e) {
if (!this.optDown.use) return;
// 如果下拉区域高度已改变,则需重置回来
if (this.isMoveDown) {
if (this.downHight >= this.optDown.offset) {
// 符合触发刷新的条件
this.triggerDownScroll();
} else {
// 不符合的话 则重置
this.downHight = 0;
this.endDownScrollCall(this);
}
this.movetype = 0;
this.isMoveDown = false;
} else if (!this.isScrollBody && this.getScrollTop() === this.startTop) { // scroll-view到顶/左/右/底的滑动事件
let isScrollUp = this.getPoint(e).y - this.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
// 上滑
if (isScrollUp) {
// 需检查滑动的角度
let angle = this.getAngle(this.getPoint(e), this.startPoint); // 两点之间的角度,区间 [0,90]
if (angle > 80) {
// 检查并触发上拉
this.triggerUpScroll(true);
}
}
}
}
/* 根据点击滑动事件获取第一个手指的坐标 */
MeScroll.prototype.getPoint = function(e) {
if (!e) {
return {
x: 0,
y: 0
}
}
if (e.touches && e.touches[0]) {
return {
x: e.touches[0].pageX,
y: e.touches[0].pageY
}
} else if (e.changedTouches && e.changedTouches[0]) {
return {
x: e.changedTouches[0].pageX,
y: e.changedTouches[0].pageY
}
} else {
return {
x: e.clientX,
y: e.clientY
}
}
}
/* 计算两点之间的角度: 区间 [0,90]*/
MeScroll.prototype.getAngle = function(p1, p2) {
let x = Math.abs(p1.x - p2.x);
let y = Math.abs(p1.y - p2.y);
let z = Math.sqrt(x * x + y * y);
let angle = 0;
if (z !== 0) {
angle = Math.asin(y / z) / Math.PI * 180;
}
return angle
}
/* 触发下拉刷新 */
MeScroll.prototype.triggerDownScroll = function() {
if (this.optDown.beforeLoading && this.optDown.beforeLoading(this)) {
//return true则处于完全自定义状态
} else {
this.showDownScroll(); // 下拉刷新中...
!this.optDown.native && this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
}
}
/* 显示下拉进度布局 */
MeScroll.prototype.showDownScroll = function() {
this.isDownScrolling = true; // 标记下拉中
if (this.optDown.native) {
uni.startPullDownRefresh(); // 系统自带的下拉刷新
this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
} else{
this.downHight = this.optDown.offset; // 更新下拉区域高度
this.showDownLoadingCall(this.downHight); // 下拉刷新中...
}
}
MeScroll.prototype.showDownLoadingCall = function(downHight) {
this.optDown.showLoading && this.optDown.showLoading(this, downHight); // 下拉刷新中...
this.optDown.afterLoading && this.optDown.afterLoading(this, downHight); // 下拉刷新中...触发之后马上要执行的代码
}
/* 显示系统自带的下拉刷新时需要处理的业务 */
MeScroll.prototype.onPullDownRefresh = function() {
this.isDownScrolling = true; // 标记下拉中
this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
}
/* 结束下拉刷新 */
MeScroll.prototype.endDownScroll = function() {
if (this.optDown.native) { // 结束原生下拉刷新
this.isDownScrolling = false;
this.endDownScrollCall(this);
uni.stopPullDownRefresh();
return
}
let me = this;
// 结束下拉刷新的方法
let endScroll = function() {
me.downHight = 0;
me.isDownScrolling = false;
me.endDownScrollCall(me);
if(!me.isScrollBody){
me.setScrollHeight(0) // scroll-view重置滚动区域,使数据不满屏时仍可检查触发翻页
me.scrollTo(0,0) // scroll-view需重置滚动条到顶部,避免startTop大于0时,对下拉刷新的影响
}
}
// 结束下拉刷新时的回调
let delay = 0;
if (me.optDown.beforeEndDownScroll) {
delay = me.optDown.beforeEndDownScroll(me); // 结束下拉刷新的延时,单位ms
if(me.isDownEndSuccess == null) delay = 0; // 没有执行加载中,则不延时
}
if (typeof delay === 'number' && delay > 0) {
setTimeout(endScroll, delay);
} else {
endScroll();
}
}
MeScroll.prototype.endDownScrollCall = function() {
this.optDown.endDownScroll && this.optDown.endDownScroll(this);
this.optDown.afterEndDownScroll && this.optDown.afterEndDownScroll(this);
}
/* 锁定下拉刷新:isLock=ture,null锁定;isLock=false解锁 */
MeScroll.prototype.lockDownScroll = function(isLock) {
if (isLock == null) isLock = true;
this.optDown.isLock = isLock;
}
/* 锁定上拉加载:isLock=ture,null锁定;isLock=false解锁 */
MeScroll.prototype.lockUpScroll = function(isLock) {
if (isLock == null) isLock = true;
this.optUp.isLock = isLock;
}
/* -------初始化上拉加载------- */
MeScroll.prototype.initUpScroll = function() {
let me = this;
// 配置参数
me.optUp = me.options.up || {use: false}
if(!me.optUp.textColor && me.hasColor(me.optUp.bgColor)) me.optUp.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
me.extendUpScroll(me.optUp);
if (me.optUp.use === false) return; // 配置不使用上拉加载时,则不初始化上拉布局
me.optUp.hasNext = true; // 如果使用上拉,则默认有下一页
me.startNum = me.optUp.page.num + 1; // 记录page开始的页码
// 初始化完毕的回调
if (me.optUp.inited) {
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
me.optUp.inited(me);
}, 0)
}
}
/*滚动到底部的事件 (仅mescroll-body生效)*/
MeScroll.prototype.onReachBottom = function() {
if (this.isScrollBody && !this.isUpScrolling) { // 只能支持下拉刷新的时候同时可以触发上拉加载,否则滚动到底部就需要上滑一点才能触发onReachBottom
if (!this.optUp.isLock && this.optUp.hasNext) {
this.triggerUpScroll();
}
}
}
/*列表滚动事件 (仅mescroll-body生效)*/
MeScroll.prototype.onPageScroll = function(e) {
if (!this.isScrollBody) return;
// 更新滚动条的位置 (主要用于判断下拉刷新时,滚动条是否在顶部)
this.setScrollTop(e.scrollTop);
// 顶部按钮的显示隐藏
if (e.scrollTop >= this.optUp.toTop.offset) {
this.showTopBtn();
} else {
this.hideTopBtn();
}
}
/*列表滚动事件*/
MeScroll.prototype.scroll = function(e, onScroll) {
// 更新滚动条的位置
this.setScrollTop(e.scrollTop);
// 更新滚动内容高度
this.setScrollHeight(e.scrollHeight);
// 向上滑还是向下滑动
if (this.preScrollY == null) this.preScrollY = 0;
this.isScrollUp = e.scrollTop - this.preScrollY > 0;
this.preScrollY = e.scrollTop;
// 上滑 && 检查并触发上拉
this.isScrollUp && this.triggerUpScroll(true);
// 顶部按钮的显示隐藏
if (e.scrollTop >= this.optUp.toTop.offset) {
this.showTopBtn();
} else {
this.hideTopBtn();
}
// 滑动监听
this.optUp.onScroll && onScroll && onScroll()
}
/* 触发上拉加载 */
MeScroll.prototype.triggerUpScroll = function(isCheck) {
if (!this.isUpScrolling && this.optUp.use && this.optUp.callback) {
// 是否校验在底部; 默认不校验
if (isCheck === true) {
let canUp = false;
// 还有下一页 && 没有锁定 && 不在下拉中
if (this.optUp.hasNext && !this.optUp.isLock && !this.isDownScrolling) {
if (this.getScrollBottom() <= this.optUp.offset) { // 到底部
canUp = true; // 标记可上拉
}
}
if (canUp === false) return;
}
this.showUpScroll(); // 上拉加载中...
this.optUp.page.num++; // 预先加一页,如果失败则减回
this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
this.num = this.optUp.page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
this.size = this.optUp.page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
this.time = this.optUp.page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
this.optUp.callback(this); // 执行回调,联网加载数据
}
}
/* 显示上拉加载中 */
MeScroll.prototype.showUpScroll = function() {
this.isUpScrolling = true; // 标记上拉加载中
this.optUp.showLoading && this.optUp.showLoading(this); // 回调
}
/* 显示上拉无更多数据 */
MeScroll.prototype.showNoMore = function() {
this.optUp.hasNext = false; // 标记无更多数据
this.optUp.showNoMore && this.optUp.showNoMore(this); // 回调
}
/* 隐藏上拉区域**/
MeScroll.prototype.hideUpScroll = function() {
this.optUp.hideUpScroll && this.optUp.hideUpScroll(this); // 回调
}
/* 结束上拉加载 */
MeScroll.prototype.endUpScroll = function(isShowNoMore) {
if (isShowNoMore != null) { // isShowNoMore=null,不处理下拉状态,下拉刷新的时候调用
if (isShowNoMore) {
this.showNoMore(); // isShowNoMore=true,显示无更多数据
} else {
this.hideUpScroll(); // isShowNoMore=false,隐藏上拉加载
}
}
this.isUpScrolling = false; // 标记结束上拉加载
}
/*
*isShowLoading 是否显示进度布局;
* 1.默认null,不传参,则显示上拉加载的进度布局
* 2.传参true, 则显示下拉刷新的进度布局
* 3.传参false,则不显示上拉和下拉的进度 (常用于静默更新列表数据)
*/
MeScroll.prototype.resetUpScroll = function(isShowLoading) {
if (this.optUp && this.optUp.use) {
let page = this.optUp.page;
this.prePageNum = page.num; // 缓存重置前的页码,加载失败可退回
this.prePageTime = page.time; // 缓存重置前的时间,加载失败可退回
page.num = this.startNum; // 重置为第一页
page.time = null; // 重置时间为空
if (!this.isDownScrolling && isShowLoading !== false) { // 如果不是下拉刷新触发的resetUpScroll并且不配置列表静默更新,则显示进度;
if (isShowLoading == null) {
this.removeEmpty(); // 移除空布局
this.showUpScroll(); // 不传参,默认显示上拉加载的进度布局
} else {
this.showDownScroll(); // 传true,显示下拉刷新的进度布局,不清空列表
}
}
this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
this.num = page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
this.size = page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
this.time = page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
this.optUp.callback && this.optUp.callback(this); // 执行上拉回调
}
}
/* 设置page.num的值 */
MeScroll.prototype.setPageNum = function(num) {
this.optUp.page.num = num - 1;
}
/* 设置page.size的值 */
MeScroll.prototype.setPageSize = function(size) {
this.optUp.page.size = size;
}
/* ,
* dataSize: 当前页的数据量(必传)
* totalPage: 总页数(必传)
* systime: 服务器时间 (可空)
*/
MeScroll.prototype.endByPage = function(dataSize, totalPage, systime) {
let hasNext;
if (this.optUp.use && totalPage != null) hasNext = this.optUp.page.num < totalPage; // 是否还有下一页
this.endSuccess(dataSize, hasNext, systime);
}
/* ,
* dataSize: 当前页的数据量(必传)
* totalSize: 列表所有数据总数量(必传)
* systime: 服务器时间 (可空)
*/
MeScroll.prototype.endBySize = function(dataSize, totalSize, systime) {
let hasNext;
if (this.optUp.use && totalSize != null) {
let loadSize = (this.optUp.page.num - 1) * this.optUp.page.size + dataSize; // 已加载的数据总数
hasNext = loadSize < totalSize; // 是否还有下一页
}
this.endSuccess(dataSize, hasNext, systime);
}
/* ,
* dataSize: 当前页的数据个数(不是所有页的数据总和),用于上拉加载判断是否还有下一页.如果不传,则会判断还有下一页
* hasNext: 是否还有下一页,布尔类型;用来解决这个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据dataSize判断,则需翻到第三页才会知道无更多数据,如果传了hasNext,则翻到第二页即可显示无更多数据.
* systime: 服务器时间(可空);用来解决这个小问题:当准备翻下一页时,数据库新增了几条记录,此时翻下一页,前面的几条数据会和上一页的重复;这里传入了systime,那么upCallback的page.time就会有值,把page.time传给服务器,让后台过滤新加入的那几条记录
*/
MeScroll.prototype.endSuccess = function(dataSize, hasNext, systime) {
let me = this;
// 结束下拉刷新
if (me.isDownScrolling) {
me.isDownEndSuccess = true
me.endDownScroll();
}
// 结束上拉加载
if (me.optUp.use) {
let isShowNoMore; // 是否已无更多数据
if (dataSize != null) {
let pageNum = me.optUp.page.num; // 当前页码
let pageSize = me.optUp.page.size; // 每页长度
// 如果是第一页
if (pageNum === 1) {
if (systime) me.optUp.page.time = systime; // 设置加载列表数据第一页的时间
}
if (dataSize < pageSize || hasNext === false) {
// 返回的数据不满一页时,则说明已无更多数据
me.optUp.hasNext = false;
if (dataSize === 0 && pageNum === 1) {
// 如果第一页无任何数据且配置了空布局
isShowNoMore = false;
me.showEmpty();
} else {
// 总列表数少于配置的数量,则不显示无更多数据
let allDataSize = (pageNum - 1) * pageSize + dataSize;
if (allDataSize < me.optUp.noMoreSize) {
isShowNoMore = false;
} else {
isShowNoMore = true;
}
me.removeEmpty(); // 移除空布局
}
} else {
// 还有下一页
isShowNoMore = false;
me.optUp.hasNext = true;
me.removeEmpty(); // 移除空布局
}
}
// 隐藏上拉
me.endUpScroll(isShowNoMore);
}
}
/* 回调失败,结束下拉刷新和上拉加载 */
MeScroll.prototype.endErr = function(errDistance) {
// 结束下拉,回调失败重置回原来的页码和时间
if (this.isDownScrolling) {
this.isDownEndSuccess = false
let page = this.optUp.page;
if (page && this.prePageNum) {
page.num = this.prePageNum;
page.time = this.prePageTime;
}
this.endDownScroll();
}
// 结束上拉,回调失败重置回原来的页码
if (this.isUpScrolling) {
this.optUp.page.num--;
this.endUpScroll(false);
// 如果是mescroll-body,则需往回滚一定距离
if(this.isScrollBody && errDistance !== 0){ // 不处理0
if(!errDistance) errDistance = this.optUp.errDistance; // 不传,则取默认
this.scrollTo(this.getScrollTop() - errDistance, 0) // 往上回滚的距离
}
}
}
/* 显示空布局 */
MeScroll.prototype.showEmpty = function() {
this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(true)
}
/* 移除空布局 */
MeScroll.prototype.removeEmpty = function() {
this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(false)
}
/* 显示回到顶部的按钮 */
MeScroll.prototype.showTopBtn = function() {
if (!this.topBtnShow) {
this.topBtnShow = true;
this.optUp.toTop.onShow && this.optUp.toTop.onShow(true);
}
}
/* 隐藏回到顶部的按钮 */
MeScroll.prototype.hideTopBtn = function() {
if (this.topBtnShow) {
this.topBtnShow = false;
this.optUp.toTop.onShow && this.optUp.toTop.onShow(false);
}
}
/* 获取滚动条的位置 */
MeScroll.prototype.getScrollTop = function() {
return this.scrollTop || 0
}
/* 记录滚动条的位置 */
MeScroll.prototype.setScrollTop = function(y) {
this.scrollTop = y;
}
/* 滚动到指定位置 */
MeScroll.prototype.scrollTo = function(y, t) {
this.myScrollTo && this.myScrollTo(y, t) // scrollview需自定义回到顶部方法
}
/* 自定义scrollTo */
MeScroll.prototype.resetScrollTo = function(myScrollTo) {
this.myScrollTo = myScrollTo
}
/* 滚动条到底部的距离 */
MeScroll.prototype.getScrollBottom = function() {
return this.getScrollHeight() - this.getClientHeight() - this.getScrollTop()
}
/*
star: 开始值
end: 结束值
callback(step,timer): 回调step值,计步器timer,可自行通过window.clearInterval(timer)结束计步器;
t: 计步时长,传0则直接回调end值;不传则默认300ms
rate: 周期;不传则默认30ms计步一次
* */
MeScroll.prototype.getStep = function(star, end, callback, t, rate) {
let diff = end - star; // 差值
if (t === 0 || diff === 0) {
callback && callback(end);
return;
}
t = t || 300; // 时长 300ms
rate = rate || 30; // 周期 30ms
let count = t / rate; // 次数
let step = diff / count; // 步长
let i = 0; // 计数
let timer = setInterval(function() {
if (i < count - 1) {
star += step;
callback && callback(star, timer);
i++;
} else {
callback && callback(end, timer); // 最后一次直接设置end,避免计算误差
clearInterval(timer);
}
}, rate);
}
/* 滚动容器的高度 */
MeScroll.prototype.getClientHeight = function(isReal) {
let h = this.clientHeight || 0
if (h === 0 && isReal !== true) { // 未获取到容器的高度,可临时取body的高度 (可能会有误差)
h = this.getBodyHeight()
}
return h
}
MeScroll.prototype.setClientHeight = function(h) {
this.clientHeight = h;
}
/* 滚动内容的高度 */
MeScroll.prototype.getScrollHeight = function() {
return this.scrollHeight || 0;
}
MeScroll.prototype.setScrollHeight = function(h) {
this.scrollHeight = h;
}
/* body的高度 */
MeScroll.prototype.getBodyHeight = function() {
return this.bodyHeight || 0;
}
MeScroll.prototype.setBodyHeight = function(h) {
this.bodyHeight = h;
}
/* 阻止浏览器默认滚动事件 */
MeScroll.prototype.preventDefault = function(e) {
// 小程序不支持e.preventDefault, 已在wxs中禁止
// app的bounce只能通过配置pages.json的style.app-plus.bounce为"none"来禁止, 或使用renderjs禁止
// cancelable:是否可以被禁用; defaultPrevented:是否已经被禁用
if (e && e.cancelable && !e.defaultPrevented) e.preventDefault()
}

View File

@ -0,0 +1,480 @@
<template>
<view class="mescroll-uni-warp">
<scroll-view :id="viewId" class="mescroll-uni" :class="{'mescroll-uni-fixed':isFixed}" :style="{'height':scrollHeight,'padding-top':padTop,'padding-bottom':padBottom,'top':fixedTop,'bottom':fixedBottom}" :scroll-top="scrollTop" :scroll-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false">
<view class="mescroll-uni-content mescroll-render-touch"
@touchstart="wxsBiz.touchstartEvent"
@touchmove="wxsBiz.touchmoveEvent"
@touchend="wxsBiz.touchendEvent"
@touchcancel="wxsBiz.touchendEvent"
:change:prop="wxsBiz.propObserver"
:prop="wxsProp">
<!-- 状态栏 -->
<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
<view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': transition}" :change:prop="wxsBiz.callObserver" :prop="callProp">
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
<view class="downwarp-content">
<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
<view class="downwarp-tip">{{downText}}</view>
</view>
</view>
<!-- 列表内容 -->
<slot></slot>
<!-- 空布局 -->
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-show="upLoadType===1">
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
</view>
<!-- 无数据 -->
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
</view>
</view>
<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
<!-- #ifdef H5 -->
<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
<!-- #endif -->
<!-- 适配iPhoneX -->
<view v-if="safearea" class="mescroll-safearea"></view>
</view>
</scroll-view>
<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
<!-- #endif -->
</view>
</template>
<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
<!-- #endif -->
<!-- app, h5使用renderjs -->
<!-- #ifdef APP-PLUS || H5 -->
<script module="renderBiz" lang="renderjs">
import renderBiz from './wxs/renderjs.js';
export default {
mixins:[renderBiz]
}
</script>
<!-- #endif -->
<script>
// mescroll-uni.js,
import MeScroll from './mescroll-uni.js';
//
import GlobalOption from './mescroll-uni-option.js';
//
import mescrollI18n from './mescroll-i18n.js';
//
import MescrollTop from './components/mescroll-top.vue';
// wxs(renderjs)mixins
import WxsMixin from './wxs/mixins.js';
/**
* mescroll-uni 嵌在页面某个区域的下拉刷新和上拉加载组件, 如嵌在弹窗,浮层,swiper中...
* @property {Object} down 下拉刷新的参数配置
* @property {Object} up 上拉加载的参数配置
* @property {Object} i18n 国际化的参数配置
* @property {String, Number} top 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
* @property {Boolean, String} topbar 偏移量top是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
* @property {String, Number} bottom 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
* @property {Boolean} safearea 偏移量bottom是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
* @property {Boolean} fixed 是否通过fixed固定mescroll的高度, 默认true
* @property {String, Number} height 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
* @property {Boolean} bottombar 底部是否偏移TabBar的高度 (仅在H5端的tab页生效)
* @property {Boolean} disableScroll 是否禁止滚动, 默认false
* @event {Function} init 初始化完成的回调
* @event {Function} down 下拉刷新的回调
* @event {Function} up 上拉加载的回调
* @event {Function} emptyclick 点击empty配置的btnText按钮回调
* @event {Function} topclick 点击回到顶部的按钮回调
* @event {Function} scroll 滚动监听 (需在 up 配置 onScroll:true 才生效)
* @example <mescroll-uni @init="mescrollInit" @down="downCallback" @up="upCallback"> ... </mescroll-uni>
*/
export default {
name: 'mescroll-uni',
mixins: [WxsMixin],
components: {
MescrollTop
},
props: {
down: Object,
up: Object,
i18n: Object,
top: [String, Number],
topbar: [Boolean, String],
bottom: [String, Number],
safearea: Boolean,
fixed: {
type: Boolean,
default: true
},
height: [String, Number],
bottombar:{
type: Boolean,
default: true
},
disableScroll: Boolean
},
data() {
return {
mescroll: {optDown:{},optUp:{}}, // mescroll
viewId: 'id_' + Math.random().toString(36).substr(2,16), // mescrollid(,)
downHight: 0, //:
downRate: 0, // (inOffset: rate<1; outOffset: rate>=1)
downLoadType: 0, // : 0(loading), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
upLoadType: 0, // : 0(loading), 1loading, 2,END, 3(,END)
isShowEmpty: false, //
isShowToTop: false, //
scrollTop: 0, //
scrollAnim: false, //
windowTop: 0, // 使
windowBottom: 0, // 使
windowHeight: 0, // 使
statusBarHeight: 0 //
}
},
watch: {
height() {
//
this.setClientHeight()
}
},
computed: {
// 使fixed (height,使)
isFixed(){
return !this.height && this.fixed
},
// mescroll
scrollHeight(){
if (this.isFixed) {
return "auto"
} else if(this.height){
return this.toPx(this.height) + 'px'
}else{
return "100%"
}
},
// (px)
numTop() {
return this.toPx(this.top)
},
fixedTop() {
return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
},
padTop() {
return !this.isFixed ? this.numTop + 'px' : 0
},
// (px)
numBottom() {
return this.toPx(this.bottom)
},
fixedBottom() {
return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
},
padBottom() {
return !this.isFixed ? this.numBottom + 'px' : 0
},
//
isDownReset(){
return this.downLoadType===3 || this.downLoadType===4
},
//
transition() {
return this.isDownReset ? 'transform 300ms' : '';
},
translateY() {
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform使fixed,fixedmescroll
},
//
scrollable(){
if(this.disableScroll) return false
return this.downLoadType===0 || this.isDownReset
},
//
isDownLoading(){
return this.downLoadType === 3
},
//
downRotate(){
return 'rotate(' + 360 * this.downRate + 'deg)'
},
//
downText(){
if(!this.mescroll) return ""; //
switch (this.downLoadType){
case 1: return this.mescroll.optDown.textInOffset;
case 2: return this.mescroll.optDown.textOutOffset;
case 3: return this.mescroll.optDown.textLoading;
case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
default: return this.mescroll.optDown.textInOffset;
}
}
},
methods: {
//number,rpx,upx,px,% --> px
toPx(num){
if(typeof num === "string"){
if (num.indexOf('px') !== -1) {
if(num.indexOf('rpx') !== -1) { // "10rpx"
num = num.replace('rpx', '');
} else if(num.indexOf('upx') !== -1) { // "10upx"
num = num.replace('upx', '');
} else { // "10px"
return Number(num.replace('px', ''))
}
}else if (num.indexOf('%') !== -1){
// ,windowHeight,"10%"windowHeight10%
let rate = Number(num.replace("%","")) / 100
return this.windowHeight * rate
}
}
return num ? uni.upx2px(Number(num)) : 0
},
//,
scroll(e) {
this.mescroll.scroll(e.detail, () => {
this.$emit('scroll', this.mescroll) // this.mescroll.scrollTop; this.mescroll.isScrollUp
})
},
//
emptyClick() {
this.$emit('emptyclick', this.mescroll)
},
//
toTopClick() {
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); //
this.$emit('topclick', this.mescroll); //
},
// (使,)
setClientHeight() {
if (!this.isExec) {
this.isExec = true; //
this.$nextTick(() => { // dom
this.getClientInfo(data=>{
this.isExec = false;
if (data) {
this.mescroll.setClientHeight(data.height);
} else if (this.clientNum != 3) { // ,dom,,3
this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
setTimeout(() => {
this.setClientHeight()
}, this.clientNum * 100)
}
})
})
}
},
//
getClientInfo(success){
let query = uni.createSelectorQuery().in(this);
let view = query.select('#' + this.viewId);
view.boundingClientRect(data => {
success(data)
}).exec();
}
},
// 使createdmescroll; mountedcssH5
created() {
let vm = this;
let diyOption = {
//
down: {
inOffset() {
vm.downLoadType = 1; // offset (mescroll,)
},
outOffset() {
vm.downLoadType = 2; // offset (mescroll,)
},
onMoving(mescroll, rate, downHight) {
// ,;
vm.downHight = downHight; // (mescroll,)
vm.downRate = rate; // (inOffset: rate<1; outOffset: rate>=1)
},
showLoading(mescroll, downHight) {
vm.downLoadType = 3; // (mescroll,)
vm.downHight = downHight; // (mescroll,)
},
beforeEndDownScroll(mescroll){
vm.downLoadType = 4;
return mescroll.optDown.beforeEndDelay //
},
endDownScroll() {
vm.downLoadType = 4; // (mescroll,)
vm.downHight = 0; // (mescroll,)
vm.downResetTimer && clearTimeout(vm.downResetTimer)
vm.downResetTimer = setTimeout(()=>{ // ,0,便this.transition,iOS
if(vm.downLoadType===4) vm.downLoadType = 0
},300)
},
//
callback: function(mescroll) {
vm.$emit('down', mescroll)
}
},
//
up: {
//
showLoading() {
vm.upLoadType = 1;
},
//
showNoMore() {
vm.upLoadType = 2;
},
//
hideUpScroll(mescroll) {
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
},
//
empty: {
onShow(isShow) { //
vm.isShowEmpty = isShow;
}
},
//
toTop: {
onShow(isShow) { //
vm.isShowToTop = isShow;
}
},
//
callback: function(mescroll) {
vm.$emit('up', mescroll);
// (mescroll)
vm.setClientHeight()
}
}
}
let i18nType = mescrollI18n.getType() //
let i18nOption = {type: i18nType} //
MeScroll.extend(i18nOption, vm.i18n) //
MeScroll.extend(i18nOption, GlobalOption.i18n) //
MeScroll.extend(diyOption, i18nOption[i18nType]); //
MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); //
let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // ,props
MeScroll.extend(myOption, diyOption); //
// MeScroll
vm.mescroll = new MeScroll(myOption);
vm.mescroll.viewId = vm.viewId; // id
vm.mescroll.i18n = i18nOption; //
// initmescroll
vm.$emit('init', vm.mescroll);
//
const sys = uni.getSystemInfoSync();
if(sys.windowTop) vm.windowTop = sys.windowTop;
if(sys.windowBottom) vm.windowBottom = sys.windowBottom;
if(sys.windowHeight) vm.windowHeight = sys.windowHeight;
if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
// 使downbottomOffset
vm.mescroll.setBodyHeight(sys.windowHeight);
// 使scrollview,scrollTo
vm.mescroll.resetScrollTo((y, t) => {
vm.scrollAnim = (t !== 0); // t0,使
if(typeof y === 'string'){
// slotscroll-into-view, 使
vm.getClientInfo(function(rect){
let mescrollTop = rect.top // mescroll
let selector;
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
selector = '#'+y // #. id
}else{
selector = y
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
if(y.indexOf('>>>')!=-1){ // ()
selector = y.split('>>>')[1].trim()
}
// #endif
}
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
if (rect) {
let curY = vm.mescroll.getScrollTop()
let top = rect.top - mescrollTop
top += curY
if(!vm.isFixed) top -= vm.numTop
vm.scrollTop = curY;
vm.$nextTick(function() {
vm.scrollTop = top
})
} else{
console.error(selector + ' does not exist');
}
}).exec()
})
return;
}
let curY = vm.mescroll.getScrollTop()
if (t === 0 || t === 300) { // t使300,使
vm.scrollTop = curY;
vm.$nextTick(function() {
vm.scrollTop = y
})
} else {
vm.mescroll.getStep(curY, y, step => { // t
vm.scrollTop = step
}, t)
}
})
// up.toTop.safearea,vuesafearea
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
vm.mescroll.optUp.toTop.safearea = vm.safearea;
}
//
uni.$on("setMescrollGlobalOption", options=>{
if(!options) return;
let i18nType = options.i18n ? options.i18n.type : null
if(i18nType && vm.mescroll.i18n.type != i18nType){
vm.mescroll.i18n.type = i18nType
mescrollI18n.setType(i18nType)
MeScroll.extend(options, vm.mescroll.i18n[i18nType])
}
if(options.down){
let down = MeScroll.extend({}, options.down)
vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
}
if(options.up){
let up = MeScroll.extend({}, options.up)
vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
}
})
},
mounted() {
//
this.setClientHeight()
},
destroyed() {
//
uni.$off("setMescrollGlobalOption")
}
}
</script>
<style>
@import "./mescroll-uni.css";
@import "./components/mescroll-down.css";
@import './components/mescroll-up.css';
</style>

View File

@ -0,0 +1,47 @@
/**
* mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期
*/
const MescrollCompMixin = {
// 因为子组件无onPageScroll和onReachBottom的页面生命周期需在页面传递进到子组件 (一级)
onPageScroll(e) {
this.handlePageScroll(e)
},
onReachBottom() {
this.handleReachBottom()
},
// 当down的native: true时, 还需传递此方法进到子组件
onPullDownRefresh(){
this.handlePullDownRefresh()
},
data() {
return {
mescroll: { // mescroll-body写在子子子...组件的情况 (多级)
onPageScroll: e=>{
this.handlePageScroll(e)
},
onReachBottom: ()=>{
this.handleReachBottom()
},
onPullDownRefresh: ()=>{
this.handlePullDownRefresh()
}
}
}
},
methods:{
handlePageScroll(e){
let item = this.$refs["mescrollItem"];
if(item && item.mescroll) item.mescroll.onPageScroll(e);
},
handleReachBottom(){
let item = this.$refs["mescrollItem"];
if(item && item.mescroll) item.mescroll.onReachBottom();
},
handlePullDownRefresh(){
let item = this.$refs["mescrollItem"];
if(item && item.mescroll) item.mescroll.onPullDownRefresh();
}
}
}
export default MescrollCompMixin;

View File

@ -0,0 +1,57 @@
/**
* mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例)
*/
const MescrollMoreItemMixin = {
// 支付宝小程序不支持props的mixin,需写在具体的页面中
// #ifndef MP-ALIPAY || MP-DINGTALK
props:{
i: Number, // 每个tab页的专属下标
index: { // 当前tab的下标
type: Number,
default(){
return 0
}
}
},
// #endif
data() {
return {
downOption:{
auto:false // 不自动加载
},
upOption:{
auto:false // 不自动加载
},
isInit: false // 当前tab是否已初始化
}
},
watch:{
// 监听下标的变化
index(val){
if (this.i === val && !this.isInit) this.mescrollTrigger()
}
},
methods: {
// mescroll组件初始化的回调,可获取到mescroll对象 (覆盖mescroll-mixins.js的mescrollInit, 为了标记isInit)
mescrollInit(mescroll) {
this.mescroll = mescroll;
// 自动加载当前tab的数据
if(this.i === this.index){
this.mescrollTrigger()
}
},
// 主动触发加载
mescrollTrigger(){
this.isInit = true; // 标记为true
if (this.mescroll) {
if (this.mescroll.optDown.use) {
this.mescroll.triggerDownScroll();
} else{
this.mescroll.triggerUpScroll();
}
}
}
}
}
export default MescrollMoreItemMixin;

View File

@ -0,0 +1,77 @@
/**
* mescroll-body写在子组件时, 需通过mescroll的mixins补充子组件缺少的生命周期
*/
const MescrollMoreMixin = {
data() {
return {
tabIndex: 0, // 当前tab下标
mescroll: { // mescroll-body写在子子子...组件的情况 (多级)
onPageScroll: e=>{
this.handlePageScroll(e)
},
onReachBottom: ()=>{
this.handleReachBottom()
},
onPullDownRefresh: ()=>{
this.handlePullDownRefresh()
}
}
}
},
// 因为子组件无onPageScroll和onReachBottom的页面生命周期需在页面传递进到子组件
onPageScroll(e) {
this.handlePageScroll(e)
},
onReachBottom() {
this.handleReachBottom()
},
// 当down的native: true时, 还需传递此方法进到子组件
onPullDownRefresh(){
this.handlePullDownRefresh()
},
methods:{
handlePageScroll(e){
let mescroll = this.getMescroll(this.tabIndex);
mescroll && mescroll.onPageScroll(e);
},
handleReachBottom(){
let mescroll = this.getMescroll(this.tabIndex);
mescroll && mescroll.onReachBottom();
},
handlePullDownRefresh(){
let mescroll = this.getMescroll(this.tabIndex);
mescroll && mescroll.onPullDownRefresh();
},
// 根据下标获取对应子组件的mescroll
getMescroll(i){
if(!this.mescrollItems) this.mescrollItems = [];
if(!this.mescrollItems[i]) {
// v-for中的refs
let vForItem = this.$refs["mescrollItem"];
if(vForItem){
this.mescrollItems[i] = vForItem[i]
}else{
// 普通的refs,不可重复
this.mescrollItems[i] = this.$refs["mescrollItem"+i];
}
}
let item = this.mescrollItems[i]
return item ? item.mescroll : null
},
// 切换tab,恢复滚动条位置
tabChange(i){
let mescroll = this.getMescroll(i);
if(mescroll){
// 恢复上次滚动条的位置
let y = mescroll.getScrollTop()
mescroll.scrollTo(y, 0)
// 再次恢复上次滚动条的位置, 确保元素已渲染
setTimeout(()=>{
mescroll.scrollTo(y, 0)
},30)
}
}
}
}
export default MescrollMoreMixin;

View File

@ -0,0 +1,109 @@
// 定义在wxs (含renderjs) 逻辑层的数据和方法, 与视图层相互通信
const WxsMixin = {
data() {
return {
// 传入wxs视图层的数据 (响应式)
wxsProp: {
optDown:{}, // 下拉刷新的配置
scrollTop:0, // 滚动条的距离
bodyHeight:0, // body的高度
isDownScrolling:false, // 是否正在下拉刷新中
isUpScrolling:false, // 是否正在上拉加载中
isScrollBody:true, // 是否为mescroll-body滚动
isUpBoth:true, // 上拉加载时,是否同时可以下拉刷新
t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
},
// 标记调用wxs视图层的方法
callProp: {
callType: '', // 方法名
t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
},
// 不用wxs的平台使用此处的wxsBiz对象,抹平wxs的写法 (微信小程序和APP使用的wxsBiz对象是./wxs/wxs.wxs)
// #ifndef MP-WEIXIN || MP-QQ || APP-PLUS || H5
wxsBiz: {
//注册列表touchstart事件,用于下拉刷新
touchstartEvent: e=> {
this.mescroll.touchstartEvent(e);
},
//注册列表touchmove事件,用于下拉刷新
touchmoveEvent: e=> {
this.mescroll.touchmoveEvent(e);
},
//注册列表touchend事件,用于下拉刷新
touchendEvent: e=> {
this.mescroll.touchendEvent(e);
},
propObserver(){}, // 抹平wxs的写法
callObserver(){} // 抹平wxs的写法
},
// #endif
// 不用renderjs的平台使用此处的renderBiz对象,抹平renderjs的写法 (app 和 h5 使用的renderBiz对象是./wxs/renderjs.js)
// #ifndef APP-PLUS || H5
renderBiz: {
propObserver(){} // 抹平renderjs的写法
}
// #endif
}
},
methods: {
// wxs视图层调用逻辑层的回调
wxsCall(msg){
if(msg.type === 'setWxsProp'){
// 更新wxsProp数据 (值改变才触发更新)
this.wxsProp = {
optDown: this.mescroll.optDown,
scrollTop: this.mescroll.getScrollTop(),
bodyHeight: this.mescroll.getBodyHeight(),
isDownScrolling: this.mescroll.isDownScrolling,
isUpScrolling: this.mescroll.isUpScrolling,
isUpBoth: this.mescroll.optUp.isBoth,
isScrollBody:this.mescroll.isScrollBody,
t: Date.now()
}
}else if(msg.type === 'setLoadType'){
// 设置inOffset,outOffset的状态
this.downLoadType = msg.downLoadType
// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
this.$set(this.mescroll, 'downLoadType', this.downLoadType)
// 重置是否加载成功的状态
this.$set(this.mescroll, 'isDownEndSuccess', null)
}else if(msg.type === 'triggerDownScroll'){
// 主动触发下拉刷新
this.mescroll.triggerDownScroll();
}else if(msg.type === 'endDownScroll'){
// 结束下拉刷新
this.mescroll.endDownScroll();
}else if(msg.type === 'triggerUpScroll'){
// 主动触发上拉加载
this.mescroll.triggerUpScroll(true);
}
}
},
mounted() {
// #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5
// 配置主动触发wxs显示加载进度的回调
this.mescroll.optDown.afterLoading = ()=>{
this.callProp = {callType: "showLoading", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
}
// 配置主动触发wxs隐藏加载进度的回调
this.mescroll.optDown.afterEndDownScroll = ()=>{
this.callProp = {callType: "endDownScroll", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
let delay = 300 + (this.mescroll.optDown.beforeEndDelay || 0)
setTimeout(()=>{
if(this.downLoadType === 4 || this.downLoadType === 0){
this.callProp = {callType: "clearTransform", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
}
// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
this.$set(this.mescroll, 'downLoadType', this.downLoadType)
}, delay)
}
// 初始化wxs的数据
this.wxsCall({type: 'setWxsProp'})
// #endif
}
}
export default WxsMixin;

View File

@ -0,0 +1,92 @@
// 使用renderjs直接操作window对象,实现动态控制app和h5的bounce
// bounce: iOS橡皮筋,Android半月弧,h5浏览器下拉背景等效果 (下拉刷新时禁止)
// https://uniapp.dcloud.io/frame?id=renderjs
// 与wxs的me实例一致
var me = {}
// 初始化window对象的touch事件 (仅初始化一次)
if(window && !window.$mescrollRenderInit){
window.$mescrollRenderInit = true
window.addEventListener('touchstart', function(e){
if (me.disabled()) return;
me.startPoint = me.getPoint(e); // 记录起点
}, {passive: true})
window.addEventListener('touchmove', function(e){
if (me.disabled()) return;
if (me.getScrollTop() > 0) return; // 需在顶部下拉,才禁止bounce
var curPoint = me.getPoint(e); // 当前点
var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
// 向下拉
if (moveY > 0) {
// 可下拉的条件
if (!me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling && me.isUpBoth))) {
// 只有touch在mescroll的view上面,才禁止bounce
var el = e.target;
var isMescrollTouch = false;
while (el && el.tagName && el.tagName !== 'UNI-PAGE-BODY' && el.tagName != "BODY") {
var cls = el.classList;
if (cls && cls.contains('mescroll-render-touch')) {
isMescrollTouch = true
break;
}
el = el.parentNode; // 继续检查其父元素
}
// 禁止bounce (不会对swiper和iOS侧滑返回造成影响)
if (isMescrollTouch && e.cancelable && !e.defaultPrevented) e.preventDefault();
}
}
}, {passive: false})
}
/* 获取滚动条的位置 */
me.getScrollTop = function() {
return me.scrollTop || document.documentElement.scrollTop || document.body.scrollTop || 0
}
/* 是否禁用下拉刷新 */
me.disabled = function(){
return !me.optDown || !me.optDown.use || me.optDown.native
}
/* 根据点击滑动事件获取第一个手指的坐标 */
me.getPoint = function(e) {
if (!e) {
return {x: 0,y: 0}
}
if (e.touches && e.touches[0]) {
return {x: e.touches[0].pageX,y: e.touches[0].pageY}
} else if (e.changedTouches && e.changedTouches[0]) {
return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
} else {
return {x: e.clientX,y: e.clientY}
}
}
/**
* 监听逻辑层数据的变化 (实时更新数据)
*/
function propObserver(wxsProp) {
me.optDown = wxsProp.optDown
me.scrollTop = wxsProp.scrollTop
me.isDownScrolling = wxsProp.isDownScrolling
me.isUpScrolling = wxsProp.isUpScrolling
me.isUpBoth = wxsProp.isUpBoth
}
/* 导出模块 */
const renderBiz = {
data() {
return {
propObserver: propObserver,
}
}
}
export default renderBiz;

View File

@ -0,0 +1,269 @@
// 使用wxs处理交互动画, 提高性能, 同时避免小程序bounce对下拉刷新的影响
// https://uniapp.dcloud.io/frame?id=wxs
// https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html
// 模拟mescroll实例, 与mescroll.js的写法尽量保持一致
var me = {}
// ------ 自定义下拉刷新动画 start ------
/* 下拉过程中的回调,滑动过程一直在执行 (rate<1为inOffset; rate>1为outOffset) */
me.onMoving = function (ins, rate, downHight){
ins.requestAnimationFrame(function () {
ins.selectComponent('.mescroll-wxs-content').setStyle({
'will-change': 'transform', // 可解决下拉过程中, image和swiper脱离文档流的问题
'transform': 'translateY(' + downHight + 'px)',
'transition': ''
})
// 环形进度条
var progress = ins.selectComponent('.mescroll-wxs-progress')
progress && progress.setStyle({transform: 'rotate(' + 360 * rate + 'deg)'})
})
}
/* 显示下拉刷新进度 */
me.showLoading = function (ins){
me.downHight = me.optDown.offset
ins.requestAnimationFrame(function () {
ins.selectComponent('.mescroll-wxs-content').setStyle({
'will-change': 'auto',
'transform': 'translateY(' + me.downHight + 'px)',
'transition': 'transform 300ms'
})
})
}
/* 结束下拉 */
me.endDownScroll = function (ins){
me.downHight = 0;
me.isDownScrolling = false;
ins.requestAnimationFrame(function () {
ins.selectComponent('.mescroll-wxs-content').setStyle({
'will-change': 'auto',
'transform': 'translateY(0)', // 不可以写空串,否则scroll-view渲染不完整 (延时350ms会调clearTransform置空)
'transition': 'transform 300ms'
})
})
}
/* 结束下拉动画执行完毕后, 清除transform和transition, 避免对列表内容样式造成影响, 如: h5的list-msg示例下拉进度条漏出来等 */
me.clearTransform = function (ins){
ins.requestAnimationFrame(function () {
ins.selectComponent('.mescroll-wxs-content').setStyle({
'will-change': '',
'transform': '',
'transition': ''
})
})
}
// ------ 自定义下拉刷新动画 end ------
/**
* 监听逻辑层数据的变化 (实时更新数据)
*/
function propObserver(wxsProp) {
if(!wxsProp) return
me.optDown = wxsProp.optDown
me.scrollTop = wxsProp.scrollTop
me.bodyHeight = wxsProp.bodyHeight
me.isDownScrolling = wxsProp.isDownScrolling
me.isUpScrolling = wxsProp.isUpScrolling
me.isUpBoth = wxsProp.isUpBoth
me.isScrollBody = wxsProp.isScrollBody
me.startTop = wxsProp.scrollTop // 及时更新touchstart触发的startTop, 避免scroll-view快速惯性滚动到顶部取值不准确
}
/**
* 监听逻辑层数据的变化 (调用wxs的方法)
*/
function callObserver(callProp, oldValue, ins) {
if (me.disabled()) return;
if(callProp.callType){
// 逻辑层App Service的style已失效,需在视图层Webview设置style
if(callProp.callType === 'showLoading'){
me.showLoading(ins)
}else if(callProp.callType === 'endDownScroll'){
me.endDownScroll(ins)
}else if(callProp.callType === 'clearTransform'){
me.clearTransform(ins)
}
}
}
/**
* touch事件
*/
function touchstartEvent(e, ins) {
me.downHight = 0; // 下拉的距离
me.startPoint = me.getPoint(e); // 记录起点
me.startTop = me.getScrollTop(); // 记录此时的滚动条位置
me.startAngle = 0; // 初始角度
me.lastPoint = me.startPoint; // 重置上次move的点
me.maxTouchmoveY = me.getBodyHeight() - me.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
me.inTouchend = false; // 标记不是touchend
me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
}
function touchmoveEvent(e, ins) {
var isPrevent = true // false表示不往上冒泡相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
if (me.disabled()) return isPrevent;
var scrollTop = me.getScrollTop(); // 当前滚动条的距离
var curPoint = me.getPoint(e); // 当前点
var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
// 向下拉 && 在顶部
// mescroll-body,直接判定在顶部即可
// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
if (moveY > 0 && (
(me.isScrollBody && scrollTop <= 0)
||
(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
)) {
// 可下拉的条件
if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
me.isUpBoth))) {
// 下拉的角度是否在配置的范围内
if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
if (me.startAngle < me.optDown.minAngle) return isPrevent; // 如果小于配置的角度,则不往下执行下拉刷新
// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
me.inTouchend = true; // 标记执行touchend
touchendEvent(e, ins); // 提前触发touchend
return isPrevent;
}
isPrevent = false // 小程序是return false
var diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
// 下拉距离 < 指定距离
if (me.downHight < me.optDown.offset) {
if (me.movetype !== 1) {
me.movetype = 1; // 加入标记,保证只执行一次
// me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
me.callMethod(ins, {type: 'setLoadType', downLoadType: 1})
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
}
me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
// 指定距离 <= 下拉距离
} else {
if (me.movetype !== 2) {
me.movetype = 2; // 加入标记,保证只执行一次
// me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
me.callMethod(ins, {type: 'setLoadType', downLoadType: 2})
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
}
if (diff > 0) { // 向下拉
me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
} else { // 向上收
me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
}
}
me.downHight = Math.round(me.downHight) // 取整
var rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
// me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
me.onMoving(ins, rate, me.downHight)
}
}
me.lastPoint = curPoint; // 记录本次移动的点
return isPrevent // false表示不往上冒泡相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
}
function touchendEvent(e, ins) {
// 如果下拉区域高度已改变,则需重置回来
if (me.isMoveDown) {
if (me.downHight >= me.optDown.offset) {
// 符合触发刷新的条件
me.downHight = me.optDown.offset; // 更新下拉区域高度
// me.triggerDownScroll();
me.callMethod(ins, {type: 'triggerDownScroll'})
} else {
// 不符合的话 则重置
me.downHight = 0;
// me.optDown.endDownScroll && me.optDown.endDownScroll(me);
me.callMethod(ins, {type: 'endDownScroll'})
}
me.movetype = 0;
me.isMoveDown = false;
} else if (!me.isScrollBody && me.getScrollTop() === me.startTop) { // scroll-view到顶/左/右/底的滑动事件
var isScrollUp = me.getPoint(e).y - me.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
// 上滑
if (isScrollUp) {
// 需检查滑动的角度
var angle = me.getAngle(me.getPoint(e), me.startPoint); // 两点之间的角度,区间 [0,90]
if (angle > 80) {
// 检查并触发上拉
// me.triggerUpScroll(true);
me.callMethod(ins, {type: 'triggerUpScroll'})
}
}
}
me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
}
/* 是否禁用下拉刷新 */
me.disabled = function(){
return !me.optDown || !me.optDown.use || me.optDown.native
}
/* 根据点击滑动事件获取第一个手指的坐标 */
me.getPoint = function(e) {
if (!e) {
return {x: 0,y: 0}
}
if (e.touches && e.touches[0]) {
return {x: e.touches[0].pageX,y: e.touches[0].pageY}
} else if (e.changedTouches && e.changedTouches[0]) {
return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
} else {
return {x: e.clientX,y: e.clientY}
}
}
/* 计算两点之间的角度: 区间 [0,90]*/
me.getAngle = function (p1, p2) {
var x = Math.abs(p1.x - p2.x);
var y = Math.abs(p1.y - p2.y);
var z = Math.sqrt(x * x + y * y);
var angle = 0;
if (z !== 0) {
angle = Math.asin(y / z) / Math.PI * 180;
}
return angle
}
/* 获取滚动条的位置 */
me.getScrollTop = function() {
return me.scrollTop || 0
}
/* 获取body的高度 */
me.getBodyHeight = function() {
return me.bodyHeight || 0;
}
/* 调用逻辑层的方法 */
me.callMethod = function(ins, param) {
if(ins) ins.callMethod('wxsCall', param)
}
/* 导出模块 */
module.exports = {
propObserver: propObserver,
callObserver: callObserver,
touchstartEvent: touchstartEvent,
touchmoveEvent: touchmoveEvent,
touchendEvent: touchendEvent
}

View File

@ -0,0 +1,66 @@
// 小程序无法在hook中使用页面级别生命周期,需单独传入: https://ask.dcloud.net.cn/question/161173
// import { onPageScroll, onReachBottom, onPullDownRefresh} from '@dcloudio/uni-app';
/**
* 初始化mescroll, 相当于vue2的mescroll-mixins.js文件 (mescroll-body mescroll-uni 通用)
* mescroll-body需传入onPageScroll, onReachBottom
* mescroll-uni无需传onPageScroll, onReachBottom
* 当down.native为true时,需传入onPullDownRefresh
*/
function useMescroll(onPageScroll, onReachBottom, onPullDownRefresh){
// mescroll实例对象
let mescroll = null;
// mescroll组件初始化的回调,可获取到mescroll对象
const mescrollInit = (e)=> {
mescroll = e;
}
// 获取mescroll对象, mescrollInit执行之后会有值, 生命周期created中会有值
const getMescroll = ()=>{
return mescroll
}
// 下拉刷新的回调 (mixin默认resetUpScroll)
const downCallback = ()=> {
if(mescroll.optUp.use){
mescroll.resetUpScroll()
}else{
setTimeout(()=>{
mescroll.endSuccess();
}, 500)
}
}
// 上拉加载的回调
const upCallback = ()=> {
// mixin默认延时500自动结束加载
setTimeout(()=>{
mescroll.endErr();
}, 500)
}
// 注册系统自带的下拉刷新 (配置down.native为true时生效, 还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
onPullDownRefresh && onPullDownRefresh(() => {
mescroll && mescroll.onPullDownRefresh();
})
// 注册列表滚动事件,用于判定在顶部可下拉刷新,在指定位置可显示隐藏回到顶部按钮 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
onPageScroll && onPageScroll(e=>{
mescroll && mescroll.onPageScroll(e);
})
// 注册滚动到底部的事件,用于上拉加载 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
onReachBottom && onReachBottom(()=>{
mescroll && mescroll.onReachBottom();
})
return {
getMescroll,
mescrollInit,
downCallback,
upCallback
}
}
export default useMescroll

View File

@ -0,0 +1,56 @@
import { ref } from 'vue';
// 小程序无法在hook中使用页面级别生命周期,需单独传入: https://ask.dcloud.net.cn/question/161173
// import { onPageScroll, onReachBottom, onPullDownRefresh} from '@dcloudio/uni-app';
/**
* mescroll-body写在子组件时,需通过useMescrollComp补充子组件缺少的生命周期, 相当于vue2的mescroll-comp.js文件
* 必须传入onPageScroll, onReachBottom
* 当down.native为true时,需传入onPullDownRefresh
*/
function useMescrollComp(onPageScroll, onReachBottom, onPullDownRefresh){
// 因为子组件无onPageScroll和onReachBottom的页面生命周期需在页面传递进到子组件
onPageScroll(e=>{
handlePageScroll(e)
})
onReachBottom(()=>{
handleReachBottom()
})
// 当down的native: true时, 还需传递此方法进到子组件
onPullDownRefresh && onPullDownRefresh(()=>{
handlePullDownRefresh()
})
const mescrollItem = ref(null)
const handlePageScroll = (e)=>{
const mescroll = getMescroll()
mescroll && mescroll.onPageScroll(e);
}
const handleReachBottom = ()=>{
const mescroll = getMescroll()
mescroll && mescroll.onReachBottom();
}
const handlePullDownRefresh = ()=>{
const mescroll = getMescroll()
mescroll && mescroll.onPullDownRefresh();
}
const getMescroll = ()=>{
if(mescrollItem.value && mescrollItem.value.getMescroll){
return mescrollItem.value.getMescroll()
}
return null
}
return {
mescrollItem,
getMescroll
}
}
export default useMescrollComp

View File

@ -0,0 +1,69 @@
import { ref } from 'vue';
// 小程序无法在hook中使用页面级别生命周期,需单独传入: https://ask.dcloud.net.cn/question/161173
// import { onPageScroll, onReachBottom, onPullDownRefresh} from '@dcloudio/uni-app';
/** mescroll-more示例写在子组件时,需通过useMescrollMore补充子组件缺少的生命周期, 相当于vue2的mescroll-more.js文件 */
function useMescrollMore(mescrollItems, onPageScroll, onReachBottom, onPullDownRefresh){
// 当前tab下标
const tabIndex = ref(0)
// 因为子组件无onPageScroll和onReachBottom的页面生命周期需在页面传递进到子组件
onPageScroll && onPageScroll(e=>{
handlePageScroll(e)
})
onReachBottom && onReachBottom(()=>{
handleReachBottom()
})
// 当down的native: true时, 还需传递此方法进到子组件
onPullDownRefresh && onPullDownRefresh(()=>{
handlePullDownRefresh()
})
const handlePageScroll = (e)=>{
let mescroll = getMescroll(tabIndex.value);
mescroll && mescroll.onPageScroll(e);
}
const handleReachBottom = ()=>{
let mescroll = getMescroll(tabIndex.value);
mescroll && mescroll.onReachBottom();
}
const handlePullDownRefresh = ()=>{
let mescroll = getMescroll(tabIndex.value);
mescroll && mescroll.onPullDownRefresh();
}
// 根据下标获取对应子组件的mescroll
const getMescroll = (i)=>{
if (mescrollItems && mescrollItems[i]) {
return mescrollItems[i].value.getMescroll()
} else{
return null
}
}
// 切换tab,恢复滚动条位置
const scrollToLastY = ()=>{
let mescroll = getMescroll(tabIndex.value);
if(mescroll){
// 恢复上次滚动条的位置
let y = mescroll.getScrollTop()
mescroll.scrollTo(y, 0)
// 再次恢复上次滚动条的位置, 确保元素已渲染
setTimeout(()=>{
mescroll.scrollTo(y, 0)
},20)
}
}
return {
tabIndex,
getMescroll,
scrollToLastY
}
}
export default useMescrollMore

View File

@ -0,0 +1,76 @@
{
"id": "mescroll-uni",
"displayName": "高性能下拉刷新上拉加载组件 支持vue3 setup",
"version": "1.3.8",
"description": "wxs+renderjs实现, 支持原生页面和局部区域滚动, 支持vue3 script setup的写法",
"keywords": [
"下拉刷新",
"上拉加载",
"翻页分页",
"wxs",
"setup"
],
"repository": "https://github.com/mescroll/mescroll",
"engines": {
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/mescroll-uni",
"type": "component-vue"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "y",
"联盟": "y"
}
}
}
}
}

View File

@ -0,0 +1,45 @@
## mescroll --【wxs+renderjs实现】高性能的下拉刷新上拉加载组件
1. mescroll的uni版本 是专门用在uni-app的下拉刷新和上拉加载的组件
2. mescroll的uni版本 继承了mescroll.js的实用功能: 自动处理分页, 自动控制无数据, 空布局提示, 回到顶部按钮 ..
3. mescroll的uni版本 丰富的案例, 自由灵活的api, 超详细的注释, 可让您快速自定义真正属于自己的下拉上拉组件
<br/>
## 最新文档(1.3.8版本): <a href="https://www.mescroll.com/uni.html">https://www.mescroll.com/uni.html</a>
2023-03-26 by 小瑾同学 (文档可能会有缓存,建议打开时刷新一下)
## 1.3.5版本已调整为[uni_modules](https://uniapp.dcloud.io/uni_modules)
uni_modules版本的mescroll-body 和 mescroll-empty 支持 [easycom规范](https://uniapp.dcloud.io/collocation/pages?id=easycom)
所以 main.js 无需再为mescroll-body注册全局组件
所以个别页面要单独使用 mescroll-empty , 也无需手动注册
#### 1.3.5以前的用户升级为uni_modules版本:
```
1. 删除原来的 @/components/mescroll-uni 组件
2. 删除 main.js 注册的 mescroll 组件
3. 从插件市场导入最新mescroll组件 (1.3.5+uni_modules版本)
4. 全局搜索 '@/components/mescroll-uni/' 替换为 '@/uni_modules/mescroll-uni/components/mescroll-uni/'
5. mescroll-empty遵循easycom规范, 若某些页面单独使用 'mescroll-empty.vue', 可删除手动导入的代码
```
## 近期已更新优化的内容:
1. 新增vue3 script setup的示例
2. 新增`入门极简`示例, 国际化`mescroll-i18n.vue`示例, 轮播吸顶菜单`mescroll-swiper-sticky.vue`示例
3. 新增 "局部区域滚动" 的案例: mescroll-body-part.vue 和 mescroll-uni-part.vue
4. 新增 me-video 视频组件, 解决APP端视频下拉悬浮错位的问题, 参考 mescroll-options.vue 示例
5. 新增 me-tabs 组件,tabs支持水平滑动; 优化mescroll-more和mescroll-swiper的案例, 顶部tab支持水平滑动
6. 吸顶悬浮提供了原生sticky和监听滚动条实现的示例: sticky.vue 和 sticky-scroll.vue (推荐使用sticky样式实现)
7. mescroll.scrollTo(y)的y支持css选择器, 包括跨自定义组件的后代选择器, 支持滚动到子组件的view (参考 mescroll-options.vue)
8. topbar 顶部是否预留状态栏的高度, 默认false; 还可支持设置状态栏背景: 如 '#ffff00', 'url(xxx) 0 0/100% 100%', 'linear-gradient(xx)'
9. down.bgColor 和 up.bgColor 加载区域的背景,不仅支持色值, 而且还是支持背景图和渐变: 如 'url(xxx) 0 0/100% 100%', 'linear-gradient(xx)'
10. topbar,bgColor支持一行代码定义background: [https://www.runoob.com/cssref/css3-pr-background.html](https://www.runoob.com/cssref/css3-pr-background.html)
<br/>
<br/>
<a href="https://ext.dcloud.net.cn/plugin?id=343&update_log">查看更多 ... </a>
<br/>
#### mescroll不支持nvue,也暂无支持的计划哈,so sorry~

View File

@ -0,0 +1,67 @@
import Request from 'luch-request';
import { useGlobSetting } from '@/config';
import { useUserStoreWithOut } from '@/store/modules/user';
const { apiUrl } = useGlobSetting();
const http = new Request();
http.setConfig((config) => {
config.baseURL = apiUrl;
config.timeout = 10000;
config.header = Object.assign({
'Content-Type': 'application/json;charset=UTF-8'
}, config.header);
/* 设置全局配置 */
config.validateStatus = (statusCode) => {
// 不论什么状态,统一在正确中处理
return true;
};
return config;
});
http.interceptors.request.use((config) => {
config.header = Object.assign({}, config.header);
if (config.params) {
for (const [key, value] of Object.entries(config.params)) {
if (value === undefined || value === null || value === '')
delete config.params[key];
}
}
if (config.data) {
for (const [key, value] of Object.entries(config.data)) {
if (value === undefined || value === null || value === '')
delete config.data[key];
}
}
const userStore = useUserStoreWithOut();
const token = userStore.token;
if (token != null) {
config.header.Authorization = `Bearer ${token}`;
}
return config;
}, (config) => {
return Promise.reject(config);
});
// 必须使用异步函数,注意
http.interceptors.response.use(async (response) => {
const { isTransformResponse = true, isReturnNativeResponse } = response.config.custom;
// 是否返回原生响应头
if (isReturnNativeResponse) {
return response;
}
// 是否需要处理请求结果
if (!isTransformResponse) {
return response.data.data;
}
return response.data;
}, (error) => {
return error;
});
export { http };
export const Method = {
GET: 'GET',
POST: 'POST',
PUT: 'PUT',
DELETE: 'DELETE',
};

View File

@ -1,10 +1,25 @@
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
import UnoCSS from 'unocss/vite'
// https://vitejs.dev/config/
import { resolve } from 'path';
export default defineConfig({
plugins: [
uni(),
UnoCSS()
],
resolve: {
alias: {
'@': resolve(__dirname, './src'),
},
},
server: {
proxy: {
'/api': {
target: 'http://store-manage.hmily.club',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api'),
},
}
}
})

7627
yarn.lock

File diff suppressed because it is too large Load Diff