【招生小程序】 新增# 主账号:工作台页面
parent
a4930290b5
commit
839a51027c
|
|
@ -0,0 +1,101 @@
|
|||
<template>
|
||||
<w-card @click="handleDetail">
|
||||
<template #title>
|
||||
<view class="flex justify-between w-full py-[10rpx]">
|
||||
<view class="flex">
|
||||
<text>{{ item.studentName }}</text>
|
||||
<view class="flex ml-[48rpx] gap-[4rpx] items-center">
|
||||
<text class="text-primary">{{ item.phone }}</text>
|
||||
<u-copy :content="item.phone">
|
||||
<TIcon name="icon-copy" color="#0E66FB" />
|
||||
</u-copy>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template #content>
|
||||
<view class="flex gap-[20rpx] mb-[16rpx]">
|
||||
<text class="text-muted w-[128rpx]">基本情况</text>
|
||||
<text class="flex-1">
|
||||
{{ ellipsisDesc(item) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex gap-[20rpx] mb-[16rpx]">
|
||||
<text class="text-muted w-[128rpx]">跟进时间</text>
|
||||
<text class="flex-1">{{ item.createTime }}</text>
|
||||
</view>
|
||||
<view class="flex gap-[20rpx] mb-[16rpx]">
|
||||
<text class="text-muted w-[128rpx]">招生老师</text>
|
||||
<text class="flex-1">{{ item.recruitTeacherName }}</text>
|
||||
</view>
|
||||
<view class="flex gap-[20rpx] mb-[16rpx]">
|
||||
<text class="text-muted w-[128rpx]">领取时间</text>
|
||||
<text class="flex-1">{{ item.createTime }}</text>
|
||||
</view>
|
||||
<view class="flex gap-[20rpx]">
|
||||
<text class="text-muted w-[128rpx]">状态</text>
|
||||
<text class="flex-1 text-orage">{{ parseStateText }}</text>
|
||||
</view>
|
||||
<view class="flex gap-[20rpx]">
|
||||
<text class="text-muted w-[128rpx]">备注</text>
|
||||
<view class="flex gap-[12rpx] flex-1">
|
||||
<text class="text-error">{{ item.remark }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex gap-[20rpx]">
|
||||
<text class="text-muted w-[128rpx]">成交时间</text>
|
||||
<text class="flex-1">{{ item.updateTime }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</w-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, PropType } from 'vue'
|
||||
import { converStatusEnum, stateEnum } from '@/enums'
|
||||
|
||||
export interface IClue {
|
||||
studentName: string
|
||||
phone: string
|
||||
id: number
|
||||
basicInformation: string
|
||||
remark: string
|
||||
isConversion: number
|
||||
listSource: number //线索来源
|
||||
state: stateEnum //状态
|
||||
situation: number // 转化情况
|
||||
createTime: string // 跟进时间
|
||||
telemarketingTeacherName: string // 电销老师
|
||||
recruitTeacherName: string // 招生老师
|
||||
recruitTeacherId: number
|
||||
telemarketingTeacherId: number
|
||||
updateTime: string
|
||||
}
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object as PropType<IClue>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
const ellipsisDesc = computed(
|
||||
() => (item: IClue) =>
|
||||
item.basicInformation?.length >= 14
|
||||
? item.basicInformation?.slice(0, 14) + '...'
|
||||
: item.basicInformation
|
||||
)
|
||||
const stateMap: Record<stateEnum, string> = {
|
||||
[stateEnum.ADD_RELATION]: '账号已添加',
|
||||
[stateEnum.NO_EXIST]: '账号不存在',
|
||||
[stateEnum.UN_PASS]: '账号未通过'
|
||||
}
|
||||
const parseStateText = computed(() => stateMap[props.item.state])
|
||||
|
||||
// 线索详情
|
||||
const handleDetail = () => {
|
||||
const { item } = props
|
||||
uni.navigateTo({
|
||||
url: '/bundle/pages/clue/detail?id=' + item.id
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<template>
|
||||
<view class="flex flex-col h-screen">
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex-1 overflow-auto">
|
||||
<u-tabs
|
||||
:list="tabs"
|
||||
:itemStyle="{ height: '44px', flex: 1 }"
|
||||
:activeStyle="{ color: '#0E66FB' }"
|
||||
lineWidth="32"
|
||||
lineColor="#0E66FB"
|
||||
:scrollable="isScrollable"
|
||||
@change="handleChangeTab"
|
||||
></u-tabs>
|
||||
</view>
|
||||
<view class="mr-[32rpx]">
|
||||
<TIcon name="icon-filter" color="#999" />
|
||||
</view>
|
||||
</view>
|
||||
<TSearch
|
||||
v-model="queryParams.likeWork"
|
||||
placeholder="搜索客户姓名/手机号码"
|
||||
backgroundColor="#F5F5F5"
|
||||
@on-input="searchChange"
|
||||
/>
|
||||
<view class="flex-1 px-[32rpx] pt-[24rpx] overflow-auto bg-[#FAFAFE]">
|
||||
<z-paging
|
||||
ref="paging"
|
||||
v-model="dataList"
|
||||
@query="queryList"
|
||||
:fixed="false"
|
||||
height="100%"
|
||||
>
|
||||
<clue-card
|
||||
v-for="(item, index) in dataList"
|
||||
:key="`${index} + 'unique'`"
|
||||
:item="item"
|
||||
/>
|
||||
</z-paging>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { ref, shallowRef } from 'vue'
|
||||
import clueCard from './components/clue-card.vue'
|
||||
import { useZPaging } from '@/hooks/useZPaging'
|
||||
import { apiTeleClueList } from '@/api/clue'
|
||||
import { computed } from 'vue'
|
||||
import { debounce } from 'lodash-es'
|
||||
|
||||
const queryParams = ref({
|
||||
likeWork: ''
|
||||
})
|
||||
const dataList = ref([])
|
||||
const { paging, queryList, refresh, changeApi, setParams } = useZPaging(
|
||||
queryParams.value,
|
||||
apiTeleClueList
|
||||
)
|
||||
const activeTab = ref(0)
|
||||
const tabOptions: Record<string, any[]> = {
|
||||
telesale: [
|
||||
{ name: '全部', value: 0 },
|
||||
{ name: '未领取', value: 1 },
|
||||
{ name: '已领取', value: 2 },
|
||||
{ name: '异常待处理', value: 3 }
|
||||
],
|
||||
recruitsale: [
|
||||
{ name: '转化中', value: 0 },
|
||||
{ name: '已添加', value: 1 },
|
||||
{ name: '异常待处理', value: 2 },
|
||||
{ name: '已成交', value: 3 },
|
||||
{ name: '已战败', value: 4 }
|
||||
]
|
||||
}
|
||||
const tabs = computed(() => tabOptions[type.value])
|
||||
const isScrollable = computed(() => tabs.value?.length >= 5)
|
||||
const handleChangeTab = item => {
|
||||
activeTab.value = item.value
|
||||
}
|
||||
const searchChange = debounce(() => {
|
||||
refresh(queryParams.value)
|
||||
}, 300)
|
||||
|
||||
const type = ref('')
|
||||
onLoad(option => {
|
||||
if (option.id) {
|
||||
uni.setNavigationBarTitle({
|
||||
title: '张三'
|
||||
})
|
||||
type.value = option.type ?? ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
|
@ -96,18 +96,15 @@ import { AgreementEnum } from '@/enums/agreementEnums'
|
|||
import { BACK_URL, ROLEINDEX } from '@/enums/cacheEnums'
|
||||
import { ChannelEnum, LoginTypeEnum } from '@/enums/appEnums'
|
||||
import logo from '@/static/images/logo.png'
|
||||
import { useRoleData } from '@/hooks/useRoleData'
|
||||
import { useTabBarStore } from '@/stores/tabbar'
|
||||
import { setNextRoute } from '@/hooks/useRoleData'
|
||||
import { getAllDict } from '@/hooks/useDictOptions'
|
||||
|
||||
const tabBarStore = useTabBarStore()
|
||||
const userStore = useUserStore()
|
||||
const appStore = useAppStore()
|
||||
const loginData = ref()
|
||||
const checked = ref<string[]>([])
|
||||
const isCheckAgreement = ref()
|
||||
const wxLoginCode = ref()
|
||||
const { roles } = useRoleData()
|
||||
|
||||
const isChecked = computed(() => unref(checked).length > 0)
|
||||
const form = ref({
|
||||
|
|
@ -237,27 +234,22 @@ async function loginHandle(data: any) {
|
|||
uni.hideLoading()
|
||||
// appStore.setFirstLogin(true)
|
||||
const { userInfo } = userStore
|
||||
if (userInfo.roles?.length > 1) {
|
||||
uni.redirectTo({
|
||||
url: '/bundle/pages/select-role/index'
|
||||
})
|
||||
} else {
|
||||
// 判断是主账号登录还是电销/招生登录
|
||||
if (userInfo.postIds == 0) {
|
||||
cache.set(ROLEINDEX, 2)
|
||||
setNextRoute()
|
||||
} else {
|
||||
if (userInfo.roles?.length > 1) {
|
||||
uni.redirectTo({
|
||||
url: '/bundle/pages/select-role/index'
|
||||
})
|
||||
} else {
|
||||
setNextRoute()
|
||||
}
|
||||
}
|
||||
cache.remove(BACK_URL)
|
||||
}
|
||||
|
||||
const setNextRoute = () => {
|
||||
const ind = cache.get(ROLEINDEX) || 0
|
||||
const { userInfo } = userStore
|
||||
const obj = roles.find(role => {
|
||||
return role.ids.includes(userInfo.roles[ind].id)
|
||||
})
|
||||
tabBarStore.setTabBarList(obj?.tabBarList)
|
||||
uni.reLaunch({
|
||||
url: obj?.tabBarList[0].path || ''
|
||||
})
|
||||
}
|
||||
/**点击空白处 */
|
||||
const handleClickEmpty = e => {
|
||||
if (e.target.dataset.name) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
<template>
|
||||
<view>
|
||||
<u-tabs
|
||||
:list="tabs"
|
||||
:scrollable="false"
|
||||
:activeStyle="{
|
||||
color: '#303133',
|
||||
fontWeight: 'bold',
|
||||
transform: 'scale(1.05)'
|
||||
}"
|
||||
:inactiveStyle="{
|
||||
color: '#606266',
|
||||
transform: 'scale(1)'
|
||||
}"
|
||||
itemStyle="padding-left: 15px; padding-right: 15px; height: 34px;"
|
||||
lineWidth="32"
|
||||
lineColor="#0E66FB"
|
||||
@change="handleChangeTab"
|
||||
></u-tabs>
|
||||
<TTable :columns="columns" :data="data">
|
||||
<template #index="scope">
|
||||
<text class="px-[16rpx] py-[6rpx] rounded-[4px]" :style="rankStyle(scope.row)">
|
||||
{{ scope.row }}
|
||||
</text>
|
||||
</template>
|
||||
</TTable>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
|
||||
const tabs = shallowRef([
|
||||
{ name: '电销业绩排行榜', value: 1 },
|
||||
{ name: '招生业绩排行榜', value: 2 }
|
||||
])
|
||||
const activeTab = ref(1)
|
||||
const totalLabel = computed(() => (activeTab.value == 1 ? '意向' : '成交'))
|
||||
|
||||
const handleChangeTab = item => {
|
||||
activeTab.value = item.value
|
||||
generateColumns()
|
||||
}
|
||||
const generateColumns = () => {
|
||||
return [
|
||||
{ name: 'index', label: '排名', slot: true },
|
||||
{ name: 'name', label: '姓名' },
|
||||
{ name: 'total', label: totalLabel.value + '客户数', width: 160, align: 'right' }
|
||||
]
|
||||
}
|
||||
|
||||
const columns = ref(generateColumns())
|
||||
const data = [
|
||||
{
|
||||
index: 1,
|
||||
name: '王小虎1789789789789789',
|
||||
total: 100
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
name: '王小虎2',
|
||||
total: 20
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
name: '王小虎2',
|
||||
total: 20
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
name: '王小虎2',
|
||||
total: 20
|
||||
}
|
||||
]
|
||||
const rankStyle = computed(() => row => {
|
||||
const styleMap: Record<number, string> = {
|
||||
'1': '#FCAE3C',
|
||||
'2': '#BCC1D8',
|
||||
'3': '#EEB286'
|
||||
}
|
||||
return {
|
||||
color: row >= 4 ? '#3d3d3d' : '#fff',
|
||||
background: styleMap[row]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
# 2.2.2
|
||||
|
||||
修复
|
||||
|
||||
1. 修复`picker`值选定残留问题
|
||||
|
||||
# 2.2.1
|
||||
|
||||
优化
|
||||
|
||||
1. 新增`getMenuList`方法,获取菜单数据【示例6】
|
||||
2. 新增`getMenuIndex`方法,获取指定`prop`的菜单索引位置
|
||||
3. 优化示例使用方法
|
||||
4. 修复互斥、联动的值处理问题
|
||||
5. 更新了说明文档
|
||||
|
||||
# 2.2.0
|
||||
|
||||
推荐更新
|
||||
|
||||
1. 新增更多的示例,示例更全面丰富
|
||||
2. 新增`openMenuItemPopup`方法,打开指定菜单弹窗【示例8】
|
||||
3. 新增`closeMenuPopup`方法,关闭菜单弹窗【示例5】
|
||||
4. 新增`getMenuValue`方法,获取菜单的值【示例8】
|
||||
5. 新增`updateMenu`方法,更新指定菜单项【示例7】
|
||||
6. 新增`setMenuLoading`方法,让某个菜单项处于加载中状态,异步数据有用【示例7】
|
||||
7. 修复`fixedTopValue`及顶部样式错误【示例2】
|
||||
8. 新增`syncDataKey`支持异步数据嵌套回调【示例7】
|
||||
9. 新增异步数据加载状态【示例7】
|
||||
10. 修复日期快捷获取上个月时间的错误
|
||||
11. 修复手动关闭时存在的点击残留
|
||||
12. 优化菜单项联动、互斥选择
|
||||
13. 优化异步数据阻塞菜单回显问题
|
||||
14. 修复数据回调时,会重新渲染菜单问题
|
||||
15. `@close`参数2支持返回当前菜单列表(Array)
|
||||
16. `@confirm`参数2支持返回当前菜单已选内容(Object)
|
||||
17. 菜单项`daterange`支持`showQuick`控制是否显示日期快选
|
||||
18. 移除`menuActiveText`,用处不大
|
||||
19. 更新了说明文档
|
||||
|
||||
# 2.1.1
|
||||
|
||||
优化
|
||||
|
||||
1. 优化图标字体命名
|
||||
|
||||
# 2.1.0
|
||||
|
||||
推荐更新
|
||||
|
||||
1. 现阶段由于兼容性问题,移除动态插槽
|
||||
2. 新增五个拓展插槽,`data`类型为 `slot1`/`slot2`/`slot3`/`slot4`/`slot5`
|
||||
3. 修复倒三角点击蒙层没有复原
|
||||
4. 新增更多演示示例
|
||||
5. 优化非固定页面顶部的效果
|
||||
6. 类型`cell`(下拉列表)数据项新增 `disabled` 用来禁用部分不可用选项
|
||||
7. 修复小程序对`v-show`、主题色的兼容问题
|
||||
8. 优化对 APP 的兼容
|
||||
|
||||
# 2.0.9
|
||||
|
||||
修复
|
||||
|
||||
1. 修复初始化数据时可能存在的选项高亮问题
|
||||
|
||||
# 2.0.8
|
||||
|
||||
优化
|
||||
|
||||
1. 优化拷贝函数引起的 App 兼容问题
|
||||
|
||||
# 2.0.7
|
||||
|
||||
优化
|
||||
|
||||
1. 优化模块图标支持主题换色
|
||||
2. 新增`fixedTopValue`,用于优化异形屏导致搜索框被挡住问题,具体请参考示例项目
|
||||
|
||||
# 2.0.6
|
||||
|
||||
优化
|
||||
|
||||
1. 优化三角图标支持主题换色
|
||||
|
||||
# 2.0.5
|
||||
|
||||
修复
|
||||
|
||||
1. 修复 `picker` 大量数据时未能滚动问题
|
||||
|
||||
# 2.0.4
|
||||
|
||||
修复
|
||||
|
||||
1. 修复弹窗后点击 `click`、`sort` 类型未能收起弹窗
|
||||
|
||||
# 2.0.3
|
||||
|
||||
优化
|
||||
|
||||
1. 移除 scss 的 @use 用法,以修复可能会导致部分用户的 lint 错误
|
||||
|
||||
# 2.0.2
|
||||
|
||||
优化
|
||||
|
||||
1. 优化菜单样式
|
||||
|
||||
# 2.0.1
|
||||
|
||||
新增
|
||||
|
||||
1. 下拉列表`cell`、级联`picker`新增选中图标`showIcon`
|
||||
|
||||
# 2.0.0
|
||||
|
||||
移除 TS
|
||||
|
||||
1. 移除了 TS 写法,现在是纯粹 JS 版本的 Vue3 版本
|
||||
2. 进一步完善使用文档及示例
|
||||
|
||||
# 1.2.1
|
||||
|
||||
新增功能
|
||||
|
||||
1. 新增顶部搜索,当 type 为 `slot` 时,可在弹窗内容自定义插槽
|
||||
2. 新增自定插槽,当 type 为 `search` 时,头部显示搜索框
|
||||
3. 优化界面样式
|
||||
|
||||
# 1.1.2
|
||||
|
||||
优化异步菜单项数据
|
||||
|
||||
1. 菜单项新增 `syncDataFn` 函数字段,返回异步菜单项数据内容
|
||||
|
||||
# 1.1.1
|
||||
|
||||
优化固定弹窗问题
|
||||
|
||||
1. `da-dropdown`新增 `bgColor` 字段,当固定在顶部时,需要填写背景颜色,默认`#fff`
|
||||
2. 优化弹窗时底部滑动问题
|
||||
|
||||
# 1.1.0
|
||||
|
||||
优化数据问题
|
||||
|
||||
### 优化
|
||||
|
||||
1. `da-dropdown`新增 `prop` 字段,通过 prop 的唯一性来区分已选的数据
|
||||
2. `da-dropdown`新增 `fixedTop` 字段,为 true 时将会固定在头部
|
||||
3. 优化说明文档
|
||||
|
||||
# 1.0.0
|
||||
|
||||
初始版本 1.0.0,基于 Typescript+Scss 进行开发,基本完善相关各大平台的小程序兼容问题
|
||||
|
||||
### 新增
|
||||
|
||||
1. 下拉列表(单选)
|
||||
2. 点击高亮
|
||||
3. 点击排序
|
||||
4. 下拉筛选(单选按钮、多选按钮、滑动选择器)
|
||||
5. 级联筛选(单选)
|
||||
6. 日期筛选(日期快选、日期区间选择)
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
<template>
|
||||
<view class="da-dropdown-cell">
|
||||
<view
|
||||
class="da-dropdown-cell-item"
|
||||
:class="[item.checked ? 'is-actived' : '', item.disabled ? 'is-disabled' : '']"
|
||||
v-for="(item, index) in cellOptions"
|
||||
:key="index"
|
||||
@click="handleSelect(item)"
|
||||
>
|
||||
<text class="da-dropdown-cell-item--label">{{ item.label }}</text>
|
||||
<text class="da-dropdown-cell-item--suffix">{{ item.suffix }}</text>
|
||||
<text class="da-dropdown-cell-item--check" v-if="item.checked && showIcon" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, watch, ref } from 'vue'
|
||||
import { deepClone } from '../utils'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
dropdownItem: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
dropdownIndex: {
|
||||
type: Number
|
||||
}
|
||||
},
|
||||
emits: ['success'],
|
||||
setup(props, { emit }) {
|
||||
const cellOptions = ref([])
|
||||
const showIcon = ref(false)
|
||||
|
||||
function initData(options, value) {
|
||||
const list = deepClone(options)
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i]
|
||||
if (item.value === value) {
|
||||
item.checked = true
|
||||
break
|
||||
}
|
||||
}
|
||||
cellOptions.value = list
|
||||
}
|
||||
|
||||
function handleSelect(item) {
|
||||
if (item.disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (props.dropdownItem?.prop) {
|
||||
const res = { [props.dropdownItem.prop]: item.value }
|
||||
emit('success', res, item, props.dropdownIndex)
|
||||
} else {
|
||||
console.error(`菜单项${props.dropdownItem.title}未定义prop,返回内容失败`)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.dropdownItem,
|
||||
v => {
|
||||
if (v?.options?.length) {
|
||||
initData(v.options, v.value)
|
||||
} else {
|
||||
cellOptions.value = []
|
||||
}
|
||||
showIcon.value = v?.showIcon || false
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
return {
|
||||
cellOptions,
|
||||
showIcon,
|
||||
handleSelect
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 下拉列表
|
||||
.da-dropdown-cell {
|
||||
--cell-height: 80rpx;
|
||||
|
||||
width: 100%;
|
||||
max-height: 60vh;
|
||||
overflow: hidden auto;
|
||||
|
||||
&-item {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: var(--cell-height);
|
||||
padding: 0 24rpx;
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
color: var(--dropdown-text-color);
|
||||
white-space: nowrap;
|
||||
border-bottom: 1rpx solid #dedede;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&--label {
|
||||
flex-grow: 1;
|
||||
max-width: 80%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
// #ifdef H5
|
||||
:deep(> span) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// #endif
|
||||
}
|
||||
|
||||
&--suffix {
|
||||
flex-grow: 1;
|
||||
margin-left: 10px;
|
||||
overflow: hidden;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
// #ifdef H5
|
||||
:deep(> span) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// #endif
|
||||
}
|
||||
|
||||
&--check {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--cell-height);
|
||||
height: var(--cell-height);
|
||||
|
||||
&::after {
|
||||
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
|
||||
font-family: 'da-dropdown-iconfont' !important;
|
||||
font-size: calc(var(--cell-height) / 3 * 2);
|
||||
font-style: normal;
|
||||
content: '\e736';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-actived {
|
||||
color: var(--dropdown-theme-color);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
color: #aaa;
|
||||
background: #efefef;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
<template>
|
||||
<view class="da-dropdown-daterange-box">
|
||||
<view class="da-dropdown-daterange">
|
||||
<view class="da-dropdown-daterange--date" :class="daterange.start ? 'is-actived' : ''">
|
||||
<picker mode="date" :value="daterange.start" @change="handleStartDate">
|
||||
{{ daterange.start || '请选择日期' }}
|
||||
</picker>
|
||||
</view>
|
||||
<view class="da-dropdown-daterange--separate">至</view>
|
||||
<view class="da-dropdown-daterange--date" :class="daterange.end ? 'is-actived' : ''">
|
||||
<picker
|
||||
mode="date"
|
||||
:value="daterange.end"
|
||||
:disabled="!daterange.start"
|
||||
:start="daterange.start"
|
||||
@change="handleEndDate"
|
||||
>
|
||||
{{ daterange.end || '请选择日期' }}
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
<view class="da-dropdown-daterange-tags" v-if="dropdownItem.showQuick">
|
||||
<block v-for="(tag, tagi) in dateTagList" :key="tagi">
|
||||
<view
|
||||
class="da-dropdown-tag"
|
||||
:class="datetag === tag.value ? 'is-actived' : ''"
|
||||
@click="handleTagDate(tag.value)"
|
||||
>
|
||||
<text class="da-dropdown-tag--text">{{ tag.label }}</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
<PartDropdownFooter
|
||||
:resetText="dropdownItem.resetText"
|
||||
:confirmText="dropdownItem.confirmText"
|
||||
@reset="handleReset()"
|
||||
@confirm="handleConfirm()"
|
||||
></PartDropdownFooter>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref, watch } from 'vue'
|
||||
import { deepClone, getRangeDate } from '../utils'
|
||||
import PartDropdownFooter from './part-dropdown-footer.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { PartDropdownFooter },
|
||||
props: {
|
||||
dropdownItem: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
dropdownIndex: {
|
||||
type: Number
|
||||
}
|
||||
},
|
||||
emits: ['success'],
|
||||
setup(props, { emit }) {
|
||||
const daterange = ref(null)
|
||||
const datetag = ref('')
|
||||
const dateTagList = ref([
|
||||
{ value: '-7', label: '本周' },
|
||||
{ value: '-14', label: '上周' },
|
||||
{ value: '-30', label: '本月' },
|
||||
{ value: '-60', label: '上月' },
|
||||
// { value: '-1', label: '昨日' },
|
||||
{ value: '7', label: '近7天' },
|
||||
{ value: '15', label: '近15天' },
|
||||
{ value: '30', label: '近30天' }
|
||||
])
|
||||
|
||||
function initData(dropdownItem, clearValue = false) {
|
||||
const item = deepClone(dropdownItem || null)
|
||||
if (clearValue === true) {
|
||||
daterange.value = {
|
||||
start: '',
|
||||
end: ''
|
||||
}
|
||||
datetag.value = ''
|
||||
} else {
|
||||
daterange.value = {
|
||||
start: item.value?.start || '',
|
||||
end: item.value?.end || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleStartDate(item) {
|
||||
daterange.value.start = item.detail.value
|
||||
daterange.value.end = ''
|
||||
datetag.value = ''
|
||||
}
|
||||
function handleEndDate(item) {
|
||||
if (!daterange.value?.start) {
|
||||
return
|
||||
}
|
||||
daterange.value.end = item.detail.value
|
||||
datetag.value = ''
|
||||
}
|
||||
function handleTagDate(code) {
|
||||
daterange.value = getRangeDate(code)
|
||||
datetag.value = code
|
||||
}
|
||||
function handleReset() {
|
||||
initData(props.dropdownItem, true)
|
||||
}
|
||||
function handleConfirm() {
|
||||
if (props.dropdownItem?.prop) {
|
||||
const res = { [props.dropdownItem.prop]: deepClone(daterange.value) }
|
||||
emit('success', res, daterange.value, props.dropdownIndex)
|
||||
} else {
|
||||
console.error(`菜单项${props.dropdownItem.title}未定义prop,返回内容失败`)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.dropdownItem,
|
||||
v => {
|
||||
initData(v)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
return {
|
||||
daterange,
|
||||
datetag,
|
||||
dateTagList,
|
||||
handleStartDate,
|
||||
handleEndDate,
|
||||
handleTagDate,
|
||||
handleReset,
|
||||
handleConfirm
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 日期范围
|
||||
.da-dropdown-daterange {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 24rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 999rpx;
|
||||
|
||||
&--date {
|
||||
flex-grow: 1;
|
||||
height: 66rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 66rpx;
|
||||
color: var(--dropdown-text-color);
|
||||
text-align: center;
|
||||
border-radius: 4rpx;
|
||||
|
||||
&.is-actived {
|
||||
color: var(--dropdown-theme-color);
|
||||
}
|
||||
}
|
||||
|
||||
&--separate {
|
||||
flex-shrink: 0;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
&-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.da-dropdown-tag {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20rpx 40rpx;
|
||||
margin-right: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
color: var(--dropdown-text-color);
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 999rpx;
|
||||
|
||||
&--text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.is-actived {
|
||||
color: var(--dropdown-theme-color);
|
||||
background-color: #fff;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
content: '';
|
||||
background-color: var(--dropdown-theme-color);
|
||||
opacity: 0.05;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
<template>
|
||||
<view class="da-dropdown-filter">
|
||||
<view class="da-dropdown-filter-box" v-for="(item, index) in filterList" :key="index">
|
||||
<view class="da-dropdown-filter--title">{{ item.title }}</view>
|
||||
<view class="da-dropdown-filter-content">
|
||||
<!-- 单选类型 -->
|
||||
<block v-if="item.type === 'radio'">
|
||||
<view
|
||||
v-for="(opt, optIdx) in item.options"
|
||||
class="da-dropdown-filter-item da-dropdown-tag"
|
||||
:class="item.value === opt.value ? 'is-actived' : ''"
|
||||
:key="optIdx"
|
||||
@click="handleRadioChange(item, opt, optIdx, index)"
|
||||
>
|
||||
<text class="da-dropdown-tag--text">{{ opt.label }}</text>
|
||||
</view>
|
||||
</block>
|
||||
<!-- 多选类型 -->
|
||||
<block v-else-if="item.type === 'checkbox'">
|
||||
<view
|
||||
v-for="(opt, optIdx) in item.options"
|
||||
class="da-dropdown-filter-item da-dropdown-tag"
|
||||
:class="opt.isActived ? 'is-actived' : ''"
|
||||
:key="optIdx"
|
||||
@click="handleCheckboxChange(item, opt, optIdx, index)"
|
||||
>
|
||||
<text class="da-dropdown-tag--text">{{ opt.label }}</text>
|
||||
</view>
|
||||
</block>
|
||||
<!-- 滑块类型 -->
|
||||
<block v-else-if="item.type === 'slider'">
|
||||
<slider
|
||||
style="width: 100%"
|
||||
:value="item.value"
|
||||
:min="item.componentProps.min || 0"
|
||||
:max="item.componentProps.max || 100"
|
||||
:step="item.componentProps.step || 1"
|
||||
:activeColor="item.componentProps.activeColor"
|
||||
:show-value="item.componentProps.showValue"
|
||||
@change="e => handleSliderChange(e, item, index)"
|
||||
/>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
<PartDropdownFooter
|
||||
:resetText="dropdownItem.resetText"
|
||||
:confirmText="dropdownItem.confirmText"
|
||||
@reset="handleReset()"
|
||||
@confirm="handleConfirm()"
|
||||
></PartDropdownFooter>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref, watch } from 'vue'
|
||||
import { deepClone } from '../utils'
|
||||
import PartDropdownFooter from './part-dropdown-footer.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { PartDropdownFooter },
|
||||
props: {
|
||||
dropdownItem: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
dropdownIndex: {
|
||||
type: Number
|
||||
}
|
||||
},
|
||||
emits: ['success', 'change'],
|
||||
setup(props, { emit }) {
|
||||
const filterList = ref(null)
|
||||
|
||||
function initData(dropdownItem, clearValue = false) {
|
||||
const { options = [], value = {} } = dropdownItem
|
||||
if (options?.length) {
|
||||
const list = deepClone(options)
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const k = list[i]
|
||||
if (clearValue !== true && (value[k.prop] || value[k.prop] === 0)) {
|
||||
k.value = value[k.prop]
|
||||
}
|
||||
|
||||
// 多选
|
||||
if (k.type === 'checkbox' && k.value?.length) {
|
||||
if (k.options?.length) {
|
||||
k.options.forEach(x => {
|
||||
x.isActived = k.value?.includes(x.value)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
filterList.value = list
|
||||
} else {
|
||||
filterList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function handleRadioChange(item, opt, _optIdx, _index) {
|
||||
item.value = opt.value
|
||||
}
|
||||
function handleCheckboxChange(item, opt, _optIdx, _index) {
|
||||
if (opt.isActived) {
|
||||
opt.isActived = false
|
||||
if (item.value?.length) {
|
||||
const idx = item.value.findIndex(k => k === opt.value)
|
||||
item.value.splice(idx, 1)
|
||||
} else {
|
||||
item.value = []
|
||||
}
|
||||
} else {
|
||||
if (item.value?.length) {
|
||||
item.value.push(opt.value)
|
||||
} else {
|
||||
item.value = [opt.value]
|
||||
}
|
||||
opt.isActived = true
|
||||
}
|
||||
}
|
||||
function handleSliderChange(event, item, _index) {
|
||||
const v = event.detail.value
|
||||
item.value = v
|
||||
}
|
||||
function handleReset() {
|
||||
initData(props.dropdownItem || [], true)
|
||||
}
|
||||
function handleConfirm() {
|
||||
const list = deepClone(filterList.value)
|
||||
|
||||
if (props.dropdownItem?.prop) {
|
||||
const obj = {}
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const k = list[i]
|
||||
if (k.value || k.value === 0) {
|
||||
obj[k.prop] = k.value
|
||||
}
|
||||
}
|
||||
const res = { [props.dropdownItem.prop]: obj }
|
||||
emit('success', res, obj, props.dropdownIndex)
|
||||
} else {
|
||||
console.error(`菜单项${props.dropdownItem.title}未定义prop,返回内容失败`)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.dropdownItem,
|
||||
v => {
|
||||
initData(v || null)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
return {
|
||||
filterList,
|
||||
handleRadioChange,
|
||||
handleCheckboxChange,
|
||||
handleSliderChange,
|
||||
handleReset,
|
||||
handleConfirm
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 多条件筛选
|
||||
.da-dropdown-filter {
|
||||
&-box {
|
||||
padding: 0 24rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&--title {
|
||||
flex-shrink: 0;
|
||||
padding: 20rpx 0;
|
||||
font-size: 26rpx;
|
||||
color: var(--dropdown-text-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.da-dropdown-tag {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20rpx 40rpx;
|
||||
margin-right: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
color: var(--dropdown-text-color);
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 999rpx;
|
||||
|
||||
&--text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.is-actived {
|
||||
color: var(--dropdown-theme-color);
|
||||
background-color: #fff;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
content: '';
|
||||
background-color: var(--dropdown-theme-color);
|
||||
opacity: 0.05;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<view class="da-dropdown-footer">
|
||||
<view class="da-dropdown-footer--reset" @click="handleReset()">
|
||||
{{ resetText || '重置' }}
|
||||
</view>
|
||||
<view class="da-dropdown-footer--confirm" @click="handleConfirm()">
|
||||
{{ confirmText || '确定' }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PartDropdownFooter',
|
||||
props: {
|
||||
resetText: {
|
||||
type: String,
|
||||
default: '重置'
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '确定'
|
||||
}
|
||||
},
|
||||
emits: ['confirm', 'reset'],
|
||||
setup(_, { emit }) {
|
||||
function handleReset() {
|
||||
emit('reset')
|
||||
}
|
||||
function handleConfirm() {
|
||||
emit('confirm')
|
||||
}
|
||||
|
||||
return {
|
||||
handleReset,
|
||||
handleConfirm
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.da-dropdown-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
margin-top: 20rpx;
|
||||
|
||||
&--reset,
|
||||
&--confirm {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 66rpx;
|
||||
font-size: 28rpx;
|
||||
color: #555;
|
||||
background-color: #fff;
|
||||
border: 2rpx solid #ccc;
|
||||
border-radius: 66rpx;
|
||||
}
|
||||
|
||||
&--confirm {
|
||||
margin-left: 24rpx;
|
||||
color: #fff;
|
||||
background-color: var(--dropdown-theme-color);
|
||||
border-color: var(--dropdown-theme-color);
|
||||
}
|
||||
|
||||
&--reset:hover,
|
||||
&--confirm:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
<template>
|
||||
<view class="da-dropdown-picker" v-if="viewCol && viewCol.length">
|
||||
<view class="da-dropdown-picker-inner" v-for="(vc, vci) in viewCol" :key="vci">
|
||||
<scroll-view class="da-dropdown-picker-view" scroll-y>
|
||||
<view
|
||||
class="da-dropdown-picker-item"
|
||||
:class="vr.checked ? 'is-actived' : ''"
|
||||
v-for="(vr, vri) in viewRow[vci]"
|
||||
:key="vri"
|
||||
@click="handleSelect(vr, vci, vri)"
|
||||
>
|
||||
<text class="da-dropdown-picker-item--name">{{ vr.label }}</text>
|
||||
<text
|
||||
class="da-dropdown-picker-item--icon"
|
||||
v-if="vr.children && vr.children.length"
|
||||
></text>
|
||||
<text
|
||||
class="da-dropdown-picker-item--check"
|
||||
v-if="vr.checked && (!vr.children || vr.children.length === 0)"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref, watch } from 'vue'
|
||||
import { deepClone } from '../utils'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
dropdownItem: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
dropdownIndex: {
|
||||
type: Number
|
||||
}
|
||||
},
|
||||
emits: ['success'],
|
||||
setup(props, { emit }) {
|
||||
const viewCol = ref([])
|
||||
const viewRow = ref([])
|
||||
|
||||
function checkData(selected, list) {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const k = list[i]
|
||||
for (let j = 0; j < selected.length; j++) {
|
||||
const x = selected[j]
|
||||
if (k.value === x) {
|
||||
k.checked = true
|
||||
viewCol.value.push(k.value)
|
||||
viewRow.value.push(list)
|
||||
if (k.children?.length) {
|
||||
checkData(selected, k.children)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initData(item) {
|
||||
const list = deepClone(item?.options || [])
|
||||
if (list?.length) {
|
||||
if (item.value?.length) {
|
||||
viewCol.value = []
|
||||
viewRow.value = []
|
||||
|
||||
checkData(item.value, list)
|
||||
} else {
|
||||
viewCol.value.push('tmpValue')
|
||||
viewRow.value.push(list)
|
||||
}
|
||||
} else {
|
||||
viewCol.value = []
|
||||
viewRow.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(item, colIndex, _rowIndex) {
|
||||
let lastItem = false
|
||||
viewCol.value.splice(colIndex)
|
||||
viewCol.value[colIndex] = item.value
|
||||
|
||||
if (viewRow.value[colIndex]?.length) {
|
||||
viewRow.value[colIndex].forEach(k => {
|
||||
k.checked = false
|
||||
})
|
||||
}
|
||||
|
||||
item.checked = true
|
||||
const list = item?.children || null
|
||||
|
||||
if (list?.length) {
|
||||
viewCol.value[colIndex + 1] = 'tmpValue'
|
||||
viewRow.value[colIndex + 1] = list
|
||||
lastItem = false
|
||||
} else {
|
||||
console.warn('最后一项', item)
|
||||
lastItem = true
|
||||
}
|
||||
|
||||
try {
|
||||
if (viewRow.value[colIndex + 1]?.length) {
|
||||
viewRow.value[colIndex + 1].forEach(k => {
|
||||
k.checked = false
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('try clean row data', e)
|
||||
// --
|
||||
}
|
||||
|
||||
if (lastItem) {
|
||||
if (props.dropdownItem?.prop) {
|
||||
const res = { [props.dropdownItem.prop]: deepClone(viewCol.value) }
|
||||
// emit('success', res, viewCol.value, props.dropdownIndex)
|
||||
} else {
|
||||
console.error(`菜单项${props.dropdownItem.title}未定义prop,返回内容失败`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.dropdownItem,
|
||||
v => {
|
||||
initData(v)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
return {
|
||||
viewCol,
|
||||
viewRow,
|
||||
|
||||
handleSelect
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.da-dropdown-picker {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-height: 60vh;
|
||||
overflow: hidden;
|
||||
line-height: 1;
|
||||
|
||||
&-inner {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&-view {
|
||||
display: flex;
|
||||
|
||||
/* #ifdef MP-ALIPAY */
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
|
||||
/* #endif */
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
+ .da-dropdown-picker-view {
|
||||
border-left: 1px solid #eee;
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--dropdown-text-color);
|
||||
text-align: left;
|
||||
|
||||
&--icon {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
|
||||
&::after {
|
||||
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
|
||||
font-family: 'da-dropdown-iconfont' !important;
|
||||
font-size: 24rpx;
|
||||
font-style: normal;
|
||||
content: '\e643';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
&--check {
|
||||
flex-shrink: 0;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
|
||||
&::after {
|
||||
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
|
||||
font-family: 'da-dropdown-iconfont' !important;
|
||||
font-size: 24rpx;
|
||||
font-style: normal;
|
||||
content: '\e696';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
&.is-actived {
|
||||
color: var(--dropdown-theme-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,946 @@
|
|||
<template>
|
||||
<view
|
||||
class="da-dropdown"
|
||||
:class="{ 'is-fixed': fixedTop, 'has-search': hasSearch }"
|
||||
:style="dropdownStyle"
|
||||
>
|
||||
<!-- 搜索 -->
|
||||
<view class="da-dropdown-search" v-if="hasSearch" @touchmove.stop.prevent="handleMove">
|
||||
<input
|
||||
class="da-dropdown-search-input"
|
||||
:value="searchItem.value"
|
||||
@input="handleSearchChange"
|
||||
:placeholder="searchItem.placeholder || '请输入'"
|
||||
@confirm="handleSearch"
|
||||
confirm-type="search"
|
||||
/>
|
||||
<button class="da-dropdown-search-btn" @click="handleSearch">搜索</button>
|
||||
</view>
|
||||
<!-- 菜单 -->
|
||||
<view class="da-dropdown-menu" @touchmove.stop.prevent="handleMove">
|
||||
<view
|
||||
class="da-dropdown-menu-item"
|
||||
:class="{ 'is-hidden': item.isHidden === 'true' }"
|
||||
v-for="(item, index) in menuList"
|
||||
:key="index"
|
||||
@click="handleMenuClick(index, item)"
|
||||
>
|
||||
<text
|
||||
class="da-dropdown-menu-item--text"
|
||||
:class="item.isActived ? 'is-actived' : ''"
|
||||
>
|
||||
{{ item.title }}
|
||||
</text>
|
||||
<view class="da-dropdown-menu-item--icon" v-if="item.showArrow">
|
||||
<text v-if="item.isLoading" class="is--loading"></text>
|
||||
<text v-else-if="item.isClick" class="is--arrup"></text>
|
||||
<text v-else class="is--arrdown"></text>
|
||||
</view>
|
||||
<view
|
||||
class="da-dropdown-menu-item--sort"
|
||||
v-else-if="item.showSort"
|
||||
:class="'is--' + item.value"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 弹出 -->
|
||||
<view class="da-dropdown-content" :class="{ 'is-show': isShow, 'is-visible': isVisible }">
|
||||
<view
|
||||
class="da-dropdown-content-popup"
|
||||
:class="isShow ? 'is-show' : ''"
|
||||
style="height: 500px"
|
||||
>
|
||||
<view class="da-dropdown-popup-box" v-for="(item, index) in menuList" :key="index">
|
||||
<!-- 下拉列表 -->
|
||||
<DropdownCell
|
||||
v-if="item.type === 'cell' && index === currentIndex"
|
||||
:dropdownItem="item"
|
||||
:dropdownIndex="index"
|
||||
@success="handleCellSelect"
|
||||
></DropdownCell>
|
||||
<!-- 多条件筛选 -->
|
||||
<DropdownFilter
|
||||
v-if="item.type === 'filter' && index === currentIndex"
|
||||
:dropdownItem="item"
|
||||
:dropdownIndex="index"
|
||||
@success="handleFilterConfirm"
|
||||
></DropdownFilter>
|
||||
<!-- 级联选择 -->
|
||||
<DropdownPicker
|
||||
v-if="item.type === 'picker' && index === currentIndex"
|
||||
:dropdownItem="item"
|
||||
:dropdownIndex="index"
|
||||
@success="handlePickerConfirm"
|
||||
/>
|
||||
<!-- 日期范围 -->
|
||||
<DropdownDaterange
|
||||
v-if="item.type === 'daterange' && index === currentIndex"
|
||||
:dropdownItem="item"
|
||||
:dropdownIndex="index"
|
||||
@success="handleDaterangeConfirm"
|
||||
/>
|
||||
<!-- 弹窗插槽拓展X5 -->
|
||||
<template v-if="item.type === 'slot1' && index === currentIndex">
|
||||
<slot name="slot1" :item="item" :index="index"></slot>
|
||||
</template>
|
||||
<template v-if="item.type === 'slot2' && index === currentIndex">
|
||||
<slot name="slot2" :item="item" :index="index"></slot>
|
||||
</template>
|
||||
<template v-if="item.type === 'slot3' && index === currentIndex">
|
||||
<slot name="slot3" :item="item" :index="index"></slot>
|
||||
</template>
|
||||
<template v-if="item.type === 'slot4' && index === currentIndex">
|
||||
<slot name="slot4" :item="item" :index="index"></slot>
|
||||
</template>
|
||||
<template v-if="item.type === 'slot5' && index === currentIndex">
|
||||
<slot name="slot5" :item="item" :index="index"></slot>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="da-dropdown-content-mask"
|
||||
v-if="fixedTop"
|
||||
@tap="handlePopupMask"
|
||||
@touchmove.stop.prevent="handleMove"
|
||||
/>
|
||||
</view>
|
||||
<view class="da-dropdown--blank" v-if="fixedTop"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref, computed, onMounted } from 'vue'
|
||||
|
||||
import { deepClone, menuInitOpts, getValueByKey, checkDataField } from './utils'
|
||||
import DropdownPicker from './components/picker.vue'
|
||||
import DropdownCell from './components/cell.vue'
|
||||
import DropdownFilter from './components/filter.vue'
|
||||
import DropdownDaterange from './components/daterange.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { DropdownPicker, DropdownCell, DropdownFilter, DropdownDaterange },
|
||||
props: {
|
||||
/**
|
||||
* 导航菜单数据
|
||||
*/
|
||||
dropdownMenu: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 激活颜色
|
||||
*/
|
||||
themeColor: {
|
||||
type: String,
|
||||
default: '#007aff'
|
||||
},
|
||||
/**
|
||||
* 常规颜色
|
||||
*/
|
||||
textColor: {
|
||||
type: String,
|
||||
default: '#333333'
|
||||
},
|
||||
/**
|
||||
* 背景颜色,当固定在顶部时,此为必填
|
||||
*/
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: '#ffffff'
|
||||
},
|
||||
/**
|
||||
* 是否固定在顶部
|
||||
*/
|
||||
fixedTop: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 固定在头部时的位置,单位px
|
||||
* 如果页面定义了 "navigationStyle": "custom" ,因此固定头部时需要额外获取状态栏高度,以免被异形屏头部覆盖
|
||||
*/
|
||||
fixedTopValue: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/**
|
||||
* 弹窗过渡时间
|
||||
*/
|
||||
duration: {
|
||||
type: [Number, String],
|
||||
default: 300
|
||||
}
|
||||
},
|
||||
emits: ['open', 'close', 'confirm'],
|
||||
setup(props, { emit }) {
|
||||
const currentIndex = ref(-1)
|
||||
const isVisible = ref(false)
|
||||
const isShow = ref(false)
|
||||
const menuList = ref([])
|
||||
const hasSearch = ref(false)
|
||||
const searchItem = ref(null)
|
||||
|
||||
// 主题样式
|
||||
const dropdownStyle = computed(() => {
|
||||
return `
|
||||
--dropdown-theme-color: ${props.themeColor};
|
||||
--dropdown-text-color: ${props.textColor};
|
||||
--dropdown-background-color: ${props.bgColor};
|
||||
--dropdown-popup-duration: ${props.duration / 1000}s;
|
||||
--dropdown-fixed-top: ${props.fixedTopValue || 0}px;
|
||||
`
|
||||
})
|
||||
/**
|
||||
* 初始化数据
|
||||
*/
|
||||
async function initData() {
|
||||
const newMenu = deepClone(props.dropdownMenu || [])
|
||||
const allItem = { label: '不限', value: '-9999' }
|
||||
if (!newMenu || newMenu.length === 0) {
|
||||
menuList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < newMenu.length; i++) {
|
||||
let item = newMenu[i]
|
||||
if (item?.type) {
|
||||
item = { ...(menuInitOpts[newMenu[i]['type']] || {}), ...item }
|
||||
}
|
||||
|
||||
// 处理异步初始项
|
||||
if (typeof item.syncDataFn === 'function') {
|
||||
item.isLoading = true
|
||||
item.syncDataFn(item, i)
|
||||
.then(res => {
|
||||
menuList.value[i].options = checkDataField(
|
||||
item.syncDataKey ? getValueByKey(res, item.syncDataKey) : res,
|
||||
item.field
|
||||
)
|
||||
// 处理 不限 项
|
||||
if (this.menuList[i].showAll) {
|
||||
if (
|
||||
this.menuList[i].options.findIndex(
|
||||
k => k.value === allItem.value
|
||||
) === -1
|
||||
) {
|
||||
this.menuList[i].options.unshift(allItem)
|
||||
}
|
||||
}
|
||||
menuList.value[i].isLoading = false
|
||||
})
|
||||
.catch(() => {
|
||||
menuList.value[i].isLoading = false
|
||||
})
|
||||
}
|
||||
|
||||
if (item.options?.length) {
|
||||
// 同步差异字段
|
||||
item.options = checkDataField(item.options, item.field)
|
||||
// 处理 不限 项
|
||||
if (item.showAll) {
|
||||
if (item.options.findIndex(k => k.value === allItem.value) === -1) {
|
||||
item.options.unshift(allItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理已选项
|
||||
if (typeof item.value !== 'undefined') {
|
||||
switch (item.type) {
|
||||
case 'cell':
|
||||
for (let x = 0; x < item.options.length; x++) {
|
||||
const k = item.options[x]
|
||||
if (k.value === item.value) {
|
||||
item.isActived = true
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'click':
|
||||
item.isActived = item.value === true
|
||||
|
||||
break
|
||||
case 'sort':
|
||||
item.isActived = item.value === 'asc' || item.value === 'desc'
|
||||
|
||||
break
|
||||
case 'filter':
|
||||
item.isActived = JSON.stringify(item.value || {}) !== '{}'
|
||||
|
||||
break
|
||||
case 'picker':
|
||||
item.isActived = item.value?.length
|
||||
|
||||
break
|
||||
case 'daterange':
|
||||
item.isActived = item.value?.start && item.value?.end
|
||||
break
|
||||
case 'slot':
|
||||
item.isActived = !!item.value
|
||||
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else {
|
||||
item.isActived = false
|
||||
}
|
||||
|
||||
// 搜索项特殊处理
|
||||
if (!hasSearch.value && item.type === 'search') {
|
||||
item.isHidden = 'true'
|
||||
searchItem.value = item
|
||||
hasSearch.value = true
|
||||
}
|
||||
|
||||
newMenu[i] = item
|
||||
}
|
||||
menuList.value = newMenu
|
||||
}
|
||||
/**
|
||||
* 更新数据
|
||||
* @param prop
|
||||
* @param value
|
||||
* @param key
|
||||
*/
|
||||
function updateMenu(prop, value, key) {
|
||||
if (!key) {
|
||||
console.error('updateMenu 错误,key不存在')
|
||||
return
|
||||
}
|
||||
const idx = getMenuIndex(prop)
|
||||
menuList.value[idx][key] =
|
||||
key === 'options' ? checkDataField(value, menuList.value[idx].field || null) : value
|
||||
|
||||
// 去除点击效果
|
||||
if (key === 'value' && !value && value !== 0) {
|
||||
menuList.value[idx].isActived = false
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 更新数据
|
||||
* @param prop
|
||||
* @param state
|
||||
*/
|
||||
function setMenuLoading(prop, state) {
|
||||
const idx = getMenuIndex(prop)
|
||||
menuList.value[idx].isLoading = state
|
||||
}
|
||||
/**
|
||||
* 获取菜单项位置
|
||||
* @param prop
|
||||
*/
|
||||
function getMenuIndex(prop) {
|
||||
return menuList.value.findIndex(k => k.prop === prop)
|
||||
}
|
||||
/**
|
||||
* 获取菜单数据
|
||||
*/
|
||||
function getMenuList() {
|
||||
return menuList.value
|
||||
}
|
||||
/**
|
||||
* 初始化获取系统信息
|
||||
*/
|
||||
function initDomInfo() {}
|
||||
/**
|
||||
* 打开弹窗
|
||||
* @param index 当前激活索引
|
||||
*/
|
||||
function openMenuItemPopup(index) {
|
||||
console.log('openMenuItemPopup')
|
||||
isShow.value = true
|
||||
isVisible.value = true
|
||||
currentIndex.value = index
|
||||
menuList.value[index].isClick = true
|
||||
emit('open', currentIndex.value)
|
||||
}
|
||||
/**
|
||||
* 关闭弹窗
|
||||
*/
|
||||
function closeMenuPopup() {
|
||||
clearClickState()
|
||||
isShow.value = false
|
||||
// 延迟移除下拉弹窗
|
||||
setTimeout(() => {
|
||||
isVisible.value = false
|
||||
clearIndex()
|
||||
}, props.duration)
|
||||
emit('close', currentIndex.value, menuList.value)
|
||||
}
|
||||
/**
|
||||
* 点击蒙层
|
||||
*/
|
||||
function handlePopupMask() {
|
||||
closeMenuPopup()
|
||||
}
|
||||
/**
|
||||
* 清除点击状态
|
||||
*/
|
||||
function clearClickState() {
|
||||
if (menuList.value?.length) {
|
||||
menuList.value.forEach(k => {
|
||||
k.isClick = false
|
||||
})
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 清理滚动
|
||||
*/
|
||||
function handleMove() {
|
||||
return false
|
||||
}
|
||||
/**
|
||||
* 关闭弹窗
|
||||
*/
|
||||
function clearIndex() {
|
||||
currentIndex.value = -1
|
||||
}
|
||||
/**
|
||||
* 点击菜单项
|
||||
*/
|
||||
function handleMenuClick(index, item) {
|
||||
if (item.isLoading) return
|
||||
|
||||
const dropdownMenu = menuList.value
|
||||
const menuItem = dropdownMenu[index]
|
||||
|
||||
dropdownMenu.forEach(k => {
|
||||
k.isClick = false
|
||||
})
|
||||
|
||||
if (menuItem.type === 'click') {
|
||||
return handleItemClick(menuItem, index)
|
||||
}
|
||||
|
||||
if (menuItem.type === 'sort') {
|
||||
return handleItemSort(menuItem, index)
|
||||
}
|
||||
if (index === currentIndex.value) {
|
||||
item.isClick = false
|
||||
closeMenuPopup()
|
||||
return
|
||||
}
|
||||
|
||||
item.isClick = true
|
||||
|
||||
openMenuItemPopup(index)
|
||||
}
|
||||
/**
|
||||
* 获取菜单值
|
||||
*/
|
||||
function getMenuValue() {
|
||||
const obj = {}
|
||||
menuList.value.forEach(k => {
|
||||
obj[k.prop] = k.value
|
||||
})
|
||||
|
||||
return obj
|
||||
}
|
||||
/**
|
||||
* 搜索输入
|
||||
*/
|
||||
function handleSearchChange(e) {
|
||||
searchItem.value.value = e?.detail?.value
|
||||
}
|
||||
/**
|
||||
* 确定搜索
|
||||
*/
|
||||
function handleSearch() {
|
||||
if (searchItem.value?.prop) {
|
||||
const res = { [searchItem.value.prop]: searchItem.value.value }
|
||||
emit('confirm', res, getMenuValue())
|
||||
} else {
|
||||
console.error(`菜单项${searchItem.value.title}未定义prop,返回内容失败`)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 菜单项-下拉列表回调
|
||||
* @param callbackData 操作返回的数据
|
||||
* @param cellItem 下拉列表项数据
|
||||
* @param index 菜单索引
|
||||
*/
|
||||
function handleCellSelect(callbackData, cellItem, index) {
|
||||
const dropdownMenu = menuList.value
|
||||
const item = dropdownMenu[index]
|
||||
item.isClick = false
|
||||
|
||||
if (cellItem.value === '-9999') {
|
||||
item.isActived = false
|
||||
item.value = null
|
||||
} else {
|
||||
item.isActived = true
|
||||
item.value = cellItem.value
|
||||
}
|
||||
|
||||
closeMenuPopup()
|
||||
emit('confirm', callbackData, getMenuValue())
|
||||
}
|
||||
/**
|
||||
* 菜单项-点击
|
||||
* @param item 菜单项
|
||||
* @param index 菜单项索引
|
||||
*/
|
||||
function handleItemClick(item, index) {
|
||||
closeMenuPopup()
|
||||
|
||||
if (currentIndex.value === -1) {
|
||||
currentIndex.value = index
|
||||
item.value = true
|
||||
item.isActived = true
|
||||
} else {
|
||||
item.value = false
|
||||
item.isActived = false
|
||||
clearIndex()
|
||||
}
|
||||
if (item?.prop) {
|
||||
const res = { [item.prop]: item.value }
|
||||
emit('confirm', res, getMenuValue())
|
||||
} else {
|
||||
console.error(`菜单项${item.title}未定义prop,返回内容失败`)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 菜单项-排序
|
||||
* @param item 菜单项
|
||||
* @param index 菜单项索引
|
||||
*/
|
||||
function handleItemSort(item, index) {
|
||||
closeMenuPopup()
|
||||
|
||||
if (item.value === 'asc') {
|
||||
item.value = 'desc'
|
||||
currentIndex.value = index
|
||||
item.isActived = true
|
||||
} else if (item.value === 'desc') {
|
||||
item.value = undefined
|
||||
item.isActived = false
|
||||
clearIndex()
|
||||
} else {
|
||||
item.value = 'asc'
|
||||
currentIndex.value = index
|
||||
item.isActived = true
|
||||
}
|
||||
|
||||
if (item?.prop) {
|
||||
const res = { [item.prop]: item.value }
|
||||
emit('confirm', res, getMenuValue())
|
||||
} else {
|
||||
console.error(`菜单项${item.title}未定义prop,返回内容失败`)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 菜单项-筛选回调
|
||||
* @param callbackData 操作返回的数据
|
||||
* @param filterData 筛选数据
|
||||
* @param index 菜单索引
|
||||
*/
|
||||
function handleFilterConfirm(callbackData, filterData, index) {
|
||||
const dropdownMenu = menuList.value
|
||||
const item = dropdownMenu[index]
|
||||
item.isClick = false
|
||||
|
||||
item.isActived = JSON.stringify(filterData || {}) !== '{}'
|
||||
item.value = filterData
|
||||
|
||||
closeMenuPopup()
|
||||
emit('confirm', callbackData, getMenuValue())
|
||||
}
|
||||
/**
|
||||
* 菜单项-级联回调
|
||||
* @param callbackData 操作返回的数据
|
||||
* @param pickerItem 级联已选数据
|
||||
* @param index 菜单索引
|
||||
*/
|
||||
function handlePickerConfirm(callbackData, pickerItem, index) {
|
||||
const dropdownMenu = menuList.value
|
||||
const item = dropdownMenu[index]
|
||||
item.isClick = false
|
||||
|
||||
if (!pickerItem || pickerItem[0] === '-9999') {
|
||||
item.isActived = false
|
||||
item.value = null
|
||||
} else {
|
||||
item.isActived = true
|
||||
item.value = pickerItem
|
||||
}
|
||||
|
||||
closeMenuPopup()
|
||||
emit('confirm', callbackData, getMenuValue())
|
||||
}
|
||||
/**
|
||||
* 菜单项-日期范围回调
|
||||
* @param callbackData 操作返回的数据
|
||||
* @param daterangeItem 日期范围数据
|
||||
* @param index 菜单索引
|
||||
*/
|
||||
function handleDaterangeConfirm(callbackData, daterangeItem, index) {
|
||||
const dropdownMenu = menuList.value
|
||||
const item = dropdownMenu[index]
|
||||
item.isClick = false
|
||||
|
||||
if (daterangeItem?.start && daterangeItem?.end) {
|
||||
item.isActived = true
|
||||
item.value = daterangeItem
|
||||
} else {
|
||||
item.isActived = false
|
||||
item.value = null
|
||||
}
|
||||
|
||||
closeMenuPopup()
|
||||
emit('confirm', callbackData, getMenuValue())
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initDomInfo()
|
||||
initData()
|
||||
})
|
||||
|
||||
return {
|
||||
initData,
|
||||
menuList,
|
||||
updateMenu,
|
||||
setMenuLoading,
|
||||
getMenuIndex,
|
||||
getMenuList,
|
||||
dropdownStyle,
|
||||
currentIndex,
|
||||
isShow,
|
||||
isVisible,
|
||||
hasSearch,
|
||||
searchItem,
|
||||
handleSearchChange,
|
||||
handleSearch,
|
||||
handleMenuClick,
|
||||
handlePopupMask,
|
||||
handleMove,
|
||||
getMenuValue,
|
||||
|
||||
handleCellSelect,
|
||||
handleFilterConfirm,
|
||||
handlePickerConfirm,
|
||||
handleDaterangeConfirm
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@font-face {
|
||||
font-family: 'da-dropdown-iconfont'; /* Project id */
|
||||
src: url('data:application/octet-stream;base64,AAEAAAALAIAAAwAwR1NVQiCLJXoAAAE4AAAAVE9TLzI8GUoGAAABjAAAAGBjbWFwgZ2FYQAAAgQAAAHIZ2x5ZmWuwwYAAAPcAAACHGhlYWQm2YiXAAAA4AAAADZoaGVhB94DhwAAALwAAAAkaG10eBgAAAAAAAHsAAAAGGxvY2EB9gF4AAADzAAAAA5tYXhwARgAVAAAARgAAAAgbmFtZRCjPLAAAAX4AAACZ3Bvc3QrCOz4AAAIYAAAAFsAAQAAA4D/gABcBAAAAAAABAAAAQAAAAAAAAAAAAAAAAAAAAYAAQAAAAEAAMt/P/FfDzz1AAsEAAAAAADh3SJNAAAAAOHdIk0AAP//BAADAQAAAAgAAgAAAAAAAAABAAAABgBIAAgAAAAAAAIAAAAKAAoAAAD/AAAAAAAAAAEAAAAKADAAPgACREZMVAAObGF0bgAaAAQAAAAAAAAAAQAAAAQAAAAAAAAAAQAAAAFsaWdhAAgAAAABAAAAAQAEAAQAAAABAAgAAQAGAAAAAQAAAAQEAAGQAAUAAAKJAswAAACPAokCzAAAAesAMgEIAAACAAUDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBmRWQAwOYE5zYDgP+AAAAD3ACAAAAAAQAAAAAAAAAAAAAAAAACBAAAAAQAAAAEAAAABAAAAAQAAAAEAAAAAAAABQAAAAMAAAAsAAAABAAAAXwAAQAAAAAAdgADAAEAAAAsAAMACgAAAXwABABKAAAADAAIAAIABOYE5ifmQ+aW5zb//wAA5gTmJ+ZD5pbnNv//AAAAAAAAAAAAAAABAAwADAAMAAwADAAAAAUAAgADAAQAAQAAAQYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAATAAAAAAAAAAFAADmBAAA5gQAAAAFAADmJwAA5icAAAACAADmQwAA5kMAAAADAADmlgAA5pYAAAAEAADnNgAA5zYAAAABAAAAAAAoAJgAwADgAQ4AAAABAAAAAANkAooAEwAAGwEeATcBNi4CBwEOAS8BJg4BFKXqBhMHAa4HAQwSB/5vBg8GzwgQDAGi/vEHAQYB0QcSDQEG/rsEAQSHBAINEQAAAAgAAAAAA3EC+AAIABEAGgAjACwANQA+AEcAAAEUBiImNDYyFgMiBhQWMjY0JiUiJjQ2MhYUBiU0JiIGFBYyNhMWFAYiJjQ2MgEGFBYyNjQmIhMGIiY0NjIWFAEmIgYUFjI2NAJYKz4rKz4rShsmJjYmJgEZFBsbJxsb/dAsPSwsPSxEFiw9LCw9AW0QIC8gIC8yCx8WFh8W/lwWPSwsPSwCrR4sLD0sLP27JjYmJjYmxBwmGxsmHC8fKys+LCwBLRY9LCw9LP4qES4gIC4hAWELFh8VFR/+kRYsPSwsPQAAAQAA//8CwAMBABQAAAE0JzUBFSYiBhQXCQEGFBYyNxUBNgLACP7AChsTCAEt/tMIExsKAUAIAYAMCQEBYAELExkJ/rX+tQkZEwsBAWEJAAACAAAAAAN0AsEADQAOAAAlATcXNjc2NxcGBwYHBgcBz/7XTa5QWYeOFF1cT0I7H1oBLz2FW1J7WClWdGRrX0YAAQAAAAADWQJKABkAAAEyHgEGBw4BBw4CJicmLwImJy4BPgEzNwMbFx0JCRBAdzcPKSooDR8hRUIgHQ0ICRsWtgJKEhwkEUeIPBARAQ4QIiNHRiMgDyEbEQEAAAAAABIA3gABAAAAAAAAABMAAAABAAAAAAABAAgAEwABAAAAAAACAAcAGwABAAAAAAADAAgAIgABAAAAAAAEAAgAKgABAAAAAAAFAAsAMgABAAAAAAAGAAgAPQABAAAAAAAKACsARQABAAAAAAALABMAcAADAAEECQAAACYAgwADAAEECQABABAAqQADAAEECQACAA4AuQADAAEECQADABAAxwADAAEECQAEABAA1wADAAEECQAFABYA5wADAAEECQAGABAA/QADAAEECQAKAFYBDQADAAEECQALACYBY0NyZWF0ZWQgYnkgaWNvbmZvbnRpY29uZm9udFJlZ3VsYXJpY29uZm9udGljb25mb250VmVyc2lvbiAxLjBpY29uZm9udEdlbmVyYXRlZCBieSBzdmcydHRmIGZyb20gRm9udGVsbG8gcHJvamVjdC5odHRwOi8vZm9udGVsbG8uY29tAEMAcgBlAGEAdABlAGQAIABiAHkAIABpAGMAbwBuAGYAbwBuAHQAaQBjAG8AbgBmAG8AbgB0AFIAZQBnAHUAbABhAHIAaQBjAG8AbgBmAG8AbgB0AGkAYwBvAG4AZgBvAG4AdABWAGUAcgBzAGkAbwBuACAAMQAuADAAaQBjAG8AbgBmAG8AbgB0AEcAZQBuAGUAcgBhAHQAZQBkACAAYgB5ACAAcwB2AGcAMgB0AHQAZgAgAGYAcgBvAG0AIABGAG8AbgB0AGUAbABsAG8AIABwAHIAbwBqAGUAYwB0AC4AaAB0AHQAcAA6AC8ALwBmAG8AbgB0AGUAbABsAG8ALgBjAG8AbQAAAgAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAQIBAwEEAQUBBgEHAAdnb3V4dWFuBmppYXphaQp5b3VqaWFudG91BnhpYXphaQh4aWFuZ3hpYQAAAA==')
|
||||
format('truetype');
|
||||
}
|
||||
|
||||
.da-dropdown {
|
||||
--dropdown-menu-height: 80rpx;
|
||||
--dropdown-popup-duration: 0.3s;
|
||||
|
||||
position: relative;
|
||||
z-index: 888;
|
||||
width: 100%;
|
||||
line-height: 1;
|
||||
|
||||
&--blank {
|
||||
width: 100%;
|
||||
height: var(--dropdown-menu-height);
|
||||
}
|
||||
|
||||
&-search {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: var(--dropdown-menu-height);
|
||||
padding: 10rpx 20rpx 6rpx;
|
||||
background: var(--dropdown-background-color, #fff);
|
||||
|
||||
&-input {
|
||||
flex-grow: 1;
|
||||
height: 60rpx;
|
||||
padding: 0 20rpx;
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
color: var(--dropdown-text-color);
|
||||
background: #f6f6f6;
|
||||
border-radius: 8rpx 0 0 8rpx;
|
||||
}
|
||||
|
||||
&-btn {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60rpx;
|
||||
padding: 0 20rpx;
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
color: var(--dropdown-text-color);
|
||||
background: #f6f6f6;
|
||||
border: none;
|
||||
border-radius: 0 8rpx 8rpx 0;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-menu {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--dropdown-menu-height);
|
||||
background: var(--dropdown-background-color, #fff);
|
||||
box-shadow: 0 1rpx 0 0 #bbb;
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
&:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
&.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--text {
|
||||
font-size: 24rpx;
|
||||
color: var(--dropdown-text-color);
|
||||
|
||||
&.is-actived {
|
||||
color: var(--dropdown-theme-color);
|
||||
}
|
||||
}
|
||||
|
||||
&--icon {
|
||||
flex-shrink: 0;
|
||||
margin-left: 2px;
|
||||
color: #bbb;
|
||||
|
||||
.is--loading,
|
||||
.is--arrup,
|
||||
.is--arrdown {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
|
||||
&::after {
|
||||
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
|
||||
font-family: 'da-dropdown-iconfont' !important;
|
||||
font-size: 24rpx;
|
||||
font-style: normal;
|
||||
content: '\e604';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
.is--loading {
|
||||
animation: RunLoading 1s linear 0s infinite;
|
||||
|
||||
&::after {
|
||||
content: '\e627';
|
||||
}
|
||||
}
|
||||
|
||||
.is--arrup {
|
||||
color: var(--dropdown-theme-color);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&--sort {
|
||||
position: relative;
|
||||
margin-left: 6rpx;
|
||||
transition: transform 0.3s;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: calc(50% - 16rpx);
|
||||
left: 0;
|
||||
content: '';
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
border-width: 8rpx;
|
||||
border-bottom-color: #bbb;
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: calc(50% + 6rpx);
|
||||
border-top-color: #bbb;
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
&.is--asc::before {
|
||||
border-bottom-color: var(--dropdown-theme-color);
|
||||
}
|
||||
|
||||
&.is--desc::after {
|
||||
border-top-color: var(--dropdown-theme-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
position: absolute;
|
||||
top: var(--dropdown-menu-height);
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
box-shadow: 0 -1rpx 0 0 #bbb;
|
||||
opacity: 0;
|
||||
transition: all var(--dropdown-popup-duration, 0.3s) linear;
|
||||
|
||||
&.is-show {
|
||||
z-index: 901;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.is-visible {
|
||||
visibility: visible;
|
||||
animation: CustomBS var(--dropdown-popup-duration) linear var(--dropdown-popup-duration)
|
||||
forwards;
|
||||
}
|
||||
|
||||
&-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 9;
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
&-popup {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
transition: transform var(--dropdown-popup-duration) linear;
|
||||
transform: translateY(-100%);
|
||||
|
||||
&.is-show {
|
||||
// transform: translateY(0);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-popup-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
line-height: 1;
|
||||
background: var(--dropdown-background-color, #fff);
|
||||
transition: border-radius var(--dropdown-popup-duration) linear;
|
||||
}
|
||||
|
||||
&.has-search {
|
||||
.da-dropdown {
|
||||
&-content {
|
||||
top: calc(var(--dropdown-menu-height) + var(--dropdown-menu-height));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 固定至顶 */
|
||||
&.is-fixed {
|
||||
z-index: 980;
|
||||
|
||||
.da-dropdown {
|
||||
&-search {
|
||||
position: fixed;
|
||||
top: calc(var(--window-top, 0px) + var(--dropdown-fixed-top, 0px));
|
||||
right: 0;
|
||||
left: 0;
|
||||
max-width: 1190px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
&-menu {
|
||||
position: fixed;
|
||||
top: calc(var(--window-top, 0px) + var(--dropdown-fixed-top, 0px));
|
||||
right: 0;
|
||||
left: 0;
|
||||
max-width: 1190px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
&-content {
|
||||
position: fixed;
|
||||
top: calc(
|
||||
var(--window-top, 0px) + var(--dropdown-fixed-top, 0px) +
|
||||
var(--dropdown-menu-height, 0px)
|
||||
);
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-search {
|
||||
.da-dropdown {
|
||||
&-menu {
|
||||
top: calc(
|
||||
var(--window-top, 0px) + var(--dropdown-fixed-top, 0px) +
|
||||
var(--dropdown-menu-height, 0px)
|
||||
);
|
||||
}
|
||||
|
||||
&-content {
|
||||
top: calc(
|
||||
var(--window-top, 0px) + var(--dropdown-fixed-top, 0px) +
|
||||
var(--dropdown-menu-height, 0px) + var(--dropdown-menu-height, 0px)
|
||||
);
|
||||
}
|
||||
|
||||
&--blank {
|
||||
height: calc(
|
||||
var(--dropdown-fixed-top, 0px) + var(--dropdown-menu-height, 0px) +
|
||||
var(--dropdown-menu-height, 0px)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes RunLoading {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes CustomBS {
|
||||
0% {
|
||||
box-shadow: 0 -1rpx 0 0 #bbb;
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 -1rpx 0 0 #bbb, 0 20rpx 20rpx -10rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,443 @@
|
|||
# da-dropdown
|
||||
|
||||
一个基于 Vue3 的头部导航栏下拉弹窗组件,多平台兼容。
|
||||
|
||||
组件一直在更新,遇到问题可在下方讨论。
|
||||
|
||||
`同时更新 Vue2 版本,在此查看 ===>` **[Vue2 版](https://ext.dcloud.net.cn/plugin?id=13062)**
|
||||
|
||||
### 关于使用
|
||||
|
||||
可在右侧的`使用 HBuilderX 导入插件`或`下载示例项目ZIP`,示例项目已添加多个示例,方便快速上手。
|
||||
|
||||
可通过下方的示例及文档说明,进一步了解使用组件相关细节参数。
|
||||
|
||||
插件地址:https://ext.dcloud.net.cn/plugin?id=11840
|
||||
|
||||
### 功能一览
|
||||
|
||||
1. 下拉列表(单选)
|
||||
2. 点击常亮
|
||||
3. 点击排序
|
||||
4. 下拉筛选(单选按钮、多选按钮、滑动选择器)
|
||||
5. 级联筛选(单选)
|
||||
6. 日期筛选(日期快选、日期区间选择)
|
||||
7. 顶部搜索
|
||||
8. 自定插槽
|
||||
|
||||
### 组件示例
|
||||
|
||||
```jsx
|
||||
<template>
|
||||
<DaDropdown
|
||||
:dropdownMenu="dropdownMenuList"
|
||||
themeColor="#007aff"
|
||||
textColor="#333333"
|
||||
:duration="300"
|
||||
fixedTop
|
||||
@confirm="handleConfirm"
|
||||
@close="handleClose"
|
||||
@open="handleOpen">
|
||||
<template #slot1="{item,index}">
|
||||
<view style="padding: 40px">自定义插槽内容</view>
|
||||
</template>
|
||||
</DaDropdown>
|
||||
</template>
|
||||
```
|
||||
|
||||
```js
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
import DaDropdown from '@/components/da-dropdown/index.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { DaDropdown },
|
||||
setup() {
|
||||
const dropdownMenuList = ref([
|
||||
// 演示数据请看下方各模块说明或下载示例项目查看
|
||||
// ...
|
||||
])
|
||||
function handleConfirm(v) {
|
||||
console.log('handleConfirm ==>', v)
|
||||
}
|
||||
function handleClose(v) {
|
||||
console.log('handleClose ==>', v)
|
||||
}
|
||||
function handleOpen(v) {
|
||||
console.log('handleOpen ==>', v)
|
||||
}
|
||||
return {
|
||||
dropdownMenuList,
|
||||
handleConfirm,
|
||||
handleClose,
|
||||
handleOpen,
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 组件参数
|
||||
|
||||
| 属性 | 类型 | 默认值 | 必填 | 说明 |
|
||||
| :------------------- | :-------- | :-------- | :--- | :--------------------------------- |
|
||||
| v-model:dropdownMenu | `Array` | `[]` | 是 | 导航菜单数据 |
|
||||
| themeColor | `String` | `#007aff` | 否 | 主题颜色 |
|
||||
| textColor | `String` | `#333333` | 否 | 导航文字颜色 |
|
||||
| bgColor | `String` | `#ffffff` | 否 | 背景颜色,当固定在顶部时,此为必填 |
|
||||
| fixedTop | `Boolean` | `false` | 否 | 是否固定在顶部 |
|
||||
| fixedTopValue | `Number` | `0` | 否 | 固定在头部时的位置,单位 px |
|
||||
| duration | `Number` | `300` | 否 | 弹窗动画的过渡时间 |
|
||||
|
||||
> 温馨提示:如果页面定义了 "navigationStyle": "custom" ,因此固定头部时需要额外获取状态栏高度,以免被异形屏头部覆盖,此时的 fixedTopValue 的作用就出来了,通过 fixedTopValue 自定义加减固定头部所处的位置。
|
||||
|
||||
|
||||
### 组件事件
|
||||
|
||||
| 事件名称 | 回调参数 | 说明 |
|
||||
| :------- | :------------------------- | :----------------------------------------------------------------- |
|
||||
| open | `(index) => void` | 打开弹窗时回调 |
|
||||
| close | `(index,menuList) => void` | 关闭弹窗时回调 |
|
||||
| confirm | `(value,data) => void` | 确定选择内容时回调,返回选择的数据,格式`{'菜单项prop值': '内容'}` |
|
||||
|
||||
|
||||
### 组件方法
|
||||
|
||||
| 事件名称 | 回调参数 | 说明 |
|
||||
| :---------------- | :------------------------- | :-------------------------------------- |
|
||||
| openMenuItemPopup | `(index) => void` | 打开指定位置的菜单项弹窗 |
|
||||
| closeMenuPopup | `() => void` | 关闭菜单项弹窗 |
|
||||
| getMenuValue | `() => object` | 获取菜单存在的值 |
|
||||
| updateMenu | `(prop,value,key) => void` | 更新菜单项内容【参考示例7】 |
|
||||
| setMenuLoading | `(prop,state) => void` | 操作指定菜单项为加载中状态【参考示例7】 |
|
||||
| getMenuIndex | `(prop) => number` | 获取菜单项所在索引位置 |
|
||||
| getMenuList | `() => array` | 获取当前菜单列表数据【参考示例6】 |
|
||||
|
||||
|
||||
### 组件菜单项
|
||||
|
||||
#### dropdownMenu 基础参数
|
||||
|
||||
| 属性 | 类型 | 默认值 | 必填 | 说明 |
|
||||
| :---------- | :--------- | :----- | :--- | :--------------------------------------------------------------- |
|
||||
| title | `String` | - | 是 | 菜单名称 |
|
||||
| prop | `String` | - | 是 | 菜单 prop 值,**菜单项的 prop 是唯一的** |
|
||||
| type | `String` | - | 是 | 菜单类型,参考下方类型说明 |
|
||||
| syncDataFn | `Function` | - | 否 | 异步函数返回子项数据,优先级大于 options |
|
||||
| syncDataKey | `String` | - | 否 | 异步数据不是根数据时需要。支持嵌套,如:`data.list`【参考示例7】 |
|
||||
|
||||
除上方基础参数以外,不同的菜单项(type)会有额外的配置参数
|
||||
|
||||
**type 说明**
|
||||
**cell** 下拉列表
|
||||
**click** 点击
|
||||
**sort** 排序
|
||||
**filter** 复杂筛选
|
||||
**picker** 级联
|
||||
**daterange** 日期范围
|
||||
**search** 搜索框(菜单项 type 唯一)
|
||||
**slot** 弹窗插槽
|
||||
|
||||
#### 菜单项 - 下拉列表(cell)
|
||||
|
||||
| 属性 | 类型 | 默认值 | 必填 | 说明 |
|
||||
| :------- | :----------------- | :----------------------------------------------------- | :--- | :----------------------------------------- |
|
||||
| value | `Number`\|`String` | - | 否 | 默认值,和`options`的 value 必须保持同类型 |
|
||||
| showAll | `Boolean` | `false` | 否 | 是否显示 “不限” 项 |
|
||||
| showIcon | `Boolean` | `false` | 否 | 是否在选中时显示勾选图标 |
|
||||
| field | `Object` | `{ label: 'label', value: 'value', suffix: 'suffix' }` | 否 | 列表子项数据对应内容字段 |
|
||||
| options | `Array` | `[]` | 否 | 下拉列表子项数据 |
|
||||
|
||||
|
||||
```js
|
||||
// 简单示例
|
||||
const dropdownMenuList = [
|
||||
{
|
||||
title: '下拉',
|
||||
type: 'cell',
|
||||
prop: 'god1',
|
||||
showAll: true,
|
||||
showIcon: true,
|
||||
// value: '2', // 默认内容2
|
||||
options: [
|
||||
{ label: '下拉列表项1', value: '1', suffix: '副标题' },
|
||||
{ label: '下拉列表项2', value: '2' },
|
||||
{ label: '下拉列表项3', value: '3' },
|
||||
],
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
#### 菜单项 - 高亮(click)
|
||||
|
||||
| 属性 | 类型 | 默认值 | 必填 | 说明 |
|
||||
| :---- | :-------- | :----- | :--- | :-------------------------------- |
|
||||
| value | `Boolean` | - | 否 | 默认值,true 选中、false 取消选中 |
|
||||
|
||||
```js
|
||||
// 简单示例
|
||||
const dropdownMenuList = [
|
||||
{
|
||||
title: '点击',
|
||||
type: 'click',
|
||||
prop: 'god2',
|
||||
// value: true, // 默认选中
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
#### 菜单项 - 排序(sort)
|
||||
|
||||
| 属性 | 类型 | 默认值 | 必填 | 说明 |
|
||||
| :---- | :------------ | :----- | :--- | :-------------------------- |
|
||||
| value | `asc`\|`desc` | - | 否 | 默认值,asc 升序、desc 倒序 |
|
||||
|
||||
```js
|
||||
// 简单示例
|
||||
const dropdownMenuList = [
|
||||
{
|
||||
title: '排序',
|
||||
type: 'sort',
|
||||
prop: 'god3',
|
||||
// value: 'asc', // 默认升序
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
#### 菜单项 - 筛选(filter)
|
||||
|
||||
| 属性 | 类型 | 默认值 | 必填 | 说明 |
|
||||
| :------ | :------- | :----- | :--- | :------------------------------------------- |
|
||||
| value | `Object` | - | 否 | 默认值,格式`{ prop1: '值1', prop2: '值2' }` |
|
||||
| options | `Array` | `[]` | 否 | 筛选子项数据,**说明见下** |
|
||||
|
||||
##### filter -> options 参数说明
|
||||
|
||||
| 属性 | 类型 | 必填 | 说明 |
|
||||
| :------------- | :---------------------------- | :--- | :-------------------------------------------------------------------------------------------- |
|
||||
| title | `String` | 是 | 筛选项的子项标题 |
|
||||
| type | `radio`\|`checkbox`\|`slider` | 是 | 筛选项的子项类型,可选 radio 单选按钮、checkbox 多选按钮、slider 滑动选择器 |
|
||||
| prop | `String` | 是 | 筛选项的子项 prop,**注意保持子项 prop 唯一** |
|
||||
| componentProps | `Object` | 否 | 筛选项的对应的组件配置,[slider 组件配置](https://uniapp.dcloud.net.cn/component/slider.html) |
|
||||
| options | `Array` | 否 | 筛选子项的类型对应的数据 |
|
||||
|
||||
```js
|
||||
// 简单示例
|
||||
const dropdownMenuList = [
|
||||
{
|
||||
title: '筛选',
|
||||
type: 'filter',
|
||||
prop: 'god4',
|
||||
// 默认选中单选2、多选2、3、滑动30
|
||||
// value: { ft1: '2', ft2: ['2', '3'], ft3: 30 },
|
||||
options: [
|
||||
{
|
||||
title: '单选',
|
||||
type: 'radio',
|
||||
prop: 'ft1',
|
||||
options: [
|
||||
{ label: '单选1', value: '1' },
|
||||
{ label: '单选2', value: '2' }
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '多选',
|
||||
type: 'checkbox',
|
||||
prop: 'ft2',
|
||||
options: [
|
||||
{ label: '多选1', value: '1' },
|
||||
{ label: '多选2', value: '2' }
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '滑块',
|
||||
type: 'slider',
|
||||
prop: 'ft3',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
showValue: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
#### 菜单项 - 级联(picker)
|
||||
|
||||
| 属性 | 类型 | 默认值 | 必填 | 说明 |
|
||||
| :---------- | :--------- | :--------------------------------------------------------- | :--- | :--------------------------------------------------------------- |
|
||||
| value | `Array` | - | 否 | 默认值,格式`['一级value', '二级value']` |
|
||||
| showAll | `Boolean` | `false` | 否 | 是否显示 “不限” 项 |
|
||||
| showIcon | `Boolean` | `false` | 否 | 是否在选中末级时显示勾选图标 |
|
||||
| field | `Object` | `{ label: 'label', value: 'value', children: 'children' }` | 否 | 级联子项数据对应内容字段 |
|
||||
| options | `Array` | `[]` | 否 | 级联子项数据 |
|
||||
| syncDataFn | `Function` | - | 否 | 异步函数返回级联子项数据,优先级大于 options |
|
||||
| syncDataKey | `String` | - | 否 | 异步数据不是根数据时需要。支持嵌套,如:`data.list`【参考示例7】 |
|
||||
|
||||
```js
|
||||
// 简单示例
|
||||
const dropdownMenuList = [
|
||||
{
|
||||
title: '级联选择',
|
||||
type: 'picker',
|
||||
prop: 'god5',
|
||||
showAll: true,
|
||||
showIcon: true,
|
||||
// showAll 为true时相当于在options第一的位置插入“不限”项
|
||||
// { label: '不限', value: '-9999' },
|
||||
field: {
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
children: 'children',
|
||||
},
|
||||
// value: ['2', '22'], // 默认选中 级联X22
|
||||
options: [
|
||||
{
|
||||
label: '级联X1',
|
||||
value: '1',
|
||||
children: [
|
||||
{ label: '级联X11', value: '11' },
|
||||
{ label: '级联X12', value: '12' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '级联X2',
|
||||
value: '2',
|
||||
children: [
|
||||
{ label: '级联X21', value: '21' },
|
||||
{ label: '级联X22', value: '22' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
#### 菜单项 - 日期(daterange)
|
||||
|
||||
| 属性 | 类型 | 默认值 | 必填 | 说明 |
|
||||
| :-------- | :-------- | :----- | :--- | :--------------------------------------------------- |
|
||||
| value | `Object` | - | 否 | 默认值,格式`{ start: '开始日期', end: '结束日期' }` |
|
||||
| showQuick | `Boolean` | `true` | 否 | 是否显示日期快选 |
|
||||
|
||||
```js
|
||||
// 简单示例
|
||||
const dropdownMenuList = [
|
||||
{
|
||||
title: '日期范围',
|
||||
type: 'daterange',
|
||||
prop: 'god6',
|
||||
// 默认选中 2022-01-01到2022-02-01
|
||||
// value: { start: '2022-01-01', end: '2022-02-01' },
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
#### 菜单项 - 顶部搜索框(search)
|
||||
|
||||
|
||||
当存在此类型时,头部将会展示搜索框,**注意:此类型唯一**
|
||||
|
||||
| 属性 | 类型 | 默认值 | 必填 | 说明 |
|
||||
| :---- | :------- | :----- | :--- | :----- |
|
||||
| value | `String` | - | 否 | 默认值 |
|
||||
|
||||
```js
|
||||
// 简单示例
|
||||
const dropdownMenuList = [
|
||||
{
|
||||
title: '搜索',
|
||||
type: 'search',
|
||||
prop: 'god0',
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
#### 菜单项 - 拓展插槽(slot1、slot2、slot3、slot4、slot5)
|
||||
|
||||
拓展插槽有 5 个,足以应付业务需求了,类型名称为`slot1`、`slot2`、`slot3`、`slot4`、`slot5`,这是固定的类型值
|
||||
|
||||
| 属性 | 类型 | 默认值 | 必填 | 说明 |
|
||||
| :---- | :------- | :----- | :--- | :----- |
|
||||
| value | `String` | - | 否 | 默认值 |
|
||||
|
||||
```jsx
|
||||
// 简单示例
|
||||
<template>
|
||||
<DaDropdown>
|
||||
<template #slot1="{item,index}">
|
||||
<view>自定义插槽2内容 {{item.value}} {{index}}</view>
|
||||
</template>
|
||||
<template #slot2="{item,index}">
|
||||
<view>自定义插槽2内容 {{item.value}} {{index}}</view>
|
||||
</template>
|
||||
<template #slot3="{item,index}">
|
||||
<view>自定义插槽3内容 {{item.value}} {{index}}</view>
|
||||
</template>
|
||||
<template #slot4="{item,index}">
|
||||
<view>自定义插槽4内容 {{item.value}} {{index}}</view>
|
||||
</template>
|
||||
<template #slot5="{item,index}">
|
||||
<view>自定义插槽5内容 {{item.value}} {{index}}</view>
|
||||
</template>
|
||||
</DaDropdown>
|
||||
</template>
|
||||
```
|
||||
|
||||
```js
|
||||
const dropdownMenuList = [
|
||||
{
|
||||
title: '插槽1',
|
||||
type: 'slot1',
|
||||
prop: 'god1',
|
||||
},
|
||||
{
|
||||
title: '插槽2',
|
||||
type: 'slot2',
|
||||
prop: 'god2',
|
||||
},
|
||||
{
|
||||
title: '插槽3',
|
||||
type: 'slot3',
|
||||
prop: 'god3',
|
||||
},
|
||||
{
|
||||
title: '插槽4',
|
||||
type: 'slot4',
|
||||
prop: 'god4',
|
||||
},
|
||||
{
|
||||
title: '插槽5',
|
||||
type: 'slot5',
|
||||
prop: 'god5',
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
### 组件版本
|
||||
|
||||
v2.2.2
|
||||
|
||||
### 差异化
|
||||
|
||||
已通过测试
|
||||
|
||||
> - H5 页面
|
||||
> - 微信小程序
|
||||
> - 支付宝、钉钉小程序
|
||||
> - 字节跳动、抖音、今日头条小程序
|
||||
> - 百度小程序
|
||||
> - 飞书小程序
|
||||
> - QQ 小程序
|
||||
> - 京东小程序
|
||||
|
||||
未测试
|
||||
|
||||
> - 快手小程序由于非企业用户暂无演示
|
||||
> - 快应用、360 小程序因 Vue3 支持的原因暂无演示
|
||||
|
||||
### 开发组
|
||||
|
||||
[@CRLANG](https://crlang.com)
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
/**
|
||||
* 菜单项-下拉配置
|
||||
*/
|
||||
export interface DaCellOption {
|
||||
/**
|
||||
* 是否显示“不限”选项
|
||||
*/
|
||||
showAll?: boolean
|
||||
/**
|
||||
* 是否显示勾选图标
|
||||
*/
|
||||
showIcon?: boolean
|
||||
}
|
||||
/**
|
||||
* 菜单项-点击配置
|
||||
*/
|
||||
export interface DaClickOption {}
|
||||
/**
|
||||
* 菜单项-排序配置
|
||||
*/
|
||||
export interface DaSortOption {}
|
||||
/**
|
||||
* 菜单项-筛选配置
|
||||
*/
|
||||
export interface DaFilterOption {}
|
||||
/**
|
||||
* 菜单项-级联配置
|
||||
*/
|
||||
export interface DaPickerOption {
|
||||
/**
|
||||
* 是否显示“不限”选项
|
||||
*/
|
||||
showAll?: boolean
|
||||
/**
|
||||
* 是否显示勾选图标
|
||||
*/
|
||||
showIcon?: boolean
|
||||
field?: {
|
||||
label: string
|
||||
value: string
|
||||
children: string
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 菜单项-日期范围配置
|
||||
*/
|
||||
export interface DaDaterangeOption {
|
||||
value?: {
|
||||
start: string
|
||||
end: string
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 下拉列表-项内容
|
||||
*/
|
||||
export interface DaCellItemOption extends DaDropdownMenuListOption {
|
||||
/**
|
||||
* 右侧子标题
|
||||
*/
|
||||
suffix?: string
|
||||
}
|
||||
/**
|
||||
* 筛选-项内容
|
||||
*/
|
||||
export interface DaFilterItemOption {
|
||||
/**
|
||||
* 筛选标题
|
||||
*/
|
||||
title: string
|
||||
/**
|
||||
* 筛选类型,可选 radio 单选按钮、checkbox 多选按钮、slider 滑动选择器
|
||||
*/
|
||||
type: 'radio' | 'checkbox' | 'slider'
|
||||
/**
|
||||
* 筛选项prop
|
||||
*/
|
||||
prop: string
|
||||
/**
|
||||
* 已选内容
|
||||
*/
|
||||
value?: string | number | string[] | number[]
|
||||
/**
|
||||
* 筛选项-slider子项组件prop,具体参考 https://uniapp.dcloud.net.cn/component/slider.html
|
||||
*/
|
||||
componentProp?: object
|
||||
/**
|
||||
* 筛选项-子项
|
||||
*/
|
||||
options?: DaDropdownMenuListOption[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 级联-项内容
|
||||
*/
|
||||
export interface DaPickerItem extends DaDropdownMenuListOption {
|
||||
isActived: boolean
|
||||
/**
|
||||
* 子项
|
||||
*/
|
||||
children?: DaPickerItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单列表选择项
|
||||
*/
|
||||
export interface DaDropdownMenuListOption {
|
||||
/**
|
||||
* 选择项标题
|
||||
*/
|
||||
label: string
|
||||
/**
|
||||
* 选择项内容
|
||||
*/
|
||||
value: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单项
|
||||
*/
|
||||
export interface DaDropdownMenuListItem extends DaCellOption, DaClickOption, DaSortOption, DaFilterOption, DaPickerOption {
|
||||
/**
|
||||
* 菜单标题
|
||||
*/
|
||||
title: string
|
||||
/**
|
||||
* 菜单类型
|
||||
* 可选:cell 下拉选择、click 点击、sort 排序、filter 复杂筛选、picker 级联、daterange 日期范围
|
||||
*/
|
||||
type: 'cell' |'click' | 'sort' | 'filter' | 'picker'| 'daterange'
|
||||
/**
|
||||
* 菜单项prop
|
||||
*/
|
||||
prop: string
|
||||
/**
|
||||
* 菜单值
|
||||
*/
|
||||
value?: string
|
||||
/**
|
||||
* 菜单选项函数,优先级大于 options
|
||||
*/
|
||||
syncDataFn?: Function
|
||||
/**
|
||||
* 菜单选项
|
||||
*/
|
||||
options?: DaDropdownMenuListOption[] | DaFilterItemOption[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单列表
|
||||
*/
|
||||
export type DaDropdownMenuList = DaDropdownMenuListItem[]
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
/**
|
||||
* 深拷贝内容
|
||||
* @param originData 拷贝对象
|
||||
* @author crlang(https://crlang.com)
|
||||
*/
|
||||
export function deepClone(originData) {
|
||||
const type = Object.prototype.toString.call(originData)
|
||||
let data
|
||||
if (type === '[object Array]') {
|
||||
data = []
|
||||
for (let i = 0; i < originData.length; i++) {
|
||||
data.push(deepClone(originData[i]))
|
||||
}
|
||||
} else if (type === '[object Object]') {
|
||||
data = {}
|
||||
for (const prop in originData) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (originData.hasOwnProperty(prop)) {
|
||||
// 非继承属性
|
||||
data[prop] = deepClone(originData[prop])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data = originData
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
export function getValueByKey(object, path, defaultVal = undefined) {
|
||||
console.log('object, path', object, path)
|
||||
// 先将path处理成统一格式
|
||||
let newPath = []
|
||||
if (Array.isArray(path)) {
|
||||
newPath = path
|
||||
} else {
|
||||
// 先将字符串中的'['、']'去除替换为'.',split分割成数组形式
|
||||
newPath = path.replace(/\[/g, '.').replace(/\]/g, '').split('.')
|
||||
}
|
||||
|
||||
// 递归处理,返回最后结果
|
||||
return (
|
||||
newPath.reduce((o, k) => {
|
||||
console.log(o, k) // 此处o初始值为下边传入的 object,后续值为每次取的内部值
|
||||
return (o || {})[k]
|
||||
}, object) || defaultVal
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理部分初始数据
|
||||
* @param data
|
||||
*/
|
||||
export function checkDataField(options, fields) {
|
||||
if (!fields || !options || options.length === 0) {
|
||||
return options
|
||||
}
|
||||
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
const k = options[i]
|
||||
k.label = k[fields.label || 'label'] || null
|
||||
k.value = k[fields.value || 'value'] || null
|
||||
k.suffix = k[fields.suffix || 'suffix'] || null
|
||||
k.children = k[fields.children || 'children'] || null
|
||||
if (k.children?.length) {
|
||||
k.options = checkDataField(k.options)
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数值-个位数补零
|
||||
* @param n 数值
|
||||
* @author crlang(https://crlang.com)
|
||||
*/
|
||||
export function formatNumber(n) {
|
||||
let s = parseInt(n)
|
||||
if (isNaN(s)) {
|
||||
s = '0'
|
||||
} else {
|
||||
s = s.toString()
|
||||
}
|
||||
return s[1] ? s : `0${s}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* @param date 时间对象
|
||||
* @param format 格式
|
||||
* @author crlang(https://crlang.com)
|
||||
*/
|
||||
export function formatTime(date, format) {
|
||||
const daDate = new Date(date.toString().length < 11 ? date * 1000 : date)
|
||||
const fromatsRule = ['y', 'm', 'd', 'h', 'i', 's']
|
||||
let tmp = []
|
||||
const year = daDate.getFullYear()
|
||||
const month = daDate.getMonth() + 1
|
||||
const day = daDate.getDate()
|
||||
const hour = daDate.getHours()
|
||||
const minute = daDate.getMinutes()
|
||||
const second = daDate.getSeconds()
|
||||
|
||||
if (format) {
|
||||
tmp.push(year, month, day, hour, minute, second)
|
||||
tmp = tmp.map(formatNumber)
|
||||
for (let i = 0; i < tmp.length; i++) {
|
||||
format = format.toLowerCase().replace(fromatsRule[i], tmp[i])
|
||||
}
|
||||
return format
|
||||
}
|
||||
|
||||
return `${[year, month, day].map(formatNumber).join('/')} ${[hour, minute, second]
|
||||
.map(formatNumber)
|
||||
.join(':')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某个时间范围
|
||||
*
|
||||
* @param v -1、-7、-14、-30、-60
|
||||
* @returns object {start: y-m-d,end: y-m-d}
|
||||
* @author crlang(https://crlang.com)
|
||||
*/
|
||||
export function getRangeDate(v) {
|
||||
const now = new Date()
|
||||
const nowTime = now.getTime()
|
||||
const oneDay = 24 * 60 * 60 * 1000
|
||||
const dateRange = { start: '', end: '' }
|
||||
const nowWeekDay = now.getDay() // 今天本周的第几天
|
||||
const nowDay = now.getDate() // 当前日
|
||||
const nowMonth = now.getMonth() // 当前月
|
||||
const nowYear = now.getFullYear() // 当前年
|
||||
|
||||
/**
|
||||
* 获得某个月的天数
|
||||
* @param month 当前月份
|
||||
*/
|
||||
const getMonthDays = function (month) {
|
||||
const monthStartDate = new Date(nowYear, month, 1)
|
||||
const monthEndDate = new Date(nowYear, month + 1, 1)
|
||||
const days = (monthEndDate - monthStartDate) / oneDay
|
||||
return days
|
||||
}
|
||||
|
||||
// 昨日
|
||||
if (v === '-1') {
|
||||
dateRange.start = formatTime(new Date(nowTime - oneDay), 'y-m-d')
|
||||
dateRange.end = dateRange.start
|
||||
// 本周
|
||||
} else if (v === '-7') {
|
||||
const weekStart = new Date(nowYear, nowMonth, nowDay - nowWeekDay + 1)
|
||||
const weekEnd = new Date(nowTime + oneDay) // 今日
|
||||
dateRange.start = formatTime(weekStart, 'y-m-d')
|
||||
dateRange.end = formatTime(weekEnd, 'y-m-d')
|
||||
// 上周
|
||||
} else if (v === '-14') {
|
||||
const weekStart = new Date(nowYear, nowMonth, nowDay - nowWeekDay - 6)
|
||||
const weekEnd = new Date(nowYear, nowMonth, nowDay - nowWeekDay)
|
||||
dateRange.start = formatTime(weekStart, 'y-m-d')
|
||||
dateRange.end = formatTime(weekEnd, 'y-m-d')
|
||||
// 本月
|
||||
} else if (v === '-30') {
|
||||
const monthStart = new Date(nowYear, nowMonth, 1)
|
||||
const monthEnd = new Date(nowTime + oneDay)
|
||||
dateRange.start = formatTime(monthStart, 'y-m-d')
|
||||
dateRange.end = formatTime(monthEnd, 'y-m-d')
|
||||
// 上月
|
||||
} else if (v === '-60') {
|
||||
const lastMonthDate = new Date() // 上月日期
|
||||
lastMonthDate.setDate(1)
|
||||
lastMonthDate.setMonth(lastMonthDate.getMonth() - 1)
|
||||
const lastMonth = lastMonthDate.getMonth()
|
||||
const lastMonthStart = new Date(nowMonth === 0 ? nowYear - 1 : nowYear, lastMonth, 1)
|
||||
const lastMonthEnd = new Date(
|
||||
nowMonth === 0 ? nowYear - 1 : nowYear,
|
||||
lastMonth,
|
||||
getMonthDays(lastMonth)
|
||||
)
|
||||
dateRange.start = formatTime(lastMonthStart, 'y-m-d')
|
||||
dateRange.end = formatTime(lastMonthEnd, 'y-m-d')
|
||||
} else {
|
||||
// 传入 v 为整数是即为近 xx 天
|
||||
if (v > 0) {
|
||||
dateRange.start = formatTime(new Date(nowTime - oneDay * parseInt(v)), 'y-m-d')
|
||||
dateRange.end = formatTime(new Date(nowTime - oneDay), 'y-m-d') // 不含今天
|
||||
}
|
||||
}
|
||||
return dateRange
|
||||
}
|
||||
|
||||
export const menuInitOpts = {
|
||||
cell: {
|
||||
showArrow: true
|
||||
},
|
||||
click: {},
|
||||
sort: {
|
||||
showSort: true
|
||||
},
|
||||
filter: {
|
||||
showArrow: true
|
||||
},
|
||||
picker: {
|
||||
showArrow: true
|
||||
},
|
||||
daterange: {
|
||||
showQuick: true,
|
||||
showArrow: true
|
||||
},
|
||||
slot: {
|
||||
showArrow: true
|
||||
},
|
||||
search: {
|
||||
showSearch: true
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,10 @@
|
|||
@click="cellClick($event, ite.name, item[ite.name])"
|
||||
:id="`${index + 1}-${indey}`"
|
||||
>
|
||||
{{ ite.filters ? itemFilter(item, ite) : item[ite.name] }}
|
||||
<slot v-if="ite.slot" name="index" :row="item[ite.name]" />
|
||||
<template v-else>
|
||||
{{ ite.filters ? itemFilter(item, ite) : item[ite.name] }}
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -36,6 +39,8 @@ interface IColumn {
|
|||
name: string
|
||||
label: string
|
||||
width: number
|
||||
align: string
|
||||
slot?: boolean
|
||||
}
|
||||
interface IData {
|
||||
[key: string]: [string, number]
|
||||
|
|
@ -63,7 +68,8 @@ const thStyle = computed(() => (column: IColumn) => {
|
|||
const { width } = column
|
||||
return {
|
||||
width: `${width ? width : '100'}px`,
|
||||
minWidth: `${width ? width : '100'}px`
|
||||
minWidth: `${width ? width : '100'}px`,
|
||||
textAlign: column.align ?? 'left'
|
||||
}
|
||||
})
|
||||
const advanceData = ref<IData[]>([])
|
||||
|
|
@ -109,14 +115,14 @@ transData()
|
|||
align-items: center;
|
||||
width: 100%;
|
||||
.design-table-th {
|
||||
background: $gray-1;
|
||||
// background: $gray-1;
|
||||
height: 100%;
|
||||
line-height: 100rpx;
|
||||
padding: 0 40rpx 0 16rpx;
|
||||
border: 1px solid $gray-6;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: $gray-6 transparent $gray-6 $gray-6;
|
||||
// border: 1px solid $gray-6;
|
||||
// border-width: 1px;
|
||||
// border-style: solid;
|
||||
// border-color: $gray-6 transparent $gray-6 $gray-6;
|
||||
font-size: 30rpx;
|
||||
&:last-child {
|
||||
border-right-color: $gray-6;
|
||||
|
|
@ -130,12 +136,12 @@ transData()
|
|||
height: 100rpx;
|
||||
line-height: 100rpx;
|
||||
padding: 0 40rpx 0 16rpx;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent $gray-6 $gray-6;
|
||||
&:last-child {
|
||||
border-right-color: $gray-6;
|
||||
}
|
||||
// border-width: 1px;
|
||||
// border-style: solid;
|
||||
// border-color: transparent transparent $gray-6 $gray-6;
|
||||
// &:last-child {
|
||||
// border-right-color: $gray-6;
|
||||
// }
|
||||
// 文本显示省略号
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
<template>
|
||||
<view class="flex flex-col bg-white rounded-[16rpx] mb-[24rpx]">
|
||||
<text class="font-bold p-[24rpx]" v-if="title">{{ title }}</text>
|
||||
<view>
|
||||
<slot />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
<template>
|
||||
<view class="da-dropdown-picker" v-if="viewCol && viewCol.length">
|
||||
<view class="da-dropdown-picker-inner" v-for="(vc, vci) in viewCol" :key="vci">
|
||||
<scroll-view class="da-dropdown-picker-view" scroll-y>
|
||||
<view
|
||||
class="da-dropdown-picker-item"
|
||||
:class="vr.checked ? 'is-actived' : ''"
|
||||
v-for="(vr, vri) in viewRow[vci]"
|
||||
:key="vri"
|
||||
@click="handleSelect(vr, vci, vri)"
|
||||
>
|
||||
<text class="da-dropdown-picker-item--name">{{ vr.label }}</text>
|
||||
<text
|
||||
class="da-dropdown-picker-item--icon"
|
||||
v-if="vr.children && vr.children.length"
|
||||
></text>
|
||||
<text
|
||||
class="da-dropdown-picker-item--check"
|
||||
v-if="vr.checked && (!vr.children || vr.children.length === 0)"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { deepClone } from '@/components/da-dropdown/utils'
|
||||
import { defineComponent, ref, watch } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
dropdownItem: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
dropdownIndex: {
|
||||
type: Number
|
||||
}
|
||||
},
|
||||
emits: ['success'],
|
||||
setup(props, { emit }) {
|
||||
const viewCol = ref([])
|
||||
const viewRow = ref([])
|
||||
|
||||
function checkData(selected, list) {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const k = list[i]
|
||||
for (let j = 0; j < selected.length; j++) {
|
||||
const x = selected[j]
|
||||
if (k.value === x) {
|
||||
k.checked = true
|
||||
viewCol.value.push(k.value)
|
||||
viewRow.value.push(list)
|
||||
if (k.children?.length) {
|
||||
checkData(selected, k.children)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initData(item) {
|
||||
const list = deepClone(item || [])
|
||||
if (list?.length) {
|
||||
if (item.value?.length) {
|
||||
viewCol.value = []
|
||||
viewRow.value = []
|
||||
|
||||
checkData(item.value, list)
|
||||
} else {
|
||||
viewCol.value.push('tmpValue')
|
||||
viewRow.value.push(list)
|
||||
}
|
||||
} else {
|
||||
viewCol.value = []
|
||||
viewRow.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(item, colIndex, _rowIndex) {
|
||||
let lastItem = false
|
||||
viewCol.value.splice(colIndex)
|
||||
viewCol.value[colIndex] = item.value
|
||||
|
||||
if (viewRow.value[colIndex]?.length) {
|
||||
viewRow.value[colIndex].forEach(k => {
|
||||
k.checked = false
|
||||
})
|
||||
}
|
||||
|
||||
item.checked = true
|
||||
const list = item?.children || null
|
||||
|
||||
if (list?.length) {
|
||||
viewCol.value[colIndex + 1] = 'tmpValue'
|
||||
viewRow.value[colIndex + 1] = list
|
||||
lastItem = false
|
||||
} else {
|
||||
console.warn('最后一项', item)
|
||||
lastItem = true
|
||||
}
|
||||
|
||||
try {
|
||||
if (viewRow.value[colIndex + 1]?.length) {
|
||||
viewRow.value[colIndex + 1].forEach(k => {
|
||||
k.checked = false
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('try clean row data', e)
|
||||
// --
|
||||
}
|
||||
|
||||
if (lastItem) {
|
||||
if (props.dropdownItem?.prop) {
|
||||
// const res = { [props.dropdownItem.prop]: deepClone(viewCol.value) }
|
||||
// emit('success', res, viewCol.value, props.dropdownIndex)
|
||||
} else {
|
||||
// console.error(`菜单项${props.dropdownItem.title}未定义prop,返回内容失败`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.dropdownItem,
|
||||
v => {
|
||||
console.log(v)
|
||||
initData(v)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
return {
|
||||
viewCol,
|
||||
viewRow,
|
||||
|
||||
handleSelect
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.da-dropdown-picker {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-height: 60vh;
|
||||
overflow: hidden;
|
||||
line-height: 1;
|
||||
|
||||
&-inner {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&-view {
|
||||
display: flex;
|
||||
|
||||
/* #ifdef MP-ALIPAY */
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
|
||||
/* #endif */
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
+ .da-dropdown-picker-view {
|
||||
border-left: 1px solid #eee;
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--dropdown-text-color);
|
||||
text-align: left;
|
||||
|
||||
&--icon {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
|
||||
&::after {
|
||||
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
|
||||
font-family: 'iconfont' !important;
|
||||
font-size: 24rpx;
|
||||
font-style: normal;
|
||||
content: '\e600';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
&--check {
|
||||
flex-shrink: 0;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
|
||||
&::after {
|
||||
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
|
||||
font-family: 'iconfont' !important;
|
||||
font-size: 24rpx;
|
||||
font-style: normal;
|
||||
content: '\e677';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
&.is-actived {
|
||||
color: $blue-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<view>
|
||||
<text></text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<card>
|
||||
<view class="flex flex-col mb-[24rpx]" @click="handleCardClick">
|
||||
<view class="flex items-center p-[24rpx] gap-[24rpx]">
|
||||
<view
|
||||
class="bg-primary rounded-full text-white w-[120rpx] h-[120rpx] flex items-center justify-center"
|
||||
>
|
||||
张三
|
||||
</view>
|
||||
<view class="flex-1 flex flex-col gap-[12rpx]">
|
||||
<text>张三</text>
|
||||
<text class="text-muted">湛江团队-A组 . 招生老师</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex gap-[12rpx] flex-wrap">
|
||||
<view
|
||||
class="w-[30%] flex justify-center items-center flex-col gap-[12rpx]"
|
||||
v-for="(item, index) in data"
|
||||
:key="`unique_${index}`"
|
||||
>
|
||||
<text class="text-muted">{{ item.label }}</text>
|
||||
<text class="font-bold text-[40rpx]">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import card from '../../card.vue'
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['handleCardClick'])
|
||||
|
||||
const data = ref([
|
||||
{ label: '转化中', value: 368 },
|
||||
{ label: '已添加', value: 100 },
|
||||
{ label: '异常待处理', value: 368 },
|
||||
{ label: '成交客户', value: 368 },
|
||||
{ label: '战败客户', value: 368 }
|
||||
])
|
||||
const handleCardClick = () => {
|
||||
emit('handleCardClick', 'recruitsale', props.item)
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<card>
|
||||
<view class="flex flex-col mb-[24rpx]" @click="handleCardClick">
|
||||
<view class="flex items-center p-[24rpx] gap-[24rpx]">
|
||||
<view
|
||||
class="bg-primary rounded-full text-white w-[120rpx] h-[120rpx] flex items-center justify-center"
|
||||
>
|
||||
张三
|
||||
</view>
|
||||
<view class="flex-1 flex flex-col gap-[12rpx]">
|
||||
<text>张三</text>
|
||||
<text class="text-muted">湛江团队-A组 . 电销老师</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex gap-[12rpx]">
|
||||
<view
|
||||
class="flex-1 flex justify-center items-center flex-col gap-[12rpx]"
|
||||
v-for="(item, index) in data"
|
||||
:key="`unique_${index}`"
|
||||
>
|
||||
<text class="text-muted">{{ item.label }}</text>
|
||||
<text class="font-bold text-[40rpx]">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import card from '../../card.vue'
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['handleCardClick'])
|
||||
|
||||
const data = ref([
|
||||
{ label: '未领取', value: 368 },
|
||||
{ label: '已领取', value: 100 },
|
||||
{ label: '异常待处理', value: 368 }
|
||||
])
|
||||
|
||||
const handleCardClick = () => {
|
||||
emit('handleCardClick', 'telesale', props.item)
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
<template>
|
||||
<view class="flex-1 h-full flex flex-col">
|
||||
<u-dropdown ref="uDropdownRef" menu-icon="arrow-down-fill">
|
||||
<u-dropdown-item title="岗位">
|
||||
<view class="bg-white">
|
||||
<view>
|
||||
<view
|
||||
v-for="item in positionList"
|
||||
:key="item.id"
|
||||
class="cell p-[24rpx] border-b border-solid border-[#f1f1f1]"
|
||||
:class="{ active: postId == item.id }"
|
||||
@click="handleCellClick(item)"
|
||||
>
|
||||
<text>{{ item.label }}</text>
|
||||
<template v-if="postId == item.id">
|
||||
<TIcon name="icon-check" color="#0E66FB" />
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex justify-between p-[32rpx] gap-[32rpx]">
|
||||
<view class="flex-1">
|
||||
<u-button class="btn" type="primary" plain text="重置"></u-button>
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<u-button class="btn" type="primary" text="确定"></u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-dropdown-item>
|
||||
<u-dropdown-item title="组织">
|
||||
<view class="bg-white">
|
||||
<DropdownPicker :dropdownItem="dropdownMenuList" />
|
||||
<view class="flex justify-between p-[32rpx] gap-[32rpx]">
|
||||
<view class="flex-1">
|
||||
<u-button class="btn" type="primary" plain text="重置"></u-button>
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<u-button class="btn" type="primary" text="确定"></u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-dropdown-item>
|
||||
</u-dropdown>
|
||||
<view class="flex-1 mt-3 px-[32rpx] overflow-auto bg-[#FAFAFE]">
|
||||
<z-paging
|
||||
ref="paging"
|
||||
v-model="dataList"
|
||||
@query="queryList"
|
||||
:fixed="false"
|
||||
height="100%"
|
||||
>
|
||||
<template v-for="(item, index) in dataList" :key="`unique_${index}`">
|
||||
<telesale-card
|
||||
v-if="postId == 5"
|
||||
:item="item"
|
||||
@handleCardClick="handleCardClick"
|
||||
/>
|
||||
<recruitsale-card
|
||||
v-if="postId == 6"
|
||||
:item="item"
|
||||
@handleCardClick="handleCardClick"
|
||||
/>
|
||||
</template>
|
||||
</z-paging>
|
||||
</view>
|
||||
<!-- <DropdownPicker :dropdownItem="dropdownMenuList" /> -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import positionTabs from './components/position-tabs.vue'
|
||||
import DaDropdown from '@/components/da-dropdown/index.vue'
|
||||
import DropdownPicker from './components/orga-picker.vue'
|
||||
import telesaleCard from './components/telesale-card.vue'
|
||||
import recruitsaleCard from './components/recruitsale-card.vue'
|
||||
import { useZPaging } from '@/hooks/useZPaging'
|
||||
import { apiTeleClueList } from '@/api/clue'
|
||||
|
||||
const positionList = [
|
||||
{
|
||||
label: '电销老师',
|
||||
id: 5,
|
||||
value: 5
|
||||
},
|
||||
{
|
||||
label: '招生老师',
|
||||
id: 6,
|
||||
value: 6
|
||||
}
|
||||
]
|
||||
const postId = ref()
|
||||
const handleCellClick = (item: any) => {
|
||||
const { id } = item
|
||||
postId.value = id
|
||||
}
|
||||
const dropdownMenuList = [
|
||||
{
|
||||
label: '级联X1',
|
||||
value: '1',
|
||||
children: [
|
||||
{
|
||||
label: '级联X11',
|
||||
value: '11',
|
||||
children: [
|
||||
{ label: '级联X111', value: '111' },
|
||||
{ label: '级联X122', value: '122' }
|
||||
]
|
||||
},
|
||||
{ label: '级联X12', value: '12' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '级联X2',
|
||||
value: '2',
|
||||
children: [
|
||||
{ label: '级联X21', value: '21' },
|
||||
{ label: '级联X22', value: '22' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const queryParams = ref({
|
||||
likeWork: ''
|
||||
})
|
||||
const dataList = ref([])
|
||||
const { paging, queryList, refresh, changeApi, setParams } = useZPaging(
|
||||
queryParams.value,
|
||||
apiTeleClueList,
|
||||
() => {}
|
||||
)
|
||||
const handleCardClick = (type: string, item: any) => {
|
||||
const { id } = item
|
||||
switch (type) {
|
||||
case 'telesale':
|
||||
case 'recruitsale':
|
||||
uni.navigateTo({
|
||||
url: '/bundle/pages/clue-list/index?id=' + id + '&type=' + type
|
||||
})
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cell {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
&.active {
|
||||
color: $blue-1;
|
||||
}
|
||||
}
|
||||
:deep(.btn) {
|
||||
button {
|
||||
@apply h-[72rpx] rounded-[12rpx];
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<template>
|
||||
<view class="px-[32rpx]">
|
||||
<card title="线索转化情况分析">
|
||||
<view class="px-[32rpx]">
|
||||
<view
|
||||
class="flex flex-col gap-[16rpx] mb-[24rpx]"
|
||||
v-for="(item, index) in data"
|
||||
:key="`unique_${index}`"
|
||||
>
|
||||
<text class="text-gray5">{{ item.label }}</text>
|
||||
<u-line-progress
|
||||
:percentage="item.value"
|
||||
activeColor="#5783FE"
|
||||
shape="square"
|
||||
></u-line-progress>
|
||||
</view>
|
||||
</view>
|
||||
</card>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import card from '../../card.vue'
|
||||
const data = ref([
|
||||
{ label: '待领取', value: 28 },
|
||||
{ label: '转化中', value: 12 },
|
||||
{ label: '已添加', value: 53 },
|
||||
{ label: '异常待处理', value: 56 },
|
||||
{ label: '已成交', value: 12 },
|
||||
{ label: '已战败', value: 100 }
|
||||
])
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<view class="px-[32rpx]">
|
||||
<card v-for="(item, index) in data" :key="`unique_${index}`" :title="item.name">
|
||||
<view class="flex bg-gray3 mx-[24rpx] mb-[24rpx]">
|
||||
<view
|
||||
v-for="(itemy, indey) in item.children"
|
||||
:key="indey"
|
||||
class="flex-1 flex flex-col gap-[12rpx] justify-center items-center py-[12rpx]"
|
||||
>
|
||||
<text class="text-muted">{{ itemy.label }}</text>
|
||||
<text class="font-bold">{{ itemy.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</card>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import card from '../../card.vue'
|
||||
const data = [
|
||||
{
|
||||
name: '湛江团队',
|
||||
children: [
|
||||
{ label: '线索', value: '52个' },
|
||||
{ label: '成交客户', value: '52个' },
|
||||
{ label: '转化率', value: '15%' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '广州团队',
|
||||
children: [
|
||||
{ label: '线索', value: '52个' },
|
||||
{ label: '成交客户', value: '52个' },
|
||||
{ label: '转化率', value: '15%' }
|
||||
]
|
||||
}
|
||||
]
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<view class="flex flex-col gap-[24rpx] px-[32rpx]">
|
||||
<card title="数据简报">
|
||||
<view class="flex flex-wrap">
|
||||
<view
|
||||
class="flex flex-col w-1/3 justify-center items-center gap-[12rpx] mb-[20rpx]"
|
||||
v-for="(item, index) in data"
|
||||
:key="`unique_${index}`"
|
||||
>
|
||||
<text class="text-muted">{{ item.label }}</text>
|
||||
<text class="font-bold text-[40rpx]">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</card>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import card from '../../card.vue'
|
||||
|
||||
const data = ref([
|
||||
{ label: '新增跟进', value: 368 },
|
||||
{ label: '新增客户', value: 368 },
|
||||
{ label: '成交客户', value: 368 },
|
||||
{ label: '转化中客户', value: 368 },
|
||||
{ label: '异常待处理', value: 368 },
|
||||
{ label: '战败客户', value: 368 }
|
||||
])
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
<template>
|
||||
<view class="px-[32rpx]">
|
||||
<card>
|
||||
<u-tabs
|
||||
:list="tabs"
|
||||
:scrollable="false"
|
||||
:activeStyle="{
|
||||
color: '#303133',
|
||||
fontWeight: 'bold',
|
||||
transform: 'scale(1.05)'
|
||||
}"
|
||||
:inactiveStyle="{
|
||||
color: '#606266',
|
||||
transform: 'scale(1)'
|
||||
}"
|
||||
itemStyle="padding-left: 15px; padding-right: 15px; height: 34px;flex:0!important; "
|
||||
lineWidth="32"
|
||||
lineColor="#0E66FB"
|
||||
@change="handleChangeTab"
|
||||
></u-tabs>
|
||||
<TTable :columns="columns" :data="data">
|
||||
<template #index="scope">
|
||||
<text class="px-[16rpx] py-[6rpx] rounded-[4px]" :style="rankStyle(scope.row)">
|
||||
{{ scope.row }}
|
||||
</text>
|
||||
</template>
|
||||
</TTable>
|
||||
<view
|
||||
class="flex justify-center py-[20rpx] border-t border-solid border-border"
|
||||
@click="handleMore"
|
||||
>
|
||||
<text>查看更多</text>
|
||||
</view>
|
||||
</card>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef } from 'vue'
|
||||
import card from '../../card.vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const activeTab = ref(1)
|
||||
const tabs = shallowRef([
|
||||
{ name: '电销TOP5', value: 1 },
|
||||
{ name: '招生TOP5', value: 2 }
|
||||
])
|
||||
const totalLabel = computed(() => (activeTab.value == 1 ? '意向' : '成交'))
|
||||
const handleChangeTab = item => {
|
||||
activeTab.value = item.value
|
||||
columns.value = generateColumns()
|
||||
}
|
||||
const generateColumns = () => {
|
||||
return [
|
||||
{ name: 'index', label: '排名', slot: true },
|
||||
{ name: 'name', label: '姓名' },
|
||||
{ name: 'total', label: totalLabel.value + '客户数', width: 120, align: 'right' }
|
||||
]
|
||||
}
|
||||
|
||||
const columns = ref(generateColumns())
|
||||
const data = [
|
||||
{
|
||||
index: 1,
|
||||
name: '王小虎1789789789789789',
|
||||
total: 100
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
name: '王小虎2',
|
||||
total: 20
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
name: '王小虎2',
|
||||
total: 20
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
name: '王小虎2',
|
||||
total: 20
|
||||
}
|
||||
]
|
||||
const rankStyle = computed(() => row => {
|
||||
const styleMap: Record<number, string> = {
|
||||
'1': '#FCAE3C',
|
||||
'2': '#BCC1D8',
|
||||
'3': '#EEB286'
|
||||
}
|
||||
return {
|
||||
color: row >= 4 ? '#3d3d3d' : '#fff',
|
||||
background: styleMap[row]
|
||||
}
|
||||
})
|
||||
const handleMore = () => {
|
||||
uni.navigateTo({
|
||||
url: '/bundle/pages/rank-list/index'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<view class="flex flex-col gap-[24rpx] mb-[20rpx]">
|
||||
<data-overview />
|
||||
<converted-overview />
|
||||
<rank />
|
||||
<clue-status />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dataOverview from './components/data-overview.vue'
|
||||
import convertedOverview from './components/converted-overview.vue'
|
||||
import rank from './components/rank.vue'
|
||||
import clueStatus from './components/clue-status.vue'
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
|
@ -17,3 +17,7 @@ export enum stateEnum {
|
|||
NO_EXIST = 1, //账号不存在
|
||||
UN_PASS = 2 //账号未通过
|
||||
}
|
||||
export enum AdminTabEnum {
|
||||
TEAM = 1,
|
||||
PERSONALLY = 2
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import contractActive from '@/static/images/contract-active.png'
|
|||
import contractInActive from '@/static/images/contract-inactive.png'
|
||||
import profileInActive from '@/static/images/profile-inactive.png'
|
||||
import profileActive from '@/static/images/profile-active.png'
|
||||
import { type TabBarProp } from '@/stores/tabbar'
|
||||
import { useTabBarStore, type TabBarProp } from '@/stores/tabbar'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import cache from '@/utils/cache'
|
||||
import { ROLEINDEX } from '@/enums/cacheEnums'
|
||||
|
|
@ -69,6 +69,24 @@ export function useRoleData() {
|
|||
activeIcon: profileActive
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '主账号',
|
||||
ids: [],
|
||||
tabBarList: [
|
||||
{
|
||||
text: '首页',
|
||||
inactiveIcon: order,
|
||||
activeIcon: orderActive,
|
||||
path: '/pages/admin/home/index'
|
||||
},
|
||||
{
|
||||
text: '个人中心',
|
||||
path: '/pages/admin/my/index',
|
||||
inactiveIcon: profileInActive,
|
||||
activeIcon: profileActive
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
return {
|
||||
|
|
@ -95,3 +113,25 @@ export function useJudgeApprentice() {
|
|||
if (typeof userInfo.isApprentice == undefined) return false
|
||||
return userInfo?.isApprentice === ApprenticeEnum.NO ? false : true
|
||||
}
|
||||
export const setNextRoute = () => {
|
||||
const roles = useRoleData().roles
|
||||
const userStore = useUserStore()
|
||||
const tabBarStore = useTabBarStore()
|
||||
const { userInfo } = userStore
|
||||
const ind = cache.get(ROLEINDEX) || 0
|
||||
const obj = roles[ind]
|
||||
if (userInfo.postIds == 0 && obj.ids.length == 0) {
|
||||
tabBarStore.setTabBarList(obj.tabBarList)
|
||||
uni.reLaunch({
|
||||
url: obj.tabBarList[0].path
|
||||
})
|
||||
} else {
|
||||
const obj = roles.find(role => {
|
||||
return role.ids.includes(userInfo?.roles[ind]?.id)
|
||||
})
|
||||
tabBarStore.setTabBarList(obj?.tabBarList)
|
||||
uni.reLaunch({
|
||||
url: obj?.tabBarList[0].path || ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/admin/home/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
|
|
@ -50,6 +57,13 @@
|
|||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/admin/my/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": ""
|
||||
},
|
||||
"auth": true
|
||||
},
|
||||
|
||||
{
|
||||
"path": "pages/salesman/contract/index",
|
||||
|
|
@ -132,6 +146,18 @@
|
|||
{
|
||||
"root": "bundle",
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/rank-list/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "业绩排行榜"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/clue-list/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/clue/detail",
|
||||
"style": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
<template>
|
||||
<TContainer>
|
||||
<u-tabs
|
||||
:list="tabs"
|
||||
:scrollable="false"
|
||||
:itemStyle="{ height: '44px', flex: 1 }"
|
||||
:activeStyle="{ color: '#0E66FB' }"
|
||||
lineWidth="32"
|
||||
lineColor="#0E66FB"
|
||||
@change="handleChangeTab"
|
||||
></u-tabs>
|
||||
<view class="flex-1 bg-gray3 pt-[32rpx]">
|
||||
<admin-team v-if="activeTab === AdminTabEnum.TEAM" />
|
||||
<personally v-if="activeTab === AdminTabEnum.PERSONALLY" />
|
||||
</view>
|
||||
</TContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef } from 'vue'
|
||||
import { AdminTabEnum } from '@/enums'
|
||||
import adminTeam from '@/components/widgets/admin/team/index.vue'
|
||||
import personally from '@/components/widgets/admin/personally/index.vue'
|
||||
|
||||
const activeTab = ref(AdminTabEnum.PERSONALLY)
|
||||
const tabs = shallowRef([
|
||||
{ name: '团队', value: AdminTabEnum.TEAM },
|
||||
{ name: '个人', value: AdminTabEnum.PERSONALLY }
|
||||
])
|
||||
const handleChangeTab = item => {
|
||||
activeTab.value = item.value
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<TContainer></TContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<style scoped></style>
|
||||
|
|
@ -3,33 +3,20 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ROLEINDEX } from '@/enums/cacheEnums'
|
||||
import { useRoleData } from '@/hooks/useRoleData'
|
||||
import { useTabBarStore } from '@/stores/tabbar'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import cache from '@/utils/cache'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { setNextRoute } from '@/hooks/useRoleData'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { roles } = useRoleData()
|
||||
const tabBarStore = useTabBarStore()
|
||||
|
||||
const setNextRoute = () => {
|
||||
const ind = cache.get(ROLEINDEX) || 0
|
||||
const { userInfo } = userStore
|
||||
const obj = roles.find(role => {
|
||||
return role.ids.includes(userInfo.roles[ind].id)
|
||||
})
|
||||
tabBarStore.setTabBarList(obj?.tabBarList)
|
||||
uni.reLaunch({
|
||||
url: obj?.tabBarList[0].path || ''
|
||||
})
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
if (userStore.isLogin) {
|
||||
// 已经登录 => 首页
|
||||
setNextRoute()
|
||||
} else {
|
||||
// uni.navigateTo({
|
||||
// url: '/bundle/pages/login/login'
|
||||
// })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
@font-face {
|
||||
font-family: 'iconfont'; /* Project id 4837700 */
|
||||
src: url('//at.alicdn.com/t/c/font_4837700_mvrnof6x61s.woff2?t=1740644911077') format('woff2'),
|
||||
url('//at.alicdn.com/t/c/font_4837700_mvrnof6x61s.woff?t=1740644911077') format('woff'),
|
||||
url('//at.alicdn.com/t/c/font_4837700_mvrnof6x61s.ttf?t=1740644911077') format('truetype');
|
||||
src: url('//at.alicdn.com/t/c/font_4837700_x1g80sb5n5.woff2?t=1741073935675') format('woff2'),
|
||||
url('//at.alicdn.com/t/c/font_4837700_x1g80sb5n5.woff?t=1741073935675') format('woff'),
|
||||
url('//at.alicdn.com/t/c/font_4837700_x1g80sb5n5.ttf?t=1741073935675') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
|
|
@ -13,6 +13,14 @@
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-filter:before {
|
||||
content: '\e672';
|
||||
}
|
||||
|
||||
.icon-check:before {
|
||||
content: '\e677';
|
||||
}
|
||||
|
||||
.icon-clear:before {
|
||||
content: '\e601';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export default {
|
|||
inactiveColor: '#ececec',
|
||||
percentage: 0,
|
||||
showText: true,
|
||||
height: 12
|
||||
height: 12,
|
||||
shape: 'circle'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@ export const props = defineMixin({
|
|||
height: {
|
||||
type: [String, Number],
|
||||
default: () => defProps.lineProgress.height
|
||||
},
|
||||
// 进度条形状
|
||||
shape: {
|
||||
type: String,
|
||||
default: () => defProps.lineProgress.shape
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,149 +1,146 @@
|
|||
<template>
|
||||
<view
|
||||
class="u-line-progress"
|
||||
:style="[addStyle(customStyle)]"
|
||||
>
|
||||
<view
|
||||
class="u-line-progress__background"
|
||||
ref="u-line-progress__background"
|
||||
:style="[{
|
||||
backgroundColor: inactiveColor,
|
||||
height: addUnit(height),
|
||||
}]"
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
class="u-line-progress__line"
|
||||
:style="[progressStyle]"
|
||||
>
|
||||
<slot>
|
||||
<text v-if="showText && percentage >= 10" class="u-line-progress__text">{{innserPercentage + '%'}}</text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
<view class="u-line-progress" :style="[addStyle(customStyle)]">
|
||||
<view
|
||||
class="u-line-progress__background"
|
||||
ref="u-line-progress__background"
|
||||
:style="[
|
||||
{
|
||||
backgroundColor: inactiveColor,
|
||||
height: addUnit(height),
|
||||
borderRadius: shape == 'circle' ? '100px' : 'none'
|
||||
}
|
||||
]"
|
||||
></view>
|
||||
<view class="u-line-progress__line" :style="[progressStyle]">
|
||||
<slot>
|
||||
<text v-if="showText && percentage >= 10" class="u-line-progress__text">
|
||||
{{ innserPercentage + '%' }}
|
||||
</text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { props } from './props';
|
||||
import { mpMixin } from '../../libs/mixin/mpMixin';
|
||||
import { mixin } from '../../libs/mixin/mixin';
|
||||
import { addUnit, addStyle, sleep, range } from '../../libs/function/index';
|
||||
// #ifdef APP-NVUE
|
||||
const dom = uni.requireNativePlugin('dom')
|
||||
// #endif
|
||||
/**
|
||||
* lineProgress 线型进度条
|
||||
* @description 展示操作或任务的当前进度,比如上传文件,是一个线形的进度条。
|
||||
* @tutorial https://ijry.github.io/uview-plus/components/lineProgress.html
|
||||
* @property {String} activeColor 激活部分的颜色 ( 默认 '#19be6b' )
|
||||
* @property {String} inactiveColor 背景色 ( 默认 '#ececec' )
|
||||
* @property {String | Number} percentage 进度百分比,数值 ( 默认 0 )
|
||||
* @property {Boolean} showText 是否在进度条内部显示百分比的值 ( 默认 true )
|
||||
* @property {String | Number} height 进度条的高度,单位px ( 默认 12 )
|
||||
*
|
||||
* @example <u-line-progress :percent="70" :show-percent="true"></u-line-progress>
|
||||
*/
|
||||
export default {
|
||||
name: "u-line-progress",
|
||||
mixins: [mpMixin, mixin, props],
|
||||
data() {
|
||||
return {
|
||||
lineWidth: 0,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
percentage(n) {
|
||||
this.resizeProgressWidth()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
progressStyle() {
|
||||
let style = {}
|
||||
style.width = this.lineWidth
|
||||
style.backgroundColor = this.activeColor
|
||||
style.height = addUnit(this.height)
|
||||
return style
|
||||
},
|
||||
innserPercentage() {
|
||||
// 控制范围在0-100之间
|
||||
return range(0, 100, this.percentage)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
},
|
||||
methods: {
|
||||
addStyle,
|
||||
addUnit,
|
||||
init() {
|
||||
sleep(20).then(() => {
|
||||
this.resizeProgressWidth()
|
||||
})
|
||||
},
|
||||
getProgressWidth() {
|
||||
// #ifndef APP-NVUE
|
||||
return this.$uGetRect('.u-line-progress__background')
|
||||
// #endif
|
||||
import { props } from './props'
|
||||
import { mpMixin } from '../../libs/mixin/mpMixin'
|
||||
import { mixin } from '../../libs/mixin/mixin'
|
||||
import { addUnit, addStyle, sleep, range } from '../../libs/function/index'
|
||||
// #ifdef APP-NVUE
|
||||
const dom = uni.requireNativePlugin('dom')
|
||||
// #endif
|
||||
/**
|
||||
* lineProgress 线型进度条
|
||||
* @description 展示操作或任务的当前进度,比如上传文件,是一个线形的进度条。
|
||||
* @tutorial https://ijry.github.io/uview-plus/components/lineProgress.html
|
||||
* @property {String} activeColor 激活部分的颜色 ( 默认 '#19be6b' )
|
||||
* @property {String} inactiveColor 背景色 ( 默认 '#ececec' )
|
||||
* @property {String | Number} percentage 进度百分比,数值 ( 默认 0 )
|
||||
* @property {Boolean} showText 是否在进度条内部显示百分比的值 ( 默认 true )
|
||||
* @property {String | Number} height 进度条的高度,单位px ( 默认 12 )
|
||||
*
|
||||
* @example <u-line-progress :percent="70" :show-percent="true"></u-line-progress>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-line-progress',
|
||||
mixins: [mpMixin, mixin, props],
|
||||
data() {
|
||||
return {
|
||||
lineWidth: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
percentage(n) {
|
||||
this.resizeProgressWidth()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
progressStyle() {
|
||||
let style = {}
|
||||
style.width = this.lineWidth
|
||||
style.backgroundColor = this.activeColor
|
||||
style.height = addUnit(this.height)
|
||||
style.borderRadius = this.shape == 'circle' ? '100px' : 'none'
|
||||
return style
|
||||
},
|
||||
innserPercentage() {
|
||||
// 控制范围在0-100之间
|
||||
return range(0, 100, this.percentage)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
},
|
||||
methods: {
|
||||
addStyle,
|
||||
addUnit,
|
||||
init() {
|
||||
sleep(20).then(() => {
|
||||
this.resizeProgressWidth()
|
||||
})
|
||||
},
|
||||
getProgressWidth() {
|
||||
// #ifndef APP-NVUE
|
||||
return this.$uGetRect('.u-line-progress__background')
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-NVUE
|
||||
// 返回一个promise
|
||||
return new Promise(resolve => {
|
||||
dom.getComponentRect(this.$refs['u-line-progress__background'], (res) => {
|
||||
resolve(res.size)
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
resizeProgressWidth() {
|
||||
this.getProgressWidth().then(size => {
|
||||
const {
|
||||
width
|
||||
} = size
|
||||
// 通过设置的percentage值,计算其所占总长度的百分比
|
||||
this.lineWidth = width * this.innserPercentage / 100 + 'px'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// #ifdef APP-NVUE
|
||||
// 返回一个promise
|
||||
return new Promise(resolve => {
|
||||
dom.getComponentRect(this.$refs['u-line-progress__background'], res => {
|
||||
resolve(res.size)
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
resizeProgressWidth() {
|
||||
this.getProgressWidth().then(size => {
|
||||
const { width } = size
|
||||
// 通过设置的percentage值,计算其所占总长度的百分比
|
||||
this.lineWidth = (width * this.innserPercentage) / 100 + 'px'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/components.scss";
|
||||
@import '../../libs/css/components.scss';
|
||||
|
||||
.u-line-progress {
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
@include flex(row);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 100px;
|
||||
.u-line-progress {
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
@include flex(row);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
// border-radius: 100px;
|
||||
|
||||
&__background {
|
||||
background-color: #ececec;
|
||||
border-radius: 100px;
|
||||
flex: 1;
|
||||
}
|
||||
&__background {
|
||||
background-color: #ececec;
|
||||
// border-radius: 100px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
align-items: center;
|
||||
@include flex(row);
|
||||
color: #ffffff;
|
||||
border-radius: 100px;
|
||||
transition: width 0.5s ease;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
&__line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
align-items: center;
|
||||
@include flex(row);
|
||||
color: #ffffff;
|
||||
// border-radius: 100px;
|
||||
transition: width 0.5s ease;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 10px;
|
||||
align-items: center;
|
||||
text-align: right;
|
||||
color: #FFFFFF;
|
||||
margin-right: 5px;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
&__text {
|
||||
font-size: 10px;
|
||||
align-items: center;
|
||||
text-align: right;
|
||||
color: #ffffff;
|
||||
margin-right: 5px;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ module.exports = {
|
|||
gray2: '#7C7E82',
|
||||
gray3: '#F8F8F8',
|
||||
gray4: '#999',
|
||||
gray5: '#6E7079',
|
||||
lightblack: '#3D3D3D',
|
||||
border: '#F1F1F1',
|
||||
border2: '#F3F3F3',
|
||||
|
|
|
|||
Loading…
Reference in New Issue