Initial commit
commit
936aa0be1d
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
|
||||
VITE_COMMON_API_PREFIX = /api
|
||||
|
||||
VITE_COMMON_API_URL = http://store-manage.hmily.club
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
VITE_COMMON_API_PREFIX = /api
|
||||
|
||||
VITE_COMMON_API_URL = http://store-manage.hmily.club
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
VITE_COMMON_API_PREFIX = /api
|
||||
|
||||
VITE_COMMON_API_URL = http://store-manage.hmily.club
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||
CSS.supports('top: constant(a)'))
|
||||
document.write(
|
||||
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||
</script>
|
||||
<title></title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"name": "uni-preset-vue",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev:app": "uni -p app",
|
||||
"dev:app-android": "uni -p app-android",
|
||||
"dev:app-ios": "uni -p app-ios",
|
||||
"dev:custom": "uni -p",
|
||||
"dev:h5": "uni",
|
||||
"dev:h5:ssr": "uni --ssr",
|
||||
"dev:mp-alipay": "uni -p mp-alipay",
|
||||
"dev:mp-baidu": "uni -p mp-baidu",
|
||||
"dev:mp-jd": "uni -p mp-jd",
|
||||
"dev:mp-kuaishou": "uni -p mp-kuaishou",
|
||||
"dev:mp-lark": "uni -p mp-lark",
|
||||
"dev:mp-qq": "uni -p mp-qq",
|
||||
"dev:mp-toutiao": "uni -p mp-toutiao",
|
||||
"dev:mp-weixin": "uni -p mp-weixin",
|
||||
"dev:mp-xhs": "uni -p mp-xhs",
|
||||
"dev:quickapp-webview": "uni -p quickapp-webview",
|
||||
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
|
||||
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
|
||||
"build:app": "uni build -p app",
|
||||
"build:app-android": "uni build -p app-android",
|
||||
"build:app-ios": "uni build -p app-ios",
|
||||
"build:custom": "uni build -p",
|
||||
"build:h5": "uni build",
|
||||
"build:h5:ssr": "uni build --ssr",
|
||||
"build:mp-alipay": "uni build -p mp-alipay",
|
||||
"build:mp-baidu": "uni build -p mp-baidu",
|
||||
"build:mp-jd": "uni build -p mp-jd",
|
||||
"build:mp-kuaishou": "uni build -p mp-kuaishou",
|
||||
"build:mp-lark": "uni build -p mp-lark",
|
||||
"build:mp-qq": "uni build -p mp-qq",
|
||||
"build:mp-toutiao": "uni build -p mp-toutiao",
|
||||
"build:mp-weixin": "uni build -p mp-weixin",
|
||||
"build:mp-xhs": "uni build -p mp-xhs",
|
||||
"build:quickapp-webview": "uni build -p quickapp-webview",
|
||||
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
|
||||
"build:quickapp-webview-union": "uni build -p quickapp-webview-union"
|
||||
},
|
||||
"dependencies": {
|
||||
"@climblee/uv-ui": "^1.1.20",
|
||||
"@dcloudio/uni-app": "3.0.0-3090920231225001",
|
||||
"@dcloudio/uni-app-plus": "3.0.0-3090920231225001",
|
||||
"@dcloudio/uni-components": "3.0.0-3090920231225001",
|
||||
"@dcloudio/uni-h5": "3.0.0-3090920231225001",
|
||||
"@dcloudio/uni-mp-alipay": "3.0.0-3090920231225001",
|
||||
"@dcloudio/uni-mp-baidu": "3.0.0-3090920231225001",
|
||||
"@dcloudio/uni-mp-jd": "3.0.0-3090920231225001",
|
||||
"@dcloudio/uni-mp-kuaishou": "3.0.0-3090920231225001",
|
||||
"@dcloudio/uni-mp-lark": "3.0.0-3090920231225001",
|
||||
"@dcloudio/uni-mp-qq": "3.0.0-3090920231225001",
|
||||
"@dcloudio/uni-mp-toutiao": "3.0.0-3090920231225001",
|
||||
"@dcloudio/uni-mp-weixin": "3.0.0-3090920231225001",
|
||||
"@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",
|
||||
"vue-i18n": "^9.1.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dcloudio/types": "^3.3.2",
|
||||
"@dcloudio/uni-automator": "3.0.0-3090920231225001",
|
||||
"@dcloudio/uni-cli-shared": "3.0.0-3090920231225001",
|
||||
"@dcloudio/uni-stacktracey": "3.0.0-3090920231225001",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-3090920231225001",
|
||||
"@vue/runtime-core": "^3.2.45",
|
||||
"sass": "^1.71.1",
|
||||
"unocss": "^0.58.5",
|
||||
"unocss-applet": "^0.7.8",
|
||||
"vite": "4.0.3"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/// <reference types='@dcloudio/types' />
|
||||
import 'vue'
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
type Hooks = App.AppInstance & Page.PageInstance;
|
||||
|
||||
interface ComponentCustomOptions extends Hooks {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<script>
|
||||
// import { useUserStoreWithOut } from "@/store/modules/user";
|
||||
export default {
|
||||
onLaunch: function () {
|
||||
console.log('App Launch')
|
||||
},
|
||||
onShow: function () {
|
||||
console.log('App Show')
|
||||
},
|
||||
onHide: function () {
|
||||
console.log('App Hide')
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/static/css/index.scss';
|
||||
@import '@climblee/uv-ui/index.scss';
|
||||
</style>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
<template>
|
||||
<uv-navbar
|
||||
:fixed="fixed"
|
||||
:bgColor="bgColor"
|
||||
:leftIcon="leftIcon"
|
||||
:title="title"
|
||||
:titleStyle="{...titleStyle}"
|
||||
:placeholder="placeholder"
|
||||
@leftClick="onLeftClick"
|
||||
z-index="1000"
|
||||
>
|
||||
<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: '',
|
||||
},
|
||||
leftIcon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
titleStyle: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
color: '#fff',
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
<template>
|
||||
<!-- 不能用v-if (i: 每个tab页的专属下标; index: 当前tab的下标 )-->
|
||||
<view v-show="i === index">
|
||||
<!-- top="120"下拉布局往下偏移,防止被悬浮菜单遮住 -->
|
||||
<mescroll-body
|
||||
:height="`${scrollHeight}px`"
|
||||
@init="mescrollInit"
|
||||
:down="downOption"
|
||||
@down="downCallback"
|
||||
:up="upOption"
|
||||
@up="upCallback"
|
||||
|
||||
>
|
||||
<slot :list="list"></slot>
|
||||
</mescroll-body>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { http } from '@/utils/request'
|
||||
import { sys } from '@climblee/uv-ui/libs/function'
|
||||
import { ref, watch, nextTick, computed } from 'vue'
|
||||
import useMescroll from '@/uni_modules/mescroll-uni/hooks/useMescroll.js'
|
||||
// import { apiGoods } from '@/api/mock.js'
|
||||
|
||||
const props = defineProps({
|
||||
i: Number,
|
||||
index: {
|
||||
type: Number,
|
||||
default() {
|
||||
return 0
|
||||
},
|
||||
},
|
||||
top: {
|
||||
type: Number,
|
||||
default: 44,
|
||||
},
|
||||
apiUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
params: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
const scrollHeight = computed(() => {
|
||||
return sys().screenHeight - props.top
|
||||
})
|
||||
|
||||
// 子组件的mescroll-body无需传入onPageScroll, onReachBottom
|
||||
const { mescrollInit, downCallback, getMescroll } = useMescroll() // 调用mescroll的hook
|
||||
|
||||
defineExpose({ getMescroll }) // 使父组件可以通过ref调用到getMescroll方法 (必须)
|
||||
|
||||
const isAutoInit = props.i === props.index // 自动加载当前tab的数据
|
||||
const downOption = {
|
||||
auto: isAutoInit, // 自动加载当前tab的数据
|
||||
}
|
||||
|
||||
const upOption = {
|
||||
auto: false, // 不自动加载
|
||||
noMoreSize: 4, //如果列表已无数据,可设置列表的总数量要大于半页才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看; 默认5
|
||||
empty: {
|
||||
tip: '~ 空空如也 ~', // 提示
|
||||
},
|
||||
}
|
||||
|
||||
const isInit = ref(isAutoInit) // 当前tab是否已初始化
|
||||
const list = ref([]) // 数据列表
|
||||
|
||||
// 监听下标的变化
|
||||
watch(
|
||||
() => props.index,
|
||||
(val) => {
|
||||
if (props.i === val && !isInit.value) mescrollTrigger()
|
||||
}
|
||||
)
|
||||
|
||||
// 主动触发加载
|
||||
const mescrollTrigger = () => {
|
||||
isInit.value = true // 标记为true
|
||||
const mescroll = getMescroll()
|
||||
if (mescroll) {
|
||||
if (mescroll.optDown.use) {
|
||||
mescroll.triggerDownScroll()
|
||||
} else {
|
||||
mescroll.triggerUpScroll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*上拉加载的回调: 其中mescroll.num:当前页 从1开始, mescroll.size:每页数据条数,默认10 */
|
||||
const upCallback = async (mescroll) => {
|
||||
await nextTick()
|
||||
if (!props.apiUrl) return
|
||||
http.request({
|
||||
url: props.apiUrl,
|
||||
method: 'GET',
|
||||
params: {
|
||||
per_page: mescroll.size,
|
||||
page: mescroll.num,
|
||||
...props.params,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
const arr = res.data || [] // 当前页数据
|
||||
if (mescroll.num == 1) list.value = [] //如果是第一页需手动制空列表
|
||||
list.value = list.value.concat(arr) //追加新数据
|
||||
mescroll.endSuccess(arr.length) // 请求成功, 结束加载
|
||||
})
|
||||
.catch(() => {
|
||||
mescroll.endErr() // 请求失败, 结束加载
|
||||
})
|
||||
// let word = props.tabs[props.i].name // 具体项目中,您可能取的是tab中的type,status等字段
|
||||
// apiGoods(mescroll.num, mescroll.size, word)
|
||||
// .then((res) => {
|
||||
// const list = res.list || [] // 当前页数据
|
||||
// if (mescroll.num == 1) goods.value = [] //如果是第一页需手动制空列表
|
||||
// goods.value = goods.value.concat(list) //追加新数据
|
||||
// mescroll.endSuccess(list.length) // 请求成功, 结束加载
|
||||
// })
|
||||
// .catch(() => {
|
||||
// mescroll.endErr() // 请求失败, 结束加载
|
||||
// })
|
||||
}
|
||||
|
||||
//点击空布局按钮的回调
|
||||
const emptyClick = () => {
|
||||
uni.showToast({
|
||||
title: '点击了按钮,具体逻辑自行实现',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<!-- 当mescroll-body写在子组件时,父页面需引入useMescrollComp.js -->
|
||||
<mescroll-body
|
||||
:height="`${scrollHeight}px`"
|
||||
@init="mescrollInit"
|
||||
@down="downCallback"
|
||||
@up="upCallback"
|
||||
>
|
||||
<slot :list="list"></slot>
|
||||
</mescroll-body>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { sys } from '@climblee/uv-ui/libs/function'
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
import useMescroll from '@/uni_modules/mescroll-uni/hooks/useMescroll.js'
|
||||
import { http } from '@/utils/request'
|
||||
const props = defineProps({
|
||||
top: {
|
||||
type: Number,
|
||||
default: 44,
|
||||
},
|
||||
apiUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
params: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
const scrollHeight = computed(() => {
|
||||
return sys().screenHeight - props.top
|
||||
})
|
||||
const list = ref([]) // 数据列表
|
||||
|
||||
// 子组件的mescroll-body无需传入onPageScroll, onReachBottom
|
||||
const { mescrollInit, downCallback, getMescroll } = useMescroll() // 调用mescroll的hook
|
||||
|
||||
// 使父组件可以通过ref调用到getMescroll方法 (必须)
|
||||
defineExpose({ getMescroll })
|
||||
|
||||
// 上拉加载的回调: 其中num:当前页 从1开始, size:每页数据条数,默认10
|
||||
const upCallback = async (mescroll) => {
|
||||
await nextTick()
|
||||
if (!props.apiUrl) return
|
||||
http
|
||||
.get(props.apiUrl, {
|
||||
params: {
|
||||
per_page: mescroll.size,
|
||||
page: mescroll.num,
|
||||
...props.params,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
const curPageData = res.data || [] // 当前页数据
|
||||
if (mescroll.num == 1) list.value = [] // 第一页需手动制空列表
|
||||
list.value = list.value.concat(curPageData) // 追加新数据
|
||||
mescroll.endSuccess(curPageData.length) // 隐藏加载进度
|
||||
})
|
||||
.catch(() => {
|
||||
mescroll.endErr() // 隐藏加载进度
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<template>
|
||||
<view class="flex items-center justify-between">
|
||||
<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">
|
||||
.title {
|
||||
@apply relative pl-20rpx;
|
||||
&::after {
|
||||
content: '';
|
||||
@apply absolute left-0 bottom-4rpx top-4rpx w-3px bg-primary;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import {
|
||||
createSSRApp
|
||||
} from "vue";
|
||||
import App from "./App.vue";
|
||||
import 'virtual:uno.css'
|
||||
import { setupStore } from '@/store';
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App);
|
||||
setupStore(app);
|
||||
return {
|
||||
app,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"name" : "",
|
||||
"appid" : "",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
"usingComponents" : true,
|
||||
"nvueStyleCompiler" : "uni-app",
|
||||
"compilerVersion" : 3,
|
||||
"splashscreen" : {
|
||||
"alwaysShowBeforeRender" : true,
|
||||
"waiting" : true,
|
||||
"autoclose" : true,
|
||||
"delay" : 0
|
||||
},
|
||||
/* 模块配置 */
|
||||
"modules" : {},
|
||||
/* 应用发布信息 */
|
||||
"distribute" : {
|
||||
/* android打包配置 */
|
||||
"android" : {
|
||||
"permissions" : [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
/* ios打包配置 */
|
||||
"ios" : {},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs" : {}
|
||||
}
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-baidu" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-toutiao" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"uniStatistics": {
|
||||
"enable": false
|
||||
},
|
||||
"vueVersion" : "3"
|
||||
}
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/home/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "首页"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/revert/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "上报"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/statement/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "报表"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的"
|
||||
}
|
||||
}
|
||||
],
|
||||
"subPackages": [
|
||||
{
|
||||
"root": "pages/data",
|
||||
"pages": [
|
||||
{
|
||||
"path": "brokerage/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "提成数据"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "performance/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "业绩数据"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "upload/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "上传数据"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/user",
|
||||
"pages": [
|
||||
{
|
||||
"path": "index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "员工管理"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "update",
|
||||
"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": "task_hygienes_submit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "清洁任务提交"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "task_ledgers_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": "升职申请"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "create",
|
||||
"style": {
|
||||
"navigationBarTitleText": "升职申请"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "uni-app",
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"navigationStyle": "custom",
|
||||
"backgroundColorTop": "#FFFFFF",
|
||||
"app-plus": {
|
||||
"bounce": "none",
|
||||
"scrollIndicator": "none"
|
||||
}
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#333",
|
||||
"selectedColor": "#ff3c2a",
|
||||
"borderStyle": "black",
|
||||
"backgroundColor": "#ffffff",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/home/index",
|
||||
"selectedIconPath": "static/images/home.png",
|
||||
"iconPath": "static/images/home.png",
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/revert/index",
|
||||
"selectedIconPath": "static/images/home.png",
|
||||
"iconPath": "static/images/home.png",
|
||||
"text": "上报"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/statement/index",
|
||||
"selectedIconPath": "static/images/home.png",
|
||||
"iconPath": "static/images/home.png",
|
||||
"text": "报表"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/mine/index",
|
||||
"selectedIconPath": "static/images/home.png",
|
||||
"iconPath": "static/images/home.png",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
},
|
||||
"easycom": {
|
||||
"autoscan": true,
|
||||
"custom": {
|
||||
"^uv-(.*)": "@climblee/uv-ui/components/uv-$1/uv-$1.vue"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<view>1</view>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<view class="card">
|
||||
<view class="flex justify-between">
|
||||
<view class="font-600">{{ data.month }}</view>
|
||||
<view class="space-x-20rpx">
|
||||
<text>提成</text>
|
||||
<text class="text-primary">{{ data.commission }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex mt-20rpx">
|
||||
<view class="w-160rpx">支出</view>
|
||||
<view class="flex-1 grid grid-cols-3 text-gray-500">
|
||||
<view class="text-right">
|
||||
<text>日常:</text>
|
||||
<text>{{ data.daily_expenses }}</text>
|
||||
</view>
|
||||
<view class="text-right">
|
||||
<text>员工:</text>
|
||||
<text>{{ data.employee_expenses }}</text>
|
||||
</view>
|
||||
<view class="text-right">
|
||||
<text>其他:</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>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<view>
|
||||
<CuNavbar title="提成数据" isBack></CuNavbar>
|
||||
|
||||
<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>
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
<template>
|
||||
<view>
|
||||
<CuNavbar title="业绩数据" isBack></CuNavbar>
|
||||
<view class="bg-primary bg-opacity-80 p-base">
|
||||
<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">{{ countData.actual_performance }}</view>
|
||||
</view>
|
||||
<view>/</view>
|
||||
<view class="flex-1 text-center">
|
||||
<view class="text-32rpx">目标业绩</view>
|
||||
<view class="mt-10rpx">{{ countData.expected_performance }}</view>
|
||||
</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>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<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>
|
||||
<mescroll-body @init="mescrollInit" @down="downCallback" @up="upCallback">
|
||||
<view class="px-base space-y-20rpx mt-30rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="card-shadow bg-white rounded-19rpx p-base space-y-10rpx"
|
||||
>
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="text-30rpx"> {{ item.type.name }} </view>
|
||||
<view
|
||||
:style="[
|
||||
{
|
||||
color: statusFun(
|
||||
item.workflow_check.check_status,
|
||||
'statusExpense',
|
||||
'color'
|
||||
),
|
||||
},
|
||||
]"
|
||||
class="text-24rpx"
|
||||
>{{ item.workflow_check.check_status_text }}</view
|
||||
>
|
||||
</view>
|
||||
<view class="text-24rpx text-hex-999999 flex">
|
||||
<view class="w-140rpx">报销金额</view>
|
||||
<view class="text-primary">{{ item.expense }}</view>
|
||||
</view>
|
||||
<view class="text-24rpx text-hex-999999 flex">
|
||||
<view class="w-140rpx">报销时间</view>
|
||||
<view class="text-hex-333">{{ timeFormat(item.created_at) }}</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">{{ item.reason }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</mescroll-body>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import CuNavbar from '@/components/cu-navbar/index'
|
||||
import { http } from '@/utils/request'
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { onPageScroll, onReachBottom } from '@dcloudio/uni-app'
|
||||
import useMescroll from '@/uni_modules/mescroll-uni/hooks/useMescroll.js'
|
||||
import { timeFormat } from '@climblee/uv-ui/libs/function'
|
||||
import statusFun from '@/utils/status'
|
||||
const { mescrollInit, downCallback, getMescroll } = useMescroll(
|
||||
onPageScroll,
|
||||
onReachBottom
|
||||
)
|
||||
const list = ref([])
|
||||
|
||||
const tabList = ref([
|
||||
{
|
||||
name: '我的报销',
|
||||
},
|
||||
{
|
||||
name: '报销审核',
|
||||
},
|
||||
])
|
||||
|
||||
const upCallback = async (mescroll) => {
|
||||
const { size, num } = mescroll
|
||||
|
||||
try {
|
||||
const resData = await http.get('/reimbursements', {
|
||||
params: {
|
||||
per_page: size,
|
||||
page: num,
|
||||
},
|
||||
})
|
||||
const curPageData = resData || []
|
||||
if (num === 1) list.value = []
|
||||
list.value = list.value.concat(curPageData)
|
||||
mescroll.endSuccess(curPageData.length)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
mescroll.endErr() // 请求失败, 结束加载
|
||||
} finally {
|
||||
// firstloading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const goPath = (url) => {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
<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 prop="reimbursement_type_id">
|
||||
<view
|
||||
@click="openType"
|
||||
keyName="name"
|
||||
class="h-full w-full flex justify-end"
|
||||
>
|
||||
{{ type.name }}
|
||||
<uv-icon name="arrow-right"></uv-icon>
|
||||
</view>
|
||||
</uv-form-item>
|
||||
<uv-line color="#f5f5f5"></uv-line>
|
||||
<uv-form-item label="报销金额" required prop="expense">
|
||||
<uv-input
|
||||
:border="`none`"
|
||||
placeholder="请输入报销金额"
|
||||
type="digit"
|
||||
input-align="right"
|
||||
v-model="form.expense"
|
||||
></uv-input>
|
||||
</uv-form-item>
|
||||
<uv-line color="#f5f5f5"></uv-line>
|
||||
<uv-form-item
|
||||
label="报销原因"
|
||||
required
|
||||
prop="reason"
|
||||
:borderBottom="true"
|
||||
labelPosition="top"
|
||||
>
|
||||
<uv-textarea
|
||||
:border="`none`"
|
||||
v-model="form.reason"
|
||||
placeholder="请输入报销原因"
|
||||
></uv-textarea>
|
||||
</uv-form-item>
|
||||
<uv-form-item
|
||||
label="报销凭证"
|
||||
labelPosition="top"
|
||||
prop="photos"
|
||||
required
|
||||
>
|
||||
<view class="w-full mt-15rpx">
|
||||
<uv-upload
|
||||
:maxCount="9"
|
||||
multiple
|
||||
:fileList="form.photos"
|
||||
@afterRead="afterRead"
|
||||
@delete="deletePic"
|
||||
name="photos"
|
||||
></uv-upload>
|
||||
</view>
|
||||
</uv-form-item>
|
||||
</uv-form>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-100rpx">
|
||||
<uv-button type="primary" @click="submit">提交</uv-button>
|
||||
</view>
|
||||
|
||||
<uv-picker
|
||||
ref="typeRef"
|
||||
keyName="name"
|
||||
:columns="[typeList]"
|
||||
@confirm="typeConfirm"
|
||||
></uv-picker>
|
||||
|
||||
<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'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
const typeRef = ref(null)
|
||||
const typeList = ref([])
|
||||
const type = ref({})
|
||||
const modalRef = ref(null)
|
||||
const formRef = ref(null)
|
||||
const loading = ref(false)
|
||||
const form = reactive({
|
||||
reimbursement_type_id: '',
|
||||
expense: '',
|
||||
reason: '',
|
||||
photos: [],
|
||||
})
|
||||
const rules = reactive({
|
||||
reimbursement_type_id: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择报销分类',
|
||||
},
|
||||
],
|
||||
expense: [{ required: true, message: '请输入报销金额' }],
|
||||
reason: [{ required: true, message: '请输入报销原因' }],
|
||||
photos: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
message: '请上传报销凭证',
|
||||
},
|
||||
})
|
||||
|
||||
onLoad(() => {
|
||||
getTypes()
|
||||
})
|
||||
|
||||
const submit = () => {
|
||||
formRef.value.validate().then((res) => {
|
||||
modalRef.value.open()
|
||||
})
|
||||
}
|
||||
const onSubmit = async () => {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const resData = await http.post('/reimbursements', {
|
||||
reimbursement_type_id: form.reimbursement_type_id,
|
||||
expense: form.expense,
|
||||
reason: form.reason,
|
||||
photos: form.photos.map((item) => item.url),
|
||||
})
|
||||
uni.showToast({
|
||||
title: '提交成功',
|
||||
icon: 'none',
|
||||
})
|
||||
formRef.value.resetFields()
|
||||
uni.$emit('ex:submit', resData)
|
||||
uni.navigateBack()
|
||||
} catch (error) {
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
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++) {
|
||||
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 uploadFilePromise = (url) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http
|
||||
.upload('/fileupload', {
|
||||
filePath: url,
|
||||
name: 'file',
|
||||
})
|
||||
.then((res) => {
|
||||
resolve(res.url)
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const deletePic = (event) => {
|
||||
form[event.name].splice(event.index, 1)
|
||||
}
|
||||
const getTypes = () => {
|
||||
http.get('/keyword?parent_key=reimbursement_type').then((res) => {
|
||||
typeList.value = res
|
||||
})
|
||||
}
|
||||
|
||||
const typeConfirm = ({ value }) => {
|
||||
type.value = value[0]
|
||||
form.reimbursement_type_id = type.value.id
|
||||
}
|
||||
|
||||
const openType = () => {
|
||||
typeRef.value.open()
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<view>
|
||||
<qiun-data-charts type="area" :opts="opts" :chartData="chartData" />
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
const opts = ref({
|
||||
color: [
|
||||
'#ee2c37',
|
||||
|
||||
],
|
||||
padding: [15, 15, 0, 15],
|
||||
enableScroll: false,
|
||||
legend: {
|
||||
show:false
|
||||
},
|
||||
xAxis: {
|
||||
disableGrid: true,
|
||||
},
|
||||
yAxis: {
|
||||
gridType: 'dash',
|
||||
dashLength: 2,
|
||||
},
|
||||
extra: {
|
||||
area: {
|
||||
type: 'curve',
|
||||
opacity: 0.2,
|
||||
addLine: true,
|
||||
width: 2,
|
||||
gradient: true,
|
||||
activeType: 'hollow',
|
||||
},
|
||||
},
|
||||
})
|
||||
const chartData = ref({
|
||||
categories: ['2016', '2017', '2018', '2019', '2020', '2021'],
|
||||
series: [
|
||||
{
|
||||
name: '目标值',
|
||||
data: [35, 36, 31, 33, 13, 34],
|
||||
}
|
||||
],
|
||||
})
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,193 @@
|
|||
<template>
|
||||
<view>
|
||||
<uv-sticky bgColor="#fff">
|
||||
<view class="flex-center h-44px">
|
||||
<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>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<view>
|
||||
<view>
|
||||
<StoreDropDown></StoreDropDown>
|
||||
</view>
|
||||
|
||||
<view class="bg-primary p-base text-center text-white relative">
|
||||
<view class="absolute top-20rpx right-20rpx">
|
||||
<uv-icon color="#fff" size="48rpx" name="chat"></uv-icon>
|
||||
</view>
|
||||
<view class="mt-60rpx">昨日累计金额</view>
|
||||
<view class="mt-20rpx">截止2024-03-21</view>
|
||||
<view class="flex items-center mt-40rpx">
|
||||
<view class="flex-1 text-center">
|
||||
<view>销售</view>
|
||||
<view>20000</view>
|
||||
</view>
|
||||
<view class="h-80rpx flex-none flex-center">
|
||||
<uv-line direction="vertical"></uv-line>
|
||||
</view>
|
||||
<view class="flex-1 text-center">
|
||||
<view>支出</view>
|
||||
<view>20000</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view>
|
||||
<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>
|
||||
<ChartComp></ChartComp>
|
||||
</view>
|
||||
</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([
|
||||
{
|
||||
name: '销售金额',
|
||||
},
|
||||
{
|
||||
name: '支出金额',
|
||||
},
|
||||
])
|
||||
|
||||
const onTabClick = (e) => {
|
||||
console.log(e)
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<view class="content">
|
||||
<uv-icon name="photo" size="30" color="#909399"></uv-icon>
|
||||
<uv-button type="primary" text="确定">我是按钮</uv-button>
|
||||
<image class="logo" src="/static/logo.png"></image>
|
||||
<view class="text-area">
|
||||
<text class="title">{{ title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
title: 'Hello',
|
||||
}
|
||||
},
|
||||
onLoad() {},
|
||||
methods: {},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 200rpx;
|
||||
width: 200rpx;
|
||||
margin-top: 200rpx;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 50rpx;
|
||||
}
|
||||
|
||||
.text-area {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
color: #8f8f94;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<view class="flex-center flex-col" @click="onClick">
|
||||
<uv-icon :name="data.icon" size="38" color="#909399"></uv-icon>
|
||||
<view class="text-28rpx mt-10rpx">{{ data.title }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
})
|
||||
|
||||
const onClick = () => {
|
||||
if (props.data.url) {
|
||||
uni.navigateTo({
|
||||
url: props.data.url,
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<view>
|
||||
<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>
|
||||
<view class="grid grid-cols-3 mt-20rpx gap-20rpx">
|
||||
<OpItem
|
||||
v-for="(child, ii) in item.children"
|
||||
:key="ii"
|
||||
:data="child"
|
||||
></OpItem>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import TitleComp from '@/components/title-comp/index'
|
||||
import OpItem from './components/op-item.vue'
|
||||
import CuNavbar from '@/components/cu-navbar/index'
|
||||
const opList = [
|
||||
{
|
||||
title: '门店数据',
|
||||
children: [
|
||||
{
|
||||
icon: 'order',
|
||||
title: '业绩数据',
|
||||
url: '/pages/data/performance/index',
|
||||
},
|
||||
{
|
||||
icon: 'folder',
|
||||
title: '提成数据',
|
||||
url: '/pages/data/brokerage/index',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '办公管理',
|
||||
children: [
|
||||
{
|
||||
icon: 'photo-fill',
|
||||
title: '员工管理',
|
||||
url: '/pages/user/index',
|
||||
},
|
||||
{
|
||||
icon: 'lock',
|
||||
title: '我的任务',
|
||||
url: '/pages/task/index',
|
||||
},
|
||||
{
|
||||
icon: 'home-fill',
|
||||
title: '报销管理',
|
||||
url: '/pages/expense-account/index',
|
||||
},
|
||||
{
|
||||
icon: 'map-fill',
|
||||
title: '升职申请',
|
||||
url: '/pages/work/list',
|
||||
},
|
||||
{
|
||||
icon: 'grid-fill',
|
||||
title: '补卡申请',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
icon: 'car',
|
||||
title: '请假申请',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
icon: 'setting-fill',
|
||||
title: '出差报备',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
icon: 'server-man',
|
||||
title: '加班报备',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
icon: 'camera',
|
||||
title: '合同管理',
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '天天向上',
|
||||
children: [
|
||||
{
|
||||
icon: 'account',
|
||||
title: '培训课件',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
icon: 'twitte',
|
||||
title: '培训考试',
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const goPath = (url) => {
|
||||
uni.navigateTo({
|
||||
url
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
@ -0,0 +1,396 @@
|
|||
<template>
|
||||
<view>
|
||||
<CuNavbar :isBack="isBack" title="上报"></CuNavbar>
|
||||
<view :class="[checkPermission(['store']) ? '' : 'pointer-events-none']">
|
||||
<uv-form
|
||||
class="mt-30rpx"
|
||||
labelPosition="left"
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
errorType="toast"
|
||||
labelWidth="130rpx"
|
||||
>
|
||||
<view class="px-base space-y-20rpx">
|
||||
<view class="card">
|
||||
<view class="pl-20rpx">
|
||||
<uv-form-item
|
||||
label="日期"
|
||||
prop="form.date"
|
||||
@click="showDateSelect"
|
||||
>
|
||||
<uv-input
|
||||
disabled
|
||||
v-model="form.date"
|
||||
disabledColor="#ffffff"
|
||||
placeholder="请选择日期"
|
||||
:border="`none`"
|
||||
>
|
||||
</uv-input>
|
||||
<template v-slot:right>
|
||||
<uv-icon name="arrow-right"></uv-icon>
|
||||
</template>
|
||||
</uv-form-item>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card" v-for="(item, i) in form.items" :key="i">
|
||||
<TitleComp :title="item.name"></TitleComp>
|
||||
<view class="mt-20rpx">
|
||||
<uv-line color="#f5f5f5"></uv-line>
|
||||
</view>
|
||||
<view class="pl-20rpx">
|
||||
<uv-form-item label="销售" :prop="`items.${i}.sales`">
|
||||
<uv-input
|
||||
@input="salesChange"
|
||||
v-model="item.sales"
|
||||
:border="`none`"
|
||||
type="digit"
|
||||
:placeholder="`请输入${item.name}销售金额`"
|
||||
></uv-input>
|
||||
</uv-form-item>
|
||||
<uv-line color="#f5f5f5"></uv-line>
|
||||
<uv-form-item label="兑奖" :prop="`items.${i}.expenditure`">
|
||||
<uv-input
|
||||
@input="expenditureChange"
|
||||
v-model="item.expenditure"
|
||||
:border="`none`"
|
||||
type="digit"
|
||||
:placeholder="`请输入${item.name}兑奖金额`"
|
||||
></uv-input>
|
||||
</uv-form-item>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card">
|
||||
<TitleComp title="汇总情况"></TitleComp>
|
||||
<view class="mt-20rpx">
|
||||
<uv-line color="#f5f5f5"></uv-line>
|
||||
</view>
|
||||
<view class="pl-20rpx">
|
||||
<uv-form-item label="销售合计" prop="sales">
|
||||
<uv-input
|
||||
:border="`none`"
|
||||
type="digit"
|
||||
v-model="form.sales"
|
||||
placeholder="请输入总账销售金额"
|
||||
></uv-input>
|
||||
</uv-form-item>
|
||||
<uv-line color="#f5f5f5"></uv-line>
|
||||
<uv-form-item label="兑奖合计" prop="expenditure">
|
||||
<uv-input
|
||||
type="digit"
|
||||
:border="`none`"
|
||||
v-model="form.expenditure"
|
||||
placeholder="请输入电总账兑奖金额"
|
||||
></uv-input>
|
||||
</uv-form-item>
|
||||
<uv-line color="#f5f5f5"></uv-line>
|
||||
<uv-form-item label="新增客户" prop="new_customers">
|
||||
<uv-input
|
||||
:border="`none`"
|
||||
v-model="form.new_customers"
|
||||
type="number"
|
||||
placeholder="请输入微信新增人数"
|
||||
></uv-input>
|
||||
</uv-form-item>
|
||||
<uv-line color="#f5f5f5"></uv-line>
|
||||
<uv-form-item label="交账金额" prop="handover_amount">
|
||||
<uv-input
|
||||
:border="`none`"
|
||||
type="digit"
|
||||
v-model="form.handover_amount"
|
||||
placeholder="请输入交账金额"
|
||||
></uv-input>
|
||||
</uv-form-item>
|
||||
<view class="text-primary text-24rpx py-10rpx"
|
||||
>* 请确保填写的交账金额正确无误!</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card">
|
||||
<TitleComp title="时段报表照片">
|
||||
<template #right>
|
||||
<view class="text-24rpx text-gray-400"
|
||||
>{{ form.photos.length }}/9</view
|
||||
>
|
||||
</template>
|
||||
</TitleComp>
|
||||
<uv-form-item label="" prop="photos">
|
||||
<view>
|
||||
<uv-upload
|
||||
width="200rpx"
|
||||
height="200rpx"
|
||||
:maxCount="9"
|
||||
multiple
|
||||
:fileList="form.photos"
|
||||
@afterRead="afterRead"
|
||||
@delete="deletePic"
|
||||
name="photos"
|
||||
></uv-upload>
|
||||
</view>
|
||||
</uv-form-item>
|
||||
<view class="text-primary text-24rpx py-10rpx">
|
||||
*竞彩时段报表照片,玩法时段报表照片,每日账本上传(需亲笔签字),销量本上传
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</uv-form>
|
||||
</view>
|
||||
<view class="h-130rpx">
|
||||
<view
|
||||
class="fixed bottom-0 left-0 right-0 h-120rpx bg-white flex-center box-border px-base"
|
||||
:style="style"
|
||||
>
|
||||
<view class="w-full">
|
||||
<uv-button type="primary" shape="circle" block @click="submit">
|
||||
上报
|
||||
</uv-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<uv-calendars
|
||||
color="#ee2c37"
|
||||
confirmColor="#ee2c37"
|
||||
ref="calendars"
|
||||
@confirm="calendarsConfirm"
|
||||
:endDate="endDate"
|
||||
:date="form.date"
|
||||
/>
|
||||
|
||||
<uv-modal
|
||||
ref="modalRef"
|
||||
title="提示"
|
||||
content="确定提交?"
|
||||
@confirm="onSubmit"
|
||||
:showCancelButton="true"
|
||||
></uv-modal>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { http } from '@/utils/request'
|
||||
import CuNavbar from '@/components/cu-navbar/index'
|
||||
import TitleComp from '@/components/title-comp/index'
|
||||
import { addUnit, sys } from '@climblee/uv-ui/libs/function/index'
|
||||
import { computed, ref, onBeforeMount, reactive, onMounted, watch } from 'vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { timeFormat } from '@climblee/uv-ui/libs/function/index'
|
||||
import { add } from '@/utils/index'
|
||||
import checkPermission from '@/utils/permission'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
const calendars = ref(null)
|
||||
const modalRef = ref(null)
|
||||
const formRef = ref(null)
|
||||
const userStore = useUserStore()
|
||||
|
||||
const userInfo = computed(() => userStore.userInfo || {})
|
||||
const store = computed(() => userInfo.value.store)
|
||||
const endDate = timeFormat(new Date(), 'yyyy-mm-dd')
|
||||
|
||||
const form = reactive({
|
||||
date: endDate,
|
||||
items: [],
|
||||
new_customers: '',
|
||||
sales: '',
|
||||
expenditure: '',
|
||||
handover_amount: '',
|
||||
photos: [],
|
||||
})
|
||||
const rules = reactive({
|
||||
date: {
|
||||
required: true,
|
||||
message: '请选择日期',
|
||||
trigger: ['change'],
|
||||
},
|
||||
handover_amount: {
|
||||
required: true,
|
||||
message: '请输入交账金额',
|
||||
trigger: ['change'],
|
||||
},
|
||||
photos: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
message: '请上传时段报表照片',
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
isBack: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
const style = computed(() => {
|
||||
const style = {}
|
||||
style.bottom = addUnit(sys().windowBottom, 'px')
|
||||
return style
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
getData(endDate)
|
||||
})
|
||||
|
||||
const submit = () => {
|
||||
formRef.value.validate().then((res) => {
|
||||
modalRef.value.open()
|
||||
})
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
const params = {
|
||||
date: form.date,
|
||||
sales: form.sales,
|
||||
expenditure: form.expenditure,
|
||||
handover_amount: form.handover_amount,
|
||||
new_customers: form.new_customers,
|
||||
photos: form.photos.map((item) => item.url),
|
||||
items: form.items,
|
||||
}
|
||||
http
|
||||
.request({
|
||||
url: '/ledgers',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
data: params,
|
||||
})
|
||||
.then((res) => {
|
||||
uni.$emit('revert:submit', res)
|
||||
uni.showToast({
|
||||
title: '提交成功',
|
||||
duration: 2000,
|
||||
icon: 'none',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const getData = async () => {
|
||||
const resData = await http.get(`/ledgers/${form.date}`)
|
||||
|
||||
Object.assign(form, resData, {
|
||||
photos:
|
||||
resData?.photos?.map((item) => {
|
||||
return { url: item }
|
||||
}) || [],
|
||||
})
|
||||
}
|
||||
|
||||
const getTypeList = () => {
|
||||
http
|
||||
.get('/keywords', {
|
||||
params: {
|
||||
parent_key: 'lottery_type',
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
form.items = res
|
||||
res.forEach((item, i) => {
|
||||
rules[`items.${i}.${'sales'}`] = {
|
||||
required: true,
|
||||
message: `请输入${item.name}销售金额`,
|
||||
}
|
||||
rules[`items.${i}.${'expenditure'}`] = {
|
||||
required: true,
|
||||
message: `请输入${item.name}兑奖金额`,
|
||||
}
|
||||
})
|
||||
console.log(rules)
|
||||
})
|
||||
}
|
||||
|
||||
const calendarsConfirm = (e) => {
|
||||
form.date = e.fulldate
|
||||
getData()
|
||||
}
|
||||
|
||||
const showDateSelect = () => {
|
||||
calendars.value.open()
|
||||
hideKeyboard()
|
||||
}
|
||||
|
||||
const hideKeyboard = () => {
|
||||
uni.hideKeyboard()
|
||||
}
|
||||
|
||||
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++) {
|
||||
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 uploadFilePromise = (url) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http
|
||||
.upload('/fileupload', {
|
||||
filePath: url,
|
||||
name: 'file',
|
||||
})
|
||||
.then((res) => {
|
||||
resolve(res.url)
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const deletePic = (event) => {
|
||||
form[event.name].splice(event.index, 1)
|
||||
}
|
||||
|
||||
const salesChange = () => {
|
||||
const val = form?.items || []
|
||||
const sales = val.reduce((a, b) => {
|
||||
return add(a, b?.sales ?? 0)
|
||||
}, 0)
|
||||
form.sales = sales || null
|
||||
}
|
||||
|
||||
const expenditureChange = () => {
|
||||
const val = form?.items || []
|
||||
const expenditure = val.reduce((a, b) => {
|
||||
return add(a, b?.expenditure ?? 0)
|
||||
}, 0)
|
||||
form.expenditure = expenditure || null
|
||||
}
|
||||
|
||||
watch(
|
||||
() => form.items,
|
||||
(val) => {
|
||||
const sales = val.reduce((a, b) => {
|
||||
return add(a, b?.sales ?? 0)
|
||||
}, 0)
|
||||
const expenditure = val.reduce((a, b) => {
|
||||
return add(a, b?.expenditure ?? 0)
|
||||
}, 0)
|
||||
// form.sales = sales || null
|
||||
// form.expenditure = expenditure || null
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
<template>
|
||||
<view>
|
||||
<CuNavbar title="数据报表"></CuNavbar>
|
||||
<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>
|
||||
</view>
|
||||
<view class="card">
|
||||
<uv-tabs
|
||||
:lineColor="'#ee2c37'"
|
||||
:list="tabsList"
|
||||
:scrollable="false"
|
||||
></uv-tabs>
|
||||
<view class="text-center text-28rpx"> 2024-03-21 至 2024-03-21 </view>
|
||||
<view class="flex my-20rpx items-center">
|
||||
<view class="text-center flex-1">
|
||||
<view>销售金额</view>
|
||||
<view class="font-600">991122</view>
|
||||
</view>
|
||||
<view class="h-60rpx">
|
||||
<uv-line direction="col"></uv-line>
|
||||
</view>
|
||||
<view class="text-center flex-1">
|
||||
<view>销售金额</view>
|
||||
<view class="font-600">991122</view>
|
||||
</view>
|
||||
<view class="h-60rpx">
|
||||
<uv-line direction="col"></uv-line>
|
||||
</view>
|
||||
<view class="text-center flex-1">
|
||||
<view>销售涨幅</view>
|
||||
<view class="text-primary font-600">+20%</view>
|
||||
</view>
|
||||
</view>
|
||||
<uv-tabs
|
||||
:lineColor="'#ee2c37'"
|
||||
:list="[{ name: '销售统计' }, { name: '门店统计' }]"
|
||||
:scrollable="false"
|
||||
></uv-tabs>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import CuNavbar from '@/components/cu-navbar/index'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
const tabsList = ref([
|
||||
{
|
||||
name: '昨日',
|
||||
},
|
||||
{
|
||||
name: '本周',
|
||||
},
|
||||
{
|
||||
name: '上周',
|
||||
},
|
||||
{
|
||||
name: '本月',
|
||||
},
|
||||
{
|
||||
name: '上月',
|
||||
},
|
||||
])
|
||||
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 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
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<view>1</view>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<view>1</view>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<view
|
||||
class="card-shadow bg-white rounded-19rpx p-base space-y-10rpx"
|
||||
@click.stop="onClick"
|
||||
>
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="text-30rpx">{{ item.name }}</view>
|
||||
<view
|
||||
:style="{
|
||||
color: statusFun(item.taskable.status, item.taskable_type, 'color'),
|
||||
}"
|
||||
class="text-24rpx"
|
||||
>{{ statusFun(item.taskable.status, item.taskable_type, 'name') }}</view
|
||||
>
|
||||
</view>
|
||||
<view class="text-24rpx text-hex-999999">
|
||||
任务时间:{{ timeFormat(item.start_at, 'yyyy年mm月dd日') }} -
|
||||
{{ timeFormat(item.end_at, 'yyyy年mm月dd日') }}
|
||||
</view>
|
||||
<template v-if="item.taskable.status == 2">
|
||||
<view class="py-10rpx">
|
||||
<uv-line></uv-line>
|
||||
</view>
|
||||
<view class="flex justify-end">
|
||||
<view @click.stop="onTask">
|
||||
<uv-button type="primary" size="mini">去完成</uv-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import statusFun from '@/utils/status'
|
||||
import { timeFormat } from '@climblee/uv-ui/libs/function/index'
|
||||
const props = defineProps({
|
||||
item: Object,
|
||||
})
|
||||
|
||||
const onClick = () => {
|
||||
const type = props.item.taskable_type
|
||||
console.log(type)
|
||||
}
|
||||
|
||||
const onTask = (e) => {
|
||||
const type = props.item.taskable_type
|
||||
uni.navigateTo({
|
||||
url: `/pages/task/${type}_submit?id=${props.item.id}`,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<view>
|
||||
<CuNavbar title="我的任务"></CuNavbar>
|
||||
<uv-sticky bgColor="#fff">
|
||||
<uv-tabs
|
||||
:activeStyle="{ color: '#ee2c37' }"
|
||||
:scrollable="false"
|
||||
lineColor="#ee2c37"
|
||||
:list="tabList"
|
||||
></uv-tabs>
|
||||
</uv-sticky>
|
||||
<mescroll-body @init="mescrollInit" @down="downCallback" @up="upCallback">
|
||||
<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>
|
||||
</mescroll-body>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import { http } from '@/utils/request'
|
||||
import CuNavbar from '@/components/cu-navbar/index'
|
||||
import { computed, reactive, 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 tabList = ref([
|
||||
{
|
||||
name: '任务列表',
|
||||
apiUrl: '/tasks',
|
||||
},
|
||||
{
|
||||
name: '任务审核',
|
||||
apiUrl: '/workflow',
|
||||
params:{
|
||||
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
const upCallback = async (mescroll) => {
|
||||
const { size, num } = mescroll
|
||||
|
||||
try {
|
||||
const resData = await http.get('/tasks', {
|
||||
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() // 请求失败, 结束加载
|
||||
} finally {
|
||||
// firstloading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<view>
|
||||
<CuNavbar title="我的任务"></CuNavbar>
|
||||
<uv-sticky bgColor="#fff">
|
||||
<uv-tabs
|
||||
height="44"
|
||||
:activeStyle="{ color: '#ee2c37' }"
|
||||
:scrollable="false"
|
||||
:current="tabIndex"
|
||||
lineColor="#ee2c37"
|
||||
:list="tabList"
|
||||
@change="tabChange"
|
||||
></uv-tabs>
|
||||
</uv-sticky>
|
||||
|
||||
<MescrollItem
|
||||
ref="mescrollItem0"
|
||||
:top="88"
|
||||
:i="0"
|
||||
:index="tabIndex"
|
||||
:apiUrl="tabList[0].apiUrl"
|
||||
>
|
||||
<template v-slot="{ list }">
|
||||
<view class="space-y-15rpx p-base">
|
||||
<view v-for="(item, i) in list" :key="i">
|
||||
<Item :item="item"></Item>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</MescrollItem>
|
||||
<MescrollItem
|
||||
ref="mescrollItem1"
|
||||
:i="1"
|
||||
:top="88"
|
||||
:index="tabIndex"
|
||||
:apiUrl="tabList[1].apiUrl"
|
||||
:params="tabList[1].params"
|
||||
></MescrollItem>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import CuNavbar from '@/components/cu-navbar/index'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import Item from './components/item.vue'
|
||||
import { onPageScroll, onReachBottom, onShow } from '@dcloudio/uni-app'
|
||||
import useMescrollMore from '@/uni_modules/mescroll-uni/hooks/useMescrollMore.js'
|
||||
import MescrollItem from '@/components/mescroll-api/more.vue'
|
||||
|
||||
const mescrollItem0 = ref(null)
|
||||
const mescrollItem1 = ref(null)
|
||||
|
||||
const mescrollItems = [mescrollItem0, mescrollItem1]
|
||||
const { tabIndex, getMescroll, scrollToLastY } = useMescrollMore(
|
||||
mescrollItems,
|
||||
onPageScroll,
|
||||
onReachBottom
|
||||
)
|
||||
const tabList = ref([
|
||||
{
|
||||
name: '任务列表',
|
||||
apiUrl: '/tasks',
|
||||
},
|
||||
{
|
||||
name: '任务审核',
|
||||
apiUrl: '/workflow',
|
||||
params: {
|
||||
// aaa:111
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const tabChange = ({ index }) => {
|
||||
tabIndex.value = index
|
||||
scrollToLastY()
|
||||
}
|
||||
</script>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
<template>
|
||||
<view class="p-base">
|
||||
<CuNavbar title="清洁任务提交"></CuNavbar>
|
||||
<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="description">
|
||||
<uv-input
|
||||
placeholder="请输入清洁范围"
|
||||
inputAlign="right"
|
||||
:border="`none`"
|
||||
v-model="form.description"
|
||||
>
|
||||
</uv-input>
|
||||
</uv-form-item>
|
||||
<uv-line color="#f5f5f5"></uv-line>
|
||||
<uv-form-item
|
||||
label="报销凭证"
|
||||
labelPosition="top"
|
||||
prop="photos"
|
||||
required
|
||||
>
|
||||
<view class="w-full mt-15rpx">
|
||||
<uv-upload
|
||||
:maxCount="9"
|
||||
multiple
|
||||
:fileList="form.photos"
|
||||
@afterRead="afterRead"
|
||||
@delete="deletePic"
|
||||
name="photos"
|
||||
></uv-upload>
|
||||
</view>
|
||||
</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 { http } from '@/utils/request'
|
||||
const formRef = ref(null)
|
||||
const modalRef = ref(null)
|
||||
const id = ref(0)
|
||||
const loading = ref(false)
|
||||
const form = reactive({
|
||||
description: '',
|
||||
photos: [],
|
||||
})
|
||||
const rules = reactive({
|
||||
description: [{ required: true, message: '请输入清洁范围' }],
|
||||
photos: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
message: '请上传报销凭证',
|
||||
},
|
||||
})
|
||||
onLoad((options) => {
|
||||
id.value = options.id
|
||||
})
|
||||
|
||||
const submit = () => {
|
||||
formRef.value.validate().then((res) => {
|
||||
modalRef.value.open()
|
||||
})
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
await http.request({
|
||||
url: `/tasks/${id.value}/submit`,
|
||||
method: 'POST',
|
||||
header: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
data: {
|
||||
task_hygiene: {
|
||||
description: form.description,
|
||||
photos: form.photos.map((item) => item.url),
|
||||
},
|
||||
},
|
||||
})
|
||||
uni.showToast({
|
||||
title: '提交成功',
|
||||
icon: 'none',
|
||||
})
|
||||
formRef.value.resetFields()
|
||||
uni.$emit('task:submit', resData)
|
||||
uni.navigateBack()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
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++) {
|
||||
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 uploadFilePromise = (url) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http
|
||||
.upload('/fileupload', {
|
||||
filePath: url,
|
||||
name: 'file',
|
||||
})
|
||||
.then((res) => {
|
||||
resolve(res.url)
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const deletePic = (event) => {
|
||||
form[event.name].splice(event.index, 1)
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<template>
|
||||
<view>
|
||||
<RevertPage :isBack="true"></RevertPage>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import RevertPage from "@/pages/revert/index";
|
||||
</script>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<view>
|
||||
<CuNavbar title="上传图片"></CuNavbar>
|
||||
<uv-upload></uv-upload>
|
||||
</view>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<view class="flex rounded-19rpx box-content card" @click="goPage(`/pages/user/detail?id=${item.id}`)">
|
||||
<view class="rounded-full w-100rpx h-100rpx overflow-hidden">
|
||||
<image class="w-full h-full" :src="item.avatar"></image>
|
||||
</view>
|
||||
<view class="flex-1 ml-20rpx flex flex-col justify-between">
|
||||
<view class="flex 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="text-24rpx text-hex-999">
|
||||
<view class="">电话:{{ item.phone }}</view>
|
||||
<view class="" v-if="item.store">门店:{{ item.store.address }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
})
|
||||
const goPage = (url) => {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
<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>
|
||||
|
||||
<uv-modal
|
||||
ref="modalRef"
|
||||
:title="modalOptin.title"
|
||||
:content="modalOptin.content"
|
||||
@confirm="modalOptin.confirm"
|
||||
:showCancelButton="true"
|
||||
></uv-modal>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import CuNavbar from '@/components/cu-navbar/index'
|
||||
import { http } from '@/utils/request'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
const modalRef = ref(null)
|
||||
const modalOptin = reactive({
|
||||
title: '提示',
|
||||
content: '',
|
||||
confirm: () => {},
|
||||
})
|
||||
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) => {
|
||||
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}`,
|
||||
})
|
||||
} else if (value === 'quit') {
|
||||
modalOptin.content = '是否确认离职该员工?'
|
||||
modalOptin.confirm = onQuick
|
||||
modalRef.value.open()
|
||||
} else if (value === 'delete') {
|
||||
modalOptin.content = '是否确认删除该员工?'
|
||||
modalOptin.confirm = onDelete
|
||||
modalRef.value.open()
|
||||
}
|
||||
}
|
||||
const onDelete = () => {
|
||||
http.delete(`/hr/employee/${id.value}`).then((res) => {
|
||||
uni.$emit('user:onRefresh')
|
||||
uni.showToast({
|
||||
title: '删除成功',
|
||||
duration: 2000,
|
||||
icon: 'none',
|
||||
})
|
||||
uni.navigateBack()
|
||||
})
|
||||
}
|
||||
const onQuick = () => {
|
||||
http.post(`/hr/employee/${id.value}/leave`).then((res) => {
|
||||
uni.showToast({
|
||||
title: '离职成功',
|
||||
duration: 2000,
|
||||
icon: 'none',
|
||||
})
|
||||
uni.$emit('user:onRefresh')
|
||||
uni.navigateBack()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
<template>
|
||||
<view>
|
||||
<CuNavbar title="员工管理">
|
||||
<template #right>
|
||||
<text @click="goPage('/pages/user/update')" class="text-white"
|
||||
>添加</text
|
||||
>
|
||||
</template>
|
||||
</CuNavbar>
|
||||
<StoreDropDown></StoreDropDown>
|
||||
|
||||
<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
|
||||
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>
|
||||
</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'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
const { mescrollInit, downCallback, getMescroll } = useMescroll(
|
||||
onPageScroll,
|
||||
onReachBottom
|
||||
)
|
||||
const firstloading = ref(true)
|
||||
const list = ref([])
|
||||
const options = [
|
||||
{
|
||||
text: '删除',
|
||||
style: {
|
||||
backgroundColor: '#f56c6c',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '修改',
|
||||
style: {
|
||||
backgroundColor: '#3c9cff',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '离职',
|
||||
style: {
|
||||
backgroundColor: '#f9ae3d',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
onShow(() => {
|
||||
if (!firstloading.value) {
|
||||
getMescroll().resetUpScroll()
|
||||
}
|
||||
})
|
||||
|
||||
const upCallback = async (mescroll) => {
|
||||
const { size, num } = mescroll
|
||||
|
||||
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() // 请求失败, 结束加载
|
||||
} finally {
|
||||
firstloading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const goPage = (url) => {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<view>
|
||||
<CuNavbar title="员工管理">
|
||||
<template #right>
|
||||
<text @click="goPage('/pages/user/update')" class="text-white"
|
||||
>添加</text
|
||||
>
|
||||
</template>
|
||||
</CuNavbar>
|
||||
<StoreDropDown></StoreDropDown>
|
||||
<MescrollApiOne
|
||||
:top="88"
|
||||
ref="mescrollItem"
|
||||
apiUrl="/hr/employee"
|
||||
:params="params"
|
||||
>
|
||||
<template v-slot="{ list }">
|
||||
<view class="space-y-15rpx p-base">
|
||||
<view v-for="(item, i) in list" :key="i">
|
||||
<Item :item="item"></Item>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</MescrollApiOne>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import CuNavbar from '@/components/cu-navbar/index'
|
||||
import StoreDropDown from '@/pages/home/components/store-drop-down/index.vue'
|
||||
import Item from './components/item.vue'
|
||||
import MescrollApiOne from '@/components/mescroll-api/one'
|
||||
import { onPageScroll, onReachBottom, onLoad } from '@dcloudio/uni-app'
|
||||
import useMescrollComp from '@/uni_modules/mescroll-uni/hooks/useMescrollComp.js'
|
||||
const { mescrollItem } = useMescrollComp(onPageScroll, onReachBottom)
|
||||
const params = ref(null)
|
||||
|
||||
onLoad(()=>{
|
||||
uni.$on('user:onRefresh',onRefresh)
|
||||
})
|
||||
|
||||
const onRefresh = () => {
|
||||
// params.value = {
|
||||
// city_code: 11,
|
||||
// store_id: 12,
|
||||
// }
|
||||
mescrollItem.value.getMescroll().resetUpScroll()
|
||||
}
|
||||
const goPage = (url) => {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
<template>
|
||||
<view class="p-base">
|
||||
<CuNavbar :title="title">
|
||||
<!-- <template #right>
|
||||
<view class="text-white">保存</view>
|
||||
</template> -->
|
||||
</CuNavbar>
|
||||
<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="请输入姓名"
|
||||
inputAlign="right"
|
||||
:border="`none`"
|
||||
v-model="form.name"
|
||||
>
|
||||
</uv-input>
|
||||
</uv-form-item>
|
||||
<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="username">
|
||||
<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"
|
||||
type="password"
|
||||
v-model="form.password"
|
||||
:border="`none`"
|
||||
>
|
||||
</uv-input>
|
||||
</uv-form-item>
|
||||
<uv-form-item :required="!isEdit" label="门店" prop="password">
|
||||
|
||||
</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
|
||||
if(isEdit.value) 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.$emit('user:onRefresh')
|
||||
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>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<view class="card-shadow bg-white rounded-19rpx p-base space-y-10rpx">
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="text-30rpx"> {{ item.job.name }} </view>
|
||||
<view class="text-24rpx text-hex-999999">{{item.promotion_status}}</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>
|
||||
</template>
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
item: Object,
|
||||
})
|
||||
</script>
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
<template>
|
||||
<view>
|
||||
<CuNavbar title="升值申请">
|
||||
<template #right>
|
||||
<view class="text-24rpx text-white" @confirm="onSubmit">保存</view>
|
||||
</template>
|
||||
</CuNavbar>
|
||||
<uv-sticky bgColor="white">
|
||||
<view class="p-40rpx">
|
||||
<view class="flex justify-between px-10rpx">
|
||||
<view>晋升职位</view>
|
||||
<view>店长</view>
|
||||
</view>
|
||||
<view class="flex justify-between mt-15rpx px-10rpx">
|
||||
<view>申请人</view>
|
||||
<view>李四</view>
|
||||
</view>
|
||||
<view class="flex justify-between mt-15rpx px-10rpx">
|
||||
<view>推荐人</view>
|
||||
<view>丁曼容</view>
|
||||
</view>
|
||||
</view>
|
||||
</uv-sticky>
|
||||
<view class="card-shadow px-base">
|
||||
<uv-form labelPosition="left" :model="form" :rules="rules" ref="formRef" errorType="toast" labelWidth="250rpx">
|
||||
<uv-form-item required label="年龄" prop="age">
|
||||
<uv-input placeholder="请输入年龄" inputAlign="right" :border="`none`" v-model="form.age"> </uv-input>
|
||||
</uv-form-item>
|
||||
<uv-line color="#f5f5f5"></uv-line>
|
||||
<uv-form-item required label="性别" prop="sex">
|
||||
<uv-input
|
||||
placeholder="请选择性别"
|
||||
@click="openPicker"
|
||||
readonly
|
||||
inputAlign="right"
|
||||
:border="`none`"
|
||||
v-model="form.sex"
|
||||
>
|
||||
</uv-input>
|
||||
</uv-form-item>
|
||||
<uv-line color="#f5f5f5"></uv-line>
|
||||
<uv-form-item required label="学历" prop="education">
|
||||
<uv-input placeholder="请输入学历" inputAlign="right" :border="`none`" v-model="form.education"> </uv-input>
|
||||
</uv-form-item>
|
||||
<uv-line color="#f5f5f5"></uv-line>
|
||||
<uv-form-item required label="首次参加工作时间" prop="first_work_time">
|
||||
<uv-input
|
||||
placeholder="请选择日期"
|
||||
readonly
|
||||
@click="openDatePicker"
|
||||
inputAlign="right"
|
||||
:border="`none`"
|
||||
v-model="form.first_work_time"
|
||||
>
|
||||
</uv-input>
|
||||
</uv-form-item>
|
||||
<uv-line color="#f5f5f5"></uv-line>
|
||||
<uv-form-item required label="工作年限" prop="work_years">
|
||||
<uv-input placeholder="请输入工作年限" inputAlign="right" :border="`none`" v-model="form.work_years">
|
||||
</uv-input>
|
||||
</uv-form-item>
|
||||
<uv-line color="#f5f5f5"></uv-line>
|
||||
<uv-form-item required label="本公司工作年限" prop="work_years_in_company">
|
||||
<uv-input
|
||||
placeholder="请输入本公司工作年限"
|
||||
inputAlign="right"
|
||||
:border="`none`"
|
||||
v-model="form.work_years_in_company"
|
||||
>
|
||||
</uv-input>
|
||||
</uv-form-item>
|
||||
<uv-line color="#f5f5f5"></uv-line>
|
||||
<uv-form-item required label="员工自评" prop="comment_self" labelPosition="top">
|
||||
<uv-textarea
|
||||
v-model="form.comment_self"
|
||||
count
|
||||
placeholder="请输入员工自评"
|
||||
:border="`none`"
|
||||
:maxlength="200"
|
||||
></uv-textarea>
|
||||
</uv-form-item>
|
||||
<uv-line color="#f5f5f5"></uv-line>
|
||||
<uv-form-item required label="未来计划" prop="plans" labelPosition="top">
|
||||
<uv-textarea
|
||||
v-model="form.plans"
|
||||
count
|
||||
placeholder="请输入未来计划"
|
||||
:border="`none`"
|
||||
:maxlength="200"
|
||||
></uv-textarea>
|
||||
</uv-form-item>
|
||||
<uv-line color="#f5f5f5"></uv-line>
|
||||
<uv-form-item label="推荐理由" prop="reason" labelPosition="top">
|
||||
<uv-textarea
|
||||
v-model="form.reason"
|
||||
count
|
||||
placeholder="请输入推荐理由"
|
||||
:border="`none`"
|
||||
:maxlength="200"
|
||||
></uv-textarea>
|
||||
</uv-form-item>
|
||||
</uv-form>
|
||||
</view>
|
||||
<!-- <view class="mt-100rpx">
|
||||
<uv-button type="primary" @click="submit">提交</uv-button>
|
||||
</view> -->
|
||||
<uv-picker ref="pickerRef" :columns="columns" @confirm="confirmPicker"></uv-picker>
|
||||
<uv-datetime-picker placeholder="请选择日期" ref="datetimePicker" mode="year-month" @confirm="confirmDatePicker">
|
||||
</uv-datetime-picker>
|
||||
<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 { http } from "@/utils/request"
|
||||
import { timeFormat } from "@climblee/uv-ui/libs/function/index"
|
||||
const columns = [["男", "女"]]
|
||||
const formRef = ref(null)
|
||||
const datetimePicker = ref(null)
|
||||
const pickerRef = ref(null)
|
||||
const modalRef = ref(null)
|
||||
const id = ref(0)
|
||||
const loading = ref(false)
|
||||
const form = reactive({
|
||||
age: "",
|
||||
sex: "",
|
||||
education: "",
|
||||
first_work_time: "",
|
||||
work_years: "",
|
||||
work_years_in_company: "",
|
||||
comment_self: "",
|
||||
plans: "",
|
||||
reason: ""
|
||||
})
|
||||
const openPicker = () => {
|
||||
pickerRef.value.open()
|
||||
}
|
||||
const openDatePicker = () => {
|
||||
datetimePicker.value.open()
|
||||
}
|
||||
const confirmDatePicker = e => {
|
||||
form.first_work_time = timeFormat(e.value, "yyyy-mm")
|
||||
}
|
||||
const confirmPicker = e => {
|
||||
form.sex = e.value[0]
|
||||
}
|
||||
const rules = reactive({
|
||||
age: [{ required: true, message: "请输入年龄" }],
|
||||
sex: [{ required: true, message: "请选择性别" }],
|
||||
education: [{ required: true, message: "请输入学历" }],
|
||||
first_work_time: [{ required: true, message: "请输入首次参加工作时间" }],
|
||||
work_years: [{ required: true, message: "请输入清洁范围" }],
|
||||
work_years_in_company: [{ required: true, message: "请输入本公司工作年限" }],
|
||||
comment_self: [{ required: true, message: "请输入员工自评" }],
|
||||
plans: [{ required: true, message: "请输入未来计划" }],
|
||||
reason: [{ required: true, message: "请输入推荐理由" }]
|
||||
})
|
||||
onLoad(options => {
|
||||
id.value = options.id
|
||||
})
|
||||
|
||||
const submit = () => {
|
||||
formRef.value.validate().then(res => {
|
||||
modalRef.value.open()
|
||||
})
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
await http.request({
|
||||
url: `/tasks/${id.value}/submit`,
|
||||
method: "POST",
|
||||
header: {
|
||||
Accept: "application/json"
|
||||
},
|
||||
data: {
|
||||
task_hygiene: {
|
||||
description: form.description,
|
||||
photos: form.photos.map(item => item.url)
|
||||
}
|
||||
}
|
||||
})
|
||||
uni.showToast({
|
||||
title: "提交成功",
|
||||
icon: "none"
|
||||
})
|
||||
formRef.value.resetFields()
|
||||
uni.$emit("task:submit", resData)
|
||||
uni.navigateBack()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<view>
|
||||
<CuNavbar title="升职申请">
|
||||
<template #right>
|
||||
<view @click="goPath('/pages/work/create')" class="text-24rpx text-white">申请</view>
|
||||
</template>
|
||||
</CuNavbar>
|
||||
<uv-sticky bgColor="#fff">
|
||||
<uv-tabs
|
||||
:activeStyle="{ color: '#ee2c37' }"
|
||||
:scrollable="false"
|
||||
lineColor="#ee2c37"
|
||||
:list="tabList"
|
||||
@change="tabChange"
|
||||
></uv-tabs>
|
||||
</uv-sticky>
|
||||
<MescrollItem ref="mescrollItem0" :top="88" :i="0" :index="tabIndex" :apiUrl="tabList[0].apiUrl">
|
||||
<template v-slot="{ list }">
|
||||
<view class="space-y-15rpx p-base">
|
||||
<view v-for="(item, i) in list" :key="i">
|
||||
<Item :item="item"></Item>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</MescrollItem>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import CuNavbar from "@/components/cu-navbar/index"
|
||||
import { ref } from "vue"
|
||||
import { onPageScroll, onReachBottom, onShow } from "@dcloudio/uni-app"
|
||||
import useMescrollMore from "@/uni_modules/mescroll-uni/hooks/useMescrollMore.js"
|
||||
import MescrollItem from "@/components/mescroll-api/more.vue"
|
||||
import Item from './components/item.vue'
|
||||
const tabList = ref([
|
||||
{
|
||||
name: "申请列表",
|
||||
apiUrl: "/hr/promotion/apply"
|
||||
},
|
||||
{
|
||||
name: "推荐列表"
|
||||
},
|
||||
{
|
||||
name: "审核列表"
|
||||
}
|
||||
])
|
||||
const mescrollItem0 = ref(null)
|
||||
const mescrollItem1 = ref(null)
|
||||
|
||||
const mescrollItems = [mescrollItem0, mescrollItem1]
|
||||
const { tabIndex, getMescroll, scrollToLastY } = useMescrollMore(mescrollItems, onPageScroll, onReachBottom)
|
||||
const goPath = url => {
|
||||
uni.navigateTo({
|
||||
url
|
||||
})
|
||||
}
|
||||
const tabChange = ({ index }) => {
|
||||
tabIndex.value = index
|
||||
scrollToLastY()
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export {};
|
||||
|
||||
declare module "vue" {
|
||||
type Hooks = App.AppInstance & Page.PageInstance;
|
||||
interface ComponentCustomOptions extends Hooks {}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
page {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #ffffff;
|
||||
box-shadow: 0px 0px 16px 0px rgba(0, 0, 0, 0.04);
|
||||
border-radius: 10px;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.card-shadow{
|
||||
background: #ffffff;
|
||||
box-shadow: 0px 0px 16px 0px rgba(0, 0, 0, 0.04);
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
// 此文件为uvUI的主题变量,这些变量目前只能通过uni.scss引入才有效,另外由于
|
||||
// uni.scss中引入的样式会同时混入到全局样式文件和单独每一个页面的样式中,造成微信程序包太大,
|
||||
// 故uni.scss只建议放scss变量名相关样式,其他的样式可以通过main.js或者App.vue引入
|
||||
|
||||
$uv-main-color: #303133;
|
||||
$uv-content-color: #606266;
|
||||
$uv-tips-color: #909193;
|
||||
$uv-light-color: #c0c4cc;
|
||||
$uv-border-color: #dadbde;
|
||||
$uv-bg-color: #f3f4f6;
|
||||
$uv-disabled-color: #c8c9cc;
|
||||
|
||||
$uv-primary: #ee2c37;
|
||||
$uv-primary-dark: #398ade;
|
||||
$uv-primary-disabled: #9acafc;
|
||||
$uv-primary-light: #ecf5ff;
|
||||
|
||||
$uv-warning: #f9ae3d;
|
||||
$uv-warning-dark: #f1a532;
|
||||
$uv-warning-disabled: #f9d39b;
|
||||
$uv-warning-light: #fdf6ec;
|
||||
|
||||
$uv-success: #5ac725;
|
||||
$uv-success-dark: #53c21d;
|
||||
$uv-success-disabled: #a9e08f;
|
||||
$uv-success-light: #f5fff0;
|
||||
|
||||
$uv-error: #f56c6c;
|
||||
$uv-error-dark: #e45656;
|
||||
$uv-error-disabled: #f7b2b2;
|
||||
$uv-error-light: #fef0f0;
|
||||
|
||||
$uv-info: #909399;
|
||||
$uv-info-dark: #767a82;
|
||||
$uv-info-disabled: #c4c6c9;
|
||||
$uv-info-light: #f4f4f5;
|
||||
|
||||
@mixin flex($direction: row) {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: $direction;
|
||||
}
|
||||
|
||||
.uv-navbar--fixed{
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
|
|
@ -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="#000000" 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="#333333" 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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
import { createPinia } from 'pinia';
|
||||
import { registerPiniaPersistPlugin } from '@/store/plugin/persist';
|
||||
|
||||
const store = createPinia();
|
||||
registerPiniaPersistPlugin(store);
|
||||
|
||||
export function setupStore(app) {
|
||||
app.use(store);
|
||||
}
|
||||
|
||||
export { store };
|
||||
|
|
@ -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']
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { store } from '@/store';
|
||||
import { http } from "@/utils/request";
|
||||
|
||||
export const useUserStore = defineStore({
|
||||
id: 'app-user',
|
||||
state: () => ({
|
||||
userInfo: null,
|
||||
token: '22|ua3STGluZFzDeV1nOs4I3S9s2UCDtQp9AtVLT2jN0535e0a5',
|
||||
roles: []
|
||||
}),
|
||||
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
|
||||
this.roles = res.role
|
||||
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);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
import { createPersistedState } from 'pinia-plugin-persistedstate';
|
||||
export const PERSIST_KEY_PREFIX = 'lottery';
|
||||
|
||||
export function registerPiniaPersistPlugin(pinia) {
|
||||
pinia.use(createPersistedState(createPersistedStateOptions(PERSIST_KEY_PREFIX)));
|
||||
}
|
||||
|
||||
|
||||
export function createPersistedStateOptions(keyPrefix) {
|
||||
return {
|
||||
storage: {
|
||||
getItem(key) {
|
||||
return uni.getStorageSync(key);
|
||||
},
|
||||
setItem(key, value) {
|
||||
uni.setStorageSync(key, value);
|
||||
}
|
||||
},
|
||||
key: (id) => `${keyPrefix}__${id}`
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* 这里是uni-app内置的常用样式变量
|
||||
*
|
||||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||
*
|
||||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||
*/
|
||||
@import '@/static/css/uvui.scss';
|
||||
|
||||
/* 颜色变量 */
|
||||
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #007aff;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
|
||||
/* 文字基本颜色 */
|
||||
$uni-text-color: #333; // 基本色
|
||||
$uni-text-color-inverse: #fff; // 反色
|
||||
$uni-text-color-grey: #999; // 辅助灰色,如加载更多的提示信息
|
||||
$uni-text-color-placeholder: #808080;
|
||||
$uni-text-color-disable: #c0c0c0;
|
||||
|
||||
/* 背景颜色 */
|
||||
$uni-bg-color: #fff;
|
||||
$uni-bg-color-grey: #f8f8f8;
|
||||
$uni-bg-color-hover: #f1f1f1; // 点击状态颜色
|
||||
$uni-bg-color-mask: rgba(0, 0, 0, 0.4); // 遮罩颜色
|
||||
|
||||
/* 边框颜色 */
|
||||
$uni-border-color: #c8c7cc;
|
||||
|
||||
/* 尺寸变量 */
|
||||
|
||||
/* 文字尺寸 */
|
||||
$uni-font-size-sm: 12px;
|
||||
$uni-font-size-base: 14px;
|
||||
$uni-font-size-lg: 16;
|
||||
|
||||
/* 图片尺寸 */
|
||||
$uni-img-size-sm: 20px;
|
||||
$uni-img-size-base: 26px;
|
||||
$uni-img-size-lg: 40px;
|
||||
|
||||
/* Border Radius */
|
||||
$uni-border-radius-sm: 2px;
|
||||
$uni-border-radius-base: 3px;
|
||||
$uni-border-radius-lg: 6px;
|
||||
$uni-border-radius-circle: 50%;
|
||||
|
||||
/* 水平间距 */
|
||||
$uni-spacing-row-sm: 5px;
|
||||
$uni-spacing-row-base: 10px;
|
||||
$uni-spacing-row-lg: 15px;
|
||||
|
||||
/* 垂直间距 */
|
||||
$uni-spacing-col-sm: 4px;
|
||||
$uni-spacing-col-base: 8px;
|
||||
$uni-spacing-col-lg: 12px;
|
||||
|
||||
/* 透明度 */
|
||||
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||
|
||||
/* 文章场景相关 */
|
||||
$uni-color-title: #2c405a; // 文章标题颜色
|
||||
$uni-font-size-title: 20px;
|
||||
$uni-color-subtitle: #555; // 二级标题颜色
|
||||
$uni-font-size-subtitle: 18px;
|
||||
$uni-color-paragraph: #3f536e; // 文章段落颜色
|
||||
$uni-font-size-paragraph: 15px;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
## 1.3.8(2023-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 小瑾同学
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示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失效,需注意把fixed元素写在mescroll之外
|
||||
},
|
||||
// 是否在加载中
|
||||
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%"则等于windowHeight的10%
|
||||
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); // 派发点击回到顶部按钮的回调
|
||||
}
|
||||
},
|
||||
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
|
||||
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的状态,避免下次inOffset不及时显示textInOffset
|
||||
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;
|
||||
// init回调mescroll对象
|
||||
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;
|
||||
// 使down的bottomOffset生效
|
||||
vm.mescroll.setBodyHeight(sys.windowHeight);
|
||||
|
||||
// 因为使用的是page的scroll,这里需自定义scrollTo
|
||||
vm.mescroll.resetScrollTo((y, t) => {
|
||||
if(typeof y === 'string'){
|
||||
// 滚动到指定view (y为css选择器)
|
||||
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,则取本vue的safearea值
|
||||
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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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, // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
|
||||
rate: Number // 下拉比率 (inOffset: rate<1; outOffset: rate>=1)
|
||||
},
|
||||
computed: {
|
||||
// 支付宝小程序需写成计算属性,prop定义default仍报错
|
||||
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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 // 上拉加载的状态:0(loading前),1(loading中),2(没有更多了)
|
||||
},
|
||||
computed: {
|
||||
// 支付宝小程序需写成计算属性,prop定义default仍报错
|
||||
mOption() {
|
||||
return this.option || {};
|
||||
},
|
||||
// 加载中
|
||||
isUpLoading() {
|
||||
return this.type === 1;
|
||||
},
|
||||
// 没有更多了
|
||||
isUpNoMore() {
|
||||
return this.type === 2;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import './mescroll-up.css';
|
||||
</style>
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
|
||||
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失效,需注意把fixed元素写在mescroll之外
|
||||
},
|
||||
// 列表是否可滑动
|
||||
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%"则等于windowHeight的10%
|
||||
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();
|
||||
}
|
||||
},
|
||||
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
|
||||
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; // 挂载语言包
|
||||
// init回调mescroll对象
|
||||
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;
|
||||
// 使down的bottomOffset生效
|
||||
vm.mescroll.setBodyHeight(sys.windowHeight);
|
||||
|
||||
// 因为使用的是scrollview,这里需自定义scrollTo
|
||||
vm.mescroll.resetScrollTo((y, t) => {
|
||||
vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡
|
||||
if(typeof y === 'string'){
|
||||
// 小程序不支持slot里面的scroll-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,则取本vue的safearea值
|
||||
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>
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue