测试上传添加水印

This commit is contained in:
xhc 2025-06-06 17:50:10 +08:00
parent b76a2e36f4
commit 54d8eb1bd2
27 changed files with 4474 additions and 105 deletions

View File

@ -2,7 +2,7 @@
* @Author: XHC
* @Date: 2025-05-19 10:21:48
* @LastEditors: XHC
* @LastEditTime: 2025-05-30 10:44:11
* @LastEditTime: 2025-06-06 17:37:54
* @Description:
-->
<script>
@ -37,6 +37,24 @@ export default {
color: #808185;
}
//
.top-back {
position: fixed;
right: 30rpx;
bottom: 180rpx;
width: 80rpx;
height: 80rpx;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
font-size: 28rpx;
z-index: 999;
}
//
.list-card {
.head {
@ -47,9 +65,10 @@ export default {
justify-content: space-between;
padding: 10rpx 0;
.title {
width: 70%;
display: inline-block;
height: 25rpx;
line-height: 25rpx;
height: 30rpx;
line-height: 30rpx;
padding-left: 15rpx;
border-left: 6rpx solid #0A61B9;
}
@ -65,9 +84,20 @@ export default {
background: #EDF3FD;
margin-right: 20rpx;
font-size: 24rpx;
.info-text {
display: inline-block;
width: 70%;
margin-left: 10rpx;
}
}
}
.title, .num, .info, .info-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
//
.search-box {

View File

@ -2,7 +2,7 @@
* @Author: xhc
* @Date: 2025-05-19 10:44:34
* @LastEditors: XHC
* @LastEditTime: 2025-05-29 17:28:49
* @LastEditTime: 2025-06-03 14:07:17
* @FilePath: \bht-app\src\api\api.js
* @Description: api
*/
@ -13,6 +13,11 @@ export const getRouteList = () => {
return http('/bht/patrolRoute/findList', 'post')
}
// 分页查询巡查路线
export const getRoutePage = () => {
return http('/bht/patrolRoute/findPage', 'post')
}
// 查询点位路线
export const getPointList = () => {
return http('/bht/patrolPoint/findList', 'post')

View File

@ -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. 日期筛选(日期快选、日期区间选择)

View File

@ -0,0 +1,172 @@
<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>

View File

@ -0,0 +1,209 @@
<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>

View File

@ -0,0 +1,220 @@
<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>

View File

@ -0,0 +1,75 @@
<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>

View File

@ -0,0 +1,220 @@
<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>

View File

@ -0,0 +1,898 @@
<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' : ''">
<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) {
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: absolute;
top: 0;
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);
}
}
}
&-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>

View File

@ -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)

View File

@ -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[]

View File

@ -0,0 +1,207 @@
/**
*
* @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,
},
}

View File

@ -0,0 +1,344 @@
<template>
<view class="points">
<view class="info-card">
<view class="info-box">
<view class="info-item" v-for="(item, index) in infoData" :key="index">
<view><text class="grey">{{ item.key }}</text>{{ item.value }}</view>
</view>
</view>
</view>
<scroll-view
scroll-x="true"
class="content-scroll"
:show-scrollbar="false"
enable-flex
>
<view
v-for="(item, index) in positionData"
:key="index"
:class="navCurIndex == index ? 'active' : ''"
class="control-item"
@click="changeTitle(index)"
>
{{ item.title }}
</view>
</scroll-view>
<view class="collapse-box">
<uni-collapse ref="collapseRef" v-model="collapseValue" accordion @change="changeCollapse">
<uni-collapse-item v-for="(val, collapseIndex) in collapseData" :key="collapseIndex">
<template v-slot:title>
<uni-section class="section" :title="val.title" type="line"></uni-section>
</template>
<view class="content">
<view class="content-item" v-for="(item, itemIndex) in val.children" :key="itemIndex">
<view class="title">
<view>{{ item.title }}</view>
<uni-tag :inverted="true" :text="item.tag" :type="item.tag == '重点关注' ? 'error' : 'warning'" />
</view>
<view class="btn">
<button size="mini" type="primary" @click="standard(item.standard)">巡检标准</button>
<button size="mini" type="primary" @click="history(item.history)">巡检历史</button>
</view>
<view :class="item.selectedImg ? '' : 'input'" v-if="item.tag">
<uni-easyinput class="textarea" type="textarea" v-model="item.described" placeholder="请输入" />
<uni-file-picker class="picker" mode="grid" :sourceType="['camera']" file-mediatype="image" limit="9" v-model="item.imageValue" @progress="progress" @select="(e) => selectImg(item, e, collapseIndex, itemIndex)" @delete="deleteImg(item, $event)"><uni-icons type="camera-filled" color="#004894" size="50"></uni-icons></uni-file-picker>
<!-- <WaterMarker :ref="el => { if (el) waterMarkRefs[`waterMarkRef_${collapseIndex}_${itemIndex}`] = el }"/> -->
<hpy-watermark :ref="el => { if (el) waterMarkRefs[`waterMarkRef_${collapseIndex}_${itemIndex}`] = el }" @waterMark="(path) => waterMark(item, path)"></hpy-watermark>
</view>
<view v-else>
<view class="state"><text>状态</text><uni-data-checkbox class="checkbox" mode="button" v-model="item.radio" :localdata="localdata"></uni-data-checkbox></view>
<view v-if="item.radio != 0">
<view class="described">描述<uni-easyinput class="textarea" type="textarea" v-model="item.described" placeholder="请输入" /></view>
<view>照片<uni-file-picker class="picker" mode="grid" :sourceType="['camera']" file-mediatype="image" limit="9" v-model="item.imageValue" @select="selectImg(item,$event)" @delete="deleteImg(item, $event)"><uni-icons type="camera-filled" color="#004894" size="50"></uni-icons></uni-file-picker></view>
</view>
</view>
</view>
</view>
</uni-collapse-item>
</uni-collapse>
</view>
<uni-popup ref="alertDialog" type="dialog">
<uni-popup-dialog type="warning" cancelText="关闭" confirmText="同意" title="通知" content="欢迎使用 uni-popup!" @confirm="dialogConfirm"
@close="dialogClose"></uni-popup-dialog>
</uni-popup>
<uni-popup ref="popup" background-color="#fff" @change="changePopup">
<view class="popup-content"><text class="text">{{ popValue }}</text></view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, reactive, nextTick, getCurrentInstance } from 'vue';
import ImageWatermarkPicker from "@/components/image-watermark-picker.vue"
import WaterMarker from '@/components/waterMarker.vue'
import HpyWatermark from '@/uni_modules/hpy-watermark/components/hpy-watermark/hpy-watermark.vue'
let waterMarkRefs = reactive({})
//
let popValue = ref(null)
let alertDialog = ref(null)
let navCurIndex = ref(0)
//
let infoData = reactive([
{ key: '点位名称', value: '厂外0.4KV供电点' },
{ key: '点位编码', value: '' },
])
//
let positionData = reactive([
{ id: 1, title: "左岸" },
{ id: 2, title: "右岸" },
{ id: 3, title: "大坝" }
])
let collapseRef = ref(null)
//
let collapseValue = ref('0')
//
let collapseData = reactive([
{ id: 1, title: '左岸进水口配电系统', children: [
{ title: '环境温湿度(°C/%)', tag: '重点关注', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: 'Ⅰ段干式变三相绕组温度', tag: '请确认', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: '带电显示装置及电磁锁正常;铁芯端部无散片、流胶;', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '盘柜无异常声音、气味;无放电、过热现象外观正常;开关状态及指示灯正常',radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '备自投及指示灯正常', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] }
] },
{ id: 2, title: '左岸排风竖井配电系统', children: [
{ title: '环境温湿度(°C/%)', tag: '重点关注', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: 'Ⅰ段干式变三相绕组温度', tag: '请确认', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: '带电显示装置及电磁锁正常;铁芯端部无散片、流胶;', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '盘柜无异常声音、气味;无放电、过热现象外观正常;开关状态及指示灯正常',radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '备自投及指示灯正常', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] }
] },
{ id: 3, title: '左岸尾水管配电系统(南端)', children: [
{ title: '环境温湿度(°C/%)', tag: '重点关注', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: 'Ⅰ段干式变三相绕组温度', tag: '请确认', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: '带电显示装置及电磁锁正常;铁芯端部无散片、流胶;', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '盘柜无异常声音、气味;无放电、过热现象外观正常;开关状态及指示灯正常',radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '备自投及指示灯正常', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] }
] },
{ id: 4, title: '左岸尾水管配电系统(北端)', children: [
{ title: '环境温湿度(°C/%)', tag: '重点关注', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: 'Ⅰ段干式变三相绕组温度', tag: '请确认', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: '带电显示装置及电磁锁正常;铁芯端部无散片、流胶;', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '盘柜无异常声音、气味;无放电、过热现象外观正常;开关状态及指示灯正常',radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '备自投及指示灯正常', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] }
] },
{ id: 5, title: '左岸尾水洞出口配电系统', children: [
{ title: '环境温湿度(°C/%)', tag: '重点关注', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: 'Ⅰ段干式变三相绕组温度', tag: '请确认', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: '带电显示装置及电磁锁正常;铁芯端部无散片、流胶;', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '盘柜无异常声音、气味;无放电、过热现象外观正常;开关状态及指示灯正常',radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '备自投及指示灯正常', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] }
] },
{ id: 6, title: '左岸水垫塘渗漏排水配电系统', children: [
{ title: '环境温湿度(°C/%)', tag: '重点关注', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: 'Ⅰ段干式变三相绕组温度', tag: '请确认', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: '带电显示装置及电磁锁正常;铁芯端部无散片、流胶;', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '盘柜无异常声音、气味;无放电、过热现象外观正常;开关状态及指示灯正常',radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '备自投及指示灯正常', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] }
] },
{ id: 7, title: '左岸水垫塘检修排水配电系统', children: [
{ title: '环境温湿度(°C/%)', tag: '重点关注', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: 'Ⅰ段干式变三相绕组温度', tag: '请确认', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: '带电显示装置及电磁锁正常;铁芯端部无散片、流胶;', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '盘柜无异常声音、气味;无放电、过热现象外观正常;开关状态及指示灯正常',radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '备自投及指示灯正常', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] }
] },
{ id: 8, title: '控制管理楼及左岸出线场配电系统', children: [
{ title: '环境温湿度(°C/%)', tag: '重点关注', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: 'Ⅰ段干式变三相绕组温度', tag: '请确认', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: '带电显示装置及电磁锁正常;铁芯端部无散片、流胶;', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '盘柜无异常声音、气味;无放电、过热现象外观正常;开关状态及指示灯正常',radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '备自投及指示灯正常', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] }
] },
])
//
let localdata = reactive([
{ text: '正常', value: 0 },
{ text: '待观察', value: 1 },
{ text: '异常', value: 2 }
])
//
const changeTitle = (index) => {
navCurIndex.value = index
}
//
const changeCollapse = () => {
// alertDialog.value.open()
}
//
const changePopup = () => {
}
//
const dialogConfirm = () => {
}
//
const dialogClose = () => {
}
const waterMark = (item, path) => {
console.log(path);
item.imageValue.push({ url: path });
console.log(item.imageValue);
};
//
const selectImg = (item, e, collapseIndex, itemIndex) => {
item.selectedImg = true
// ref key
const refKey = `waterMarkRef_${collapseIndex}_${itemIndex}`
const waterMarkerInstance = waterMarkRefs[refKey]
console.log(waterMarkerInstance);
if (waterMarkerInstance) {
// const imgFileArr = waterMarkerInstance.callAddWaterMark(e.tempFilePaths);
// imgFileArr.forEach((el) => {
// item.imageValue.push({
// url: el,
// extname: el.substring(el.lastIndexOf(".") + 1),
// name: el,
// });
// })
console.log(waterMarkerInstance);
var fillTexts = ["人员:张三", "地址广东省珠海市香洲区XXX"];
fillTexts.push("时间:");
//
waterMarkerInstance.addWaterMark({
filePaths: e.tempFilePaths,
fillTexts
});
} else {
console.warn('未找到对应的WaterMarker实例或方法未暴露', refKey, waterMarkerInstance)
}
}
const progress = (item, index) => {
uni.previewImage({
current: index,
urls: item.map((o) => o.url),
});
}
//
const deleteImg = (item,e) => {
item.imageValue.splice(e.index, 1)
console.log(item, e)
if (item.imageValue.length == 0) {
item.selectedImg = false
}
}
//
const standard = (val) => {
popup.value.open()
popValue.value = val
}
//
const history = (val) => {
popup.value.open()
popValue.value = val
}
</script>
<style lang="scss" scoped>
.points {
.content-scroll {
height: 100rpx;
line-height: 100rpx;
white-space: nowrap;
.control-item {
width: 160rpx;
height: 35rpx;
line-height: 35rpx;
display: inline-block;
padding: 0 20rpx;
margin-left: 30rpx;
position: relative;
&.active {
padding: 15rpx 20rpx!important;
}
}
}
.collapse-box {
margin: 10rpx 30rpx;
border-radius: 10rpx;
padding: 10rpx;
background: #fff;
:deep(.uni-icons) {
color: #1464BB!important;
}
}
.content-item {
padding: 20rpx;
margin: 0 20rpx 20rpx;
background: #F1F7FD;
border-radius: 10rpx;
.title, .btn, .input {
display: flex;
align-items: center;
}
.title {
color: #004895;
justify-content: space-between;
}
.btn {
:deep(uni-button) {
margin:30rpx 20rpx 30rpx 0!important;
}
}
.input {
.textarea {
width: 400rpx;
flex: none!important;
}
.picker {
margin-left: 10rpx;
:deep(.uni-file-picker__container) {
width: 580rpx;
height: 200rpx;
}
}
}
// .warp {
// display: block !important;
// :deep(.uni-file-picker__container) {
// width: auto !important;
// height: auto !important;
// }
// }
.state {
display: flex;
align-items: center;
.checkbox {
:deep(.uni-label-pointer) {
margin-right: 10rpx;
}
}
}
.described {
margin: 30rpx 0;
}
.textarea, .picker {
margin-top: 10rpx;
}
}
}
</style>

View File

@ -0,0 +1,275 @@
<template>
<view class="image-picker">
<uni-file-picker
v-model="imageValue"
:auto-upload="false"
:title="title"
:limit="limit"
:image-styles="imageStyles"
:file-mediatype="fileMediatype"
:mode="mode"
:sourceType="['camera']"
@select="select"
>
<view v-if="fileMediatype === 'image'" class="form-item-column-center">
<!-- <uni-icons type="image" size="30"></uni-icons>
<view :style="{ marginTop: '5px' }">上传照片</view>
<view>最多{{ limit }}</view> -->
<uni-icons type="camera-filled" color="#004894" size="50"></uni-icons>
</view>
</uni-file-picker>
<view class="watermark-canvas">
<canvas
id="watermark-canvas"
:style="{ width: canvasWidth, height: canvasHeight }"
canvas-id="watermark-canvas"
/>
</view>
</view>
</template>
<script>
export default {
name: 'ImageWatermarkPicker',
props: {
limit: {
type: [Number, String],
default: 1,
},
title: {
type: String,
default: null,
},
mode: {
type: String,
default: 'grid',
},
fileMediatype: {
type: String,
default: 'image',
},
imageStyles: {
type: Object,
default: null,
},
watermark: {
type: Boolean,
default: true,
},
// #ifdef VUE3
modelValue: {
type: Array,
default() {
return []
},
},
// #endif
// #ifndef VUE3
value: {
type: Array,
default() {
return []
},
},
// #endif
},
emits: ['input', 'update:modelValue'],
data() {
return {
imageValue: [],
canvasWidth: '1080px',
canvasHeight: '2160px',
}
},
watch: {
imageValue(newVal) {
// #ifdef VUE3
this.$emit('update:modelValue', newVal)
// #endif
// #ifndef VUE3
this.$emit('input', newVal)
// #endif
// this.$emit('change', newVal)
},
// #ifndef VUE3
value: {
handler(newVal) {
this.imageValue = newVal
},
immediate: true,
},
// #endif
// #ifdef VUE3
modelValue: {
handler(newVal) {
this.imageValue = newVal
},
immediate: true,
},
// #endif
},
methods: {
checkImage(url) {
const checkNum = 5
let currentCheckNum = 1
return new Promise((resolve, reject) => {
process()
function process() {
uni.getImageInfo({
src: url,
success: function (image) {
resolve(image)
},
fail: function (err) {
if (checkNum <= currentCheckNum) {
uni.showToast({ title: '图片上传失败', icon: 'none' })
reject(err)
} else {
currentCheckNum++
const timer = setTimeout(() => {
clearTimeout(timer)
process()
}, 300)
}
},
})
}
})
},
async select(e) {
for (let tempFile of e.tempFiles) {
await this.watermarkProcess(tempFile)
}
},
async watermarkProcess(tempFile) {
const { name, size, extname, uuid, path } = tempFile
let url = null
//
if (this.watermark) {
url = await this.addWatermark(path)
}
//
// url = await this.uploadFile(path)
//
await this.checkImage(url)
this.imageValue = [
...this.imageValue,
{
name,
extname,
url,
size,
uuid,
},
]
},
async addWatermark(tempFilePath) {
return new Promise((resolve, reject) => {
uni.getImageInfo({
src: tempFilePath,
success: async (res) => {
//
this.canvasWidth = `${res.width}px`
this.canvasHeight = `${res.height}px`
await this.sleep(1000) // canvas
this.$nextTick(() => {
const ctx = uni.createCanvasContext('watermark-canvas', this)
ctx.clearRect(0, 0, res.width, res.height)
ctx.beginPath()
ctx.drawImage(tempFilePath, 0, 0, res.width, res.height)
//
ctx.beginPath()
ctx.setFontSize(24)
ctx.setFillStyle('rgba(250,250,250,0.8)')
ctx.fillText('我是水印1', 60, res.height - 90)
ctx.fillText('我是水印2', 60, res.height - 60)
})
// (canvas -> )
ctx.draw(false, async () => {
await this.sleep(1000) // canvas
uni.canvasToTempFilePath(
{
canvasId: 'watermark-canvas',
destWidth: res.width,
destHeight: res.height,
fileType: 'jpg',
quality: 0.8,
success: (fileRes) => {
resolve(fileRes.tempFilePath)
},
fail: (err) => {
console.log('[Error draw]', err)
uni.showToast({ title: err.errMsg, icon: 'none' })
reject()
},
},
this,
)
})
},
fail: (err) => {
console.log('[Error getImageInfo]', err)
uni.showToast({ title: err.errMsg, icon: 'none' })
reject()
},
})
})
},
async uploadFile(path) {
const formData = {
'params.bizType': 'xxx',
'params.tags': 'xxx',
'meta.code': 'xxxx',
'meta.client': 'uniapp',
'meta.tag': 'xxx',
'meta.time': new Date().getTime(),
}
const res = await uni.uploadFile({
url: `${process.env.VUE_APP_BASE_URL}/file/upload`,
filePath: path,
name: 'params.files',
formData,
header: {
Authorization: uni.getStorageSync('accessToken'),
},
})
return JSON.parse(res[1].data).data.filePaths[0]
},
sleep(millisecond) {
return new Promise((resolve) => {
setTimeout(resolve, millisecond)
})
},
},
}
</script>
<style lang="scss">
.image-picker {
position: relative;
.form-item-column-center {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
flex-direction: column;
}
.watermark-canvas {
position: absolute;
top: 5px;
left: 5px;
width: 1px;
height: 1px;
overflow: hidden;
}
}
</style>

View File

@ -0,0 +1,147 @@
<!--
* @Author: XHC
* @Date: 2025-06-06 15:36:04
* @LastEditors: XHC
* @LastEditTime: 2025-06-06 16:38:39
* @Description:
-->
<template>
<canvas v-show="waterMarkParams.display" canvas-id="waterMarkCanvas" :style="canvasStyle" />
</template>
<script setup>
import { reactive, ref, computed, nextTick, defineExpose } from 'vue';
let waterMarkParams = reactive({
display: false, // canvas
canvasWidth: 300, //
canvasHeight: 225, //
contentHeight: 170, // (px)
})
let username= ref("YourUsername") //
//
const canvasStyle = computed(() => {
return {
position: "fixed", //
left: "9999px",
width: waterMarkParams.canvasWidth + "px",
height: waterMarkParams.canvasHeight + "px",
}
})
//
async function callAddWaterMark(imgPathArr) {
let results = [];
if (imgPathArr.length > 0) {
let addIndex = 0;
while (addIndex < imgPathArr.length) {
const tempFilePath = await addWaterMark(imgPathArr[addIndex]);
results.push(tempFilePath);
addIndex = addIndex + 1;
}
}
return results;
}
//
function addWaterMark(src) {
return new Promise((resolve, reject) => {
// canvas
uni.getImageInfo({
src,
success: (res) => {
// (9)
waterMarkParams.canvasWidth = Math.max(res.width, 886);
waterMarkParams.canvasHeight = res.height;
waterMarkParams.display = true;
console.log("当前图片信息waterMarkParams", waterMarkParams);
// canvas
nextTick(() => {
let context = uni.createCanvasContext("waterMarkCanvas", this);
/* 绘制 */
const {
canvasWidth,
canvasHeight,
contentHeight
} =
waterMarkParams;
//
context.clearRect(0, 0, canvasWidth, canvasHeight);
// srccancas
context.drawImage(
src,
0,
0,
canvasWidth,
canvasHeight,
canvasWidth,
canvasHeight
);
//
context.setGlobalAlpha(0.3);
context.beginPath();
//
context.rect(
0,
canvasHeight - contentHeight,
canvasWidth,
contentHeight
);
// context.setFillStyle("white"); //
context.fill();
//
context.setGlobalAlpha(1);
// 3.
context.setFontSize(32);
context.setTextAlign("left");
context.setFillStyle("white"); //
context.fillText(`拍摄人:${username.value}`, 50, canvasHeight -
120);
context.fillText(
`拍摄时间:${new Date()
}`,
50,
canvasHeight - 70
);
//
setTimeout(() => {
//
context.draw(false, () => {
console.log("!!!!!开始绘画", canvasWidth,
canvasHeight);
uni.canvasToTempFilePath({
canvasId: "waterMarkCanvas",
fileType: "jpg",
width: canvasWidth,
height: canvasHeight,
destWidth: canvasWidth,
destHeight: canvasHeight,
success: ({
tempFilePath
}) => {
console.log("绘制成功",
tempFilePath
);
waterMarkParams
.display =
false;
resolve(
tempFilePath
);
},
fail: (err) => {
reject(err);
console.log(err);
},
},
this
);
});
}, 1000);
});
},
});
});
}
//
defineExpose({ callAddWaterMark })
</script>

20
src/package.json Normal file
View File

@ -0,0 +1,20 @@
{
"id": "da-dropdown",
"name": "da-dropdown 下拉筛选菜单(支持主题色、功能丰富Vue3版)",
"displayName": "da-dropdown 下拉筛选菜单(支持主题色、功能丰富Vue3版)",
"version": "2.2.2",
"description": "一个基于 Vue3 的头部导航栏下拉筛选菜单组件兼容App、H5、微信小程序、支付宝小程序、抖音小程序等。。。",
"keywords": [
"dropdown",
"导航栏",
"筛选",
"下拉菜单",
"da系列"
],
"dcloudext": {
"category": [
"前端组件",
"通用组件"
]
}
}

View File

@ -92,7 +92,7 @@ const submit = (ref) => {
.tips {
border-radius: 10rpx;
background: #ffffff3f;
margin: 30rpx;
margin: 0 30rpx 30rpx;
padding: 10rpx;
display: flex;
align-items: center;

View File

@ -2,7 +2,7 @@
* @Author: XHC
* @Date: 2025-05-19 11:07:37
* @LastEditors: XHC
* @LastEditTime: 2025-05-29 16:55:28
* @LastEditTime: 2025-06-06 17:40:38
* @Description: 首页
-->
<template>
@ -80,18 +80,21 @@
</view>
<view class="category-content">
<view>
<uni-card class="list-card" v-for="(item, index) in listData" :key="index" @click="goToStartInspection(item)">
<uni-card class="list-card" v-for="(item, index) in listData.taskList" :key="index" @click="goToStartInspection(item)">
<view class="head">
<text class="title">{{ item.routeName }}</text>
<uni-tag :inverted="true" :text="item.state == 0 ? '已巡检' : '未巡检'" :type="item.state == 0 ? 'success' : 'error'" />
<text class="title">{{ item.title }}</text>
<!-- <uni-tag :inverted="true" :text="item.state == 0 ? '已巡检' : '未巡检'" :type="item.state == 0 ? 'success' : 'error'" /> -->
<uni-tag :inverted="true" :text="item.state" :type="item.state == '已巡检' ? 'success' : 'error'" />
</view>
<view class="num grey">线路编号{{ item.routeNumber }}</view>
<view class="num grey">线路编号{{ item.num }}</view>
<view class="info-box grey">
<view class="info"> <uni-icons fontFamily="iconfont" color="#808185">{{'&#xe74e;'}}</uni-icons> {{ item.patrolPersonnel }}</view>
<view class="info"> <uni-icons fontFamily="iconfont" color="#808185">{{'&#xe74e;'}}</uni-icons> {{ item.info }}</view>
<text class="info">任务 {{ item.task }}</text>
</view>
</uni-card>
<uni-load-more status="more" />
<view v-if="listData.isLoadMore">
<uni-load-more :status="listData.loadStatus" ></uni-load-more>
</view>
</view>
<!-- <view v-if="categoryCurIndex == 1"></view>
<view v-if="categoryCurIndex == 2"></view>
@ -102,15 +105,22 @@
<view v-if="navCurIndex == 2">自动分部</view>
<view v-if="navCurIndex == 3">保护分部</view>
</view>
<!-- 回到顶部 -->
<view class="top-back" @click="topBack" v-if="isShow">
<uni-icons type="up" size="16" color="#fff"></uni-icons>
<text>顶部</text>
</view>
</view>
</template>
<script setup>
import { nextTick, onMounted, reactive, ref } from "vue";
import { onReachBottom, onLoad, onShow, onPullDownRefresh } from "@dcloudio/uni-app";
import { onReachBottom, onLoad, onShow, onPullDownRefresh, onPageScroll } from "@dcloudio/uni-app";
import Dashboard from "@/components/echarts/dashboard.vue";
import { getRouteList } from '@/api/api.js'
import { getRouteList, getRoutePage } from '@/api/api.js'
let isShow = ref(false)
//
let navCurIndex = ref(0)
const departments = reactive([
@ -146,54 +156,85 @@ const category = reactive([
])
//
let listData = ref([
// { title: '', state: '', num: 'BHT-YX-XJ-L1.2', info: '', task: '5/32' },
// { title: '', state: '', num: 'BHT-YX-XJ-L1.2', info: '', task: '5/32' },
// { title: '', state: '', num: 'BHT-YX-XJ-L1.2', info: '', task: '5/32' },
// { title: '', state: '', num: 'BHT-YX-XJ-L1.2', info: '', task: '5/32' },
// { title: '', state: '', num: 'BHT-YX-XJ-L1.2', info: '', task: '5/32' },
// { title: '', state: '', num: 'BHT-YX-XJ-L1.2', info: '', task: '5/32' },
// { title: '', state: '', num: 'BHT-YX-XJ-L1.2', info: '', task: '5/32' },
// { title: '', state: '', num: 'BHT-YX-XJ-L1.2', info: '', task: '5/32' },
// { title: '', state: '', num: 'BHT-YX-XJ-L1.2', info: '', task: '5/32' },
// { title: '', state: '', num: 'BHT-YX-XJ-L1.2', info: '', task: '5/32' },
// { title: '', state: '', num: 'BHT-YX-XJ-L1.2', info: '', task: '5/32' },
])
let listData = reactive({
page: {//
size: 10, //
num: 1, //
},
loadStatus:'loading', //more-loading-nomore-
isLoadMore:false, //
taskList: [
{ title: '左岸白班机组', state: '巡检中', num: 'BHT-YX-XJ-L1.2', info: '白班每班一次,由当班值执行', task: '5/32' },
{ title: '左岸白班机组', state: '已巡检', num: 'BHT-YX-XJ-L1.2', info: '白班每班一次,由当班值执行', task: '5/32' },
{ title: '左岸白班机组', state: '巡检中', num: 'BHT-YX-XJ-L1.2', info: '白班每班一次,由当班值执行', task: '5/32' },
{ title: '左岸白班机组', state: '已巡检', num: 'BHT-YX-XJ-L1.2', info: '白班每班一次,由当班值执行', task: '5/32' },
{ title: '左岸白班机组', state: '未巡检', num: 'BHT-YX-XJ-L1.2', info: '白班每班一次,由当班值执行', task: '5/32' },
{ title: '左岸白班机组', state: '巡检中', num: 'BHT-YX-XJ-L1.2', info: '白班每班一次,由当班值执行', task: '5/32' },
{ title: '左岸白班机组', state: '未巡检', num: 'BHT-YX-XJ-L1.2', info: '白班每班一次,由当班值执行', task: '5/32' },
{ title: '左岸白班机组', state: '巡检中', num: 'BHT-YX-XJ-L1.2', info: '白班每班一次,由当班值执行', task: '5/32' },
{ title: '左岸白班机组', state: '巡检中', num: 'BHT-YX-XJ-L1.2', info: '白班每班一次,由当班值执行', task: '5/32' },
{ title: '左岸白班机组', state: '巡检中', num: 'BHT-YX-XJ-L1.2', info: '白班每班一次,由当班值执行', task: '5/32' },
{ title: '左岸白班机组', state: '巡检中', num: 'BHT-YX-XJ-L1.2', info: '白班每班一次,由当班值执行', task: '5/32' },
]//
})
//
onPullDownRefresh(() => {
nextTick(() => {
console.log('下拉刷新完成');
queryList({})
queryList(listData.page)
//
uni.stopPullDownRefresh();
});
})
//
onReachBottom(() => {
loadMore()
if(!listData.isLoadMore){
listData.isLoadMore = true
listData.filtrate.num += 1
queryList(listData.page)
}
})
//
onPageScroll((e) => {
console.log('Scroll position:', e.scrollTop);
isShow.value = e.scrollTop >= 200
});
onLoad(() => {
queryList({})
queryList(listData.page)
})
const queryList = (data) => {
console.log(data)
getRouteList(data).then((res) => {
getRoutePage(data).then((res) => {
console.log(res.data);
if (res.code == 200) {
listData.value = res.data
}
// if(res.records.length!==0){
// //10
// state.taskList = state.taskList.concat(res.records)
// //
// if(res.records.length<state.filtrate.size){
// state.isLoadMore=true
// state.loadStatus='nomore'
// }else{
// state.isLoadMore=false
// }
// }else{
// state.isLoadMore=true
// state.loadStatus='nomore'
// }
// if (res.code == 200) {
// listData.taskList = res.data
// }
}).catch((err) => {
});
}
const loadMore = () => {
}
//
const changeTitle = (index) => {
navCurIndex.value = index
@ -236,6 +277,14 @@ const goToStartInspection = (item) => {
// }
}
//
const topBack = () => {
uni.pageScrollTo({
scrollTop: 0, //
duration: 300 // 300ms
});
};
</script>
<style lang="scss" scoped>

View File

@ -2,7 +2,7 @@
* @Author: XHC
* @Date: 2025-05-19 15:18:26
* @LastEditors: XHC
* @LastEditTime: 2025-05-29 16:14:30
* @LastEditTime: 2025-06-05 17:18:03
* @Description: 知识库
-->
<template>
@ -19,7 +19,7 @@
>
{{ item.name }}
</view>
<uni-icons fontFamily="iconfont" color="#fff" :size="24">{{'&#xe621;'}}</uni-icons>
<uni-icons fontFamily="iconfont" color="#fff" :size="24" @click="screening">{{'&#xe621;'}}</uni-icons>
</view>
</view>
<view class="list-box" v-if="titleCurIndex == 0">
@ -29,7 +29,7 @@
<view class="info-box grey">
<uni-row :gutter="30">
<uni-col :span="16">
<view class="info"><uni-icons fontFamily="iconfont" color="#1469BC">{{'&#xe753;'}}</uni-icons> <text>{{ item.departments }}</text></view>
<view class="info"><uni-icons fontFamily="iconfont" color="#1469BC">{{'&#xe753;'}}</uni-icons> <text class="info-text">{{ item.departments }}</text></view>
</uni-col>
<uni-col :span="8">
<view class="info"><uni-icons fontFamily="iconfont" color="#808185">{{'&#xe74e;'}}</uni-icons> {{ item.time }}</view>
@ -38,13 +38,19 @@
</view>
</uni-card>
</view>
<DaDropdown
ref="daDropdownRef"
v-model:dropdownMenu="dropdownMenuList"
@confirm="handleConfirm"
@close="handleClose"
@open="handleOpen">
</DaDropdown>
</view>
</template>
<script setup>
import { reactive, ref } from 'vue';
import DaDropdown from '@/components/da-dropdown/index.vue'
//
let titleCurIndex = ref(0)
let titleList = reactive([
@ -55,6 +61,70 @@ let titleList = reactive([
{ id: 5, name: "发电机" },
])
const daDropdownRef = ref(null)
const menuVal = ref(null)
const dropdownMenuList = ref([
{
// title: '',
type: 'filter',
prop: 'god5',
// 22330
// value: { ft1: '2', ft2: ['2', '3'], ft3: 30 },
options: [
{
title: '单选',
type: 'radio',
prop: 'ft1',
options: [
{ label: '单选1', value: '1' },
{ label: '单选2', value: '2' },
{ label: '单选3', value: '3' },
{ label: '单选4', value: '4' },
],
},
{
title: '多选',
type: 'checkbox',
prop: 'ft2',
options: [
{ label: '多选1', value: '1' },
{ label: '多选2', value: '2' },
{ label: '多选3', value: '3' },
{ label: '多选4', value: '4' },
{ label: '多选5', value: '5' },
],
},
// {
// title: '',
// type: 'slider',
// prop: 'ft3',
// componentProps: {
// min: 0,
// max: 100,
// step: 1,
// showValue: true,
// },
// },
],
},
])
function handleConfirm(v, selectedValue) {
console.log('handleConfirm ==>', v, selectedValue)
}
function handleClose(v, callbackMenuList) {
console.log('handleClose ==>', v, callbackMenuList)
}
function handleOpen(v) {
console.log('handleOpen ==>', v)
}
function handleMenuVal() {
menuVal.value = daDropdownRef.value?.getMenuValue()
console.log('已选数据', menuVal.value)
}
//
const listData = reactive([
{ title: '电站运行巡检作业指导书', num: 'Q/BHTHP.ZDS 7008-2024', departments: '水力发电厂运行部', time: '2025-02-20' },
@ -76,10 +146,19 @@ const search = () => {
const changeTitle = (index) => {
titleCurIndex.value = index
}
const screening = () => {
// picker.value.show()
console.log(daDropdownRef.value);
daDropdownRef.value?.handleMenuClick()
}
</script>
<style lang="scss" scoped>
.knowledgeBase {
// position: relative;
.search-title {
width: 100%;
display: flex;
@ -96,10 +175,25 @@ const changeTitle = (index) => {
}
.list-box {
.title {
width: 100%;
}
.info {
margin-right: 0;
width: 100%;
}
}
:deep(.da-dropdown-menu) {
position: absolute!important;
width: 50rpx;
height: 60rpx;
top: 100rpx!important;
right: 40rpx!important;
opacity: 0;
}
:deep(.da-dropdown-content) {
top: 170rpx!important;
}
}
</style>

View File

@ -2,7 +2,7 @@
* @Author: XHC
* @Date: 2025-05-19 15:19:14
* @LastEditors: XHC
* @LastEditTime: 2025-05-23 15:30:08
* @LastEditTime: 2025-06-05 17:46:03
* @Description: 消息
-->
<template>
@ -11,7 +11,7 @@
<view class="notice-list" v-for="(item, index) in noticeList" :key="index">
<view class="title">
<uni-icons fontFamily="iconfont" color="#1571F3" :size="24">{{'&#xe651;'}}</uni-icons>
{{ item.title }}
<text class="title-text">{{ item.title }}</text>
</view>
<view class="info">{{ item.info }}</view>
<view class="bottom">
@ -20,11 +20,19 @@
</view>
</view>
</view>
<!-- 回到顶部 -->
<view class="top-back" @click="topBack" v-if="isShow">
<uni-icons type="up" size="16" color="#fff"></uni-icons>
<text>顶部</text>
</view>
</view>
</template>
<script setup>
import { reactive } from 'vue';
import { ref, reactive } from 'vue';
import { onPageScroll } from "@dcloudio/uni-app";
let isShow = ref(false)
//
const noticeList = reactive([
@ -34,7 +42,31 @@ const noticeList = reactive([
{ title: '系统通知', info: '即将开展“控制楼巡检”任务,请…', time: '2025-02-20' },
{ title: '系统通知', info: '即将开展“控制楼巡检”任务,请…', time: '2025-02-20' },
{ title: '系统通知', info: '即将开展“控制楼巡检”任务,请…', time: '2025-02-20' },
{ title: '系统通知', info: '即将开展“控制楼巡检”任务,请…', time: '2025-02-20' },
{ title: '系统通知', info: '即将开展“控制楼巡检”任务,请…', time: '2025-02-20' },
{ title: '系统通知', info: '即将开展“控制楼巡检”任务,请…', time: '2025-02-20' },
{ title: '系统通知', info: '即将开展“控制楼巡检”任务,请…', time: '2025-02-20' },
{ title: '系统通知', info: '即将开展“控制楼巡检”任务,请…', time: '2025-02-20' },
{ title: '系统通知', info: '即将开展“控制楼巡检”任务,请…', time: '2025-02-20' },
{ title: '系统通知', info: '即将开展“控制楼巡检”任务,请…', time: '2025-02-20' },
{ title: '系统通知', info: '即将开展“控制楼巡检”任务,请…', time: '2025-02-20' },
{ title: '系统通知', info: '即将开展“控制楼巡检”任务,请…', time: '2025-02-20' },
{ title: '系统通知', info: '即将开展“控制楼巡检”任务,请…', time: '2025-02-20' },
])
//
onPageScroll((e) => {
isShow.value = e.scrollTop >= 300
});
//
const topBack = () => {
uni.pageScrollTo({
scrollTop: 0, //
duration: 300 // 300ms
});
};
</script>
<style lang="scss" scoped>
@ -53,6 +85,10 @@ const noticeList = reactive([
color: #004291;
font-weight: bold;
font-size: 32rpx;
.title-text {
display: inline-block;
max-width: 90%;
}
}
.info, .bottom {
color: #75777A;
@ -62,6 +98,11 @@ const noticeList = reactive([
.info {
border-bottom: 3rpx solid #fff;
}
.title-text, .info {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bottom, .detail {
display: flex;
justify-content: space-between;

View File

@ -2,7 +2,7 @@
* @Author: XHC
* @Date: 2025-05-19 15:19:14
* @LastEditors: XHC
* @LastEditTime: 2025-05-30 17:01:11
* @LastEditTime: 2025-06-03 09:23:19
* @Description: 我的
-->
<template>
@ -83,9 +83,9 @@ const goToPages = (index) => {
});
break;
case 2:
// uni.navigateTo({
// url: '/pages/points/points'
// });
uni.navigateTo({
url: '/pages/points/points'
});
break
default:
break;

View File

@ -24,50 +24,61 @@
</view>
</scroll-view>
<view class="collapse-box" v-if="navCurIndex == 0">
<uni-collapse ref="collapse" v-model="collapseValue" accordion @change="change">
<uni-collapse-item v-for="(item, index) in collapsetitle" :key="index">
<view class="collapse-box">
<uni-collapse ref="collapseRef" v-model="collapseValue" accordion @change="changeCollapse">
<uni-collapse-item v-for="(val, collapseIndex) in collapseData" :key="collapseIndex">
<template v-slot:title>
<uni-section class="section" :title="item.title" type="line"></uni-section>
<uni-section class="section" :title="val.title" type="line"></uni-section>
</template>
<view class="content">
<view class="content-item" v-for="(item, index) in contentList" :key="index">
<view class="content-item" v-for="(item, itemIndex) in val.children" :key="itemIndex">
<view class="title">
<view>{{ item.title }}</view>
<uni-tag :inverted="true" :text="item.tag" :type="item.tag == '重点关注' ? 'error' : 'warning'" />
</view>
<view class="btn">
<button size="mini" type="primary" @click="standard">巡检标准</button>
<button size="mini" type="primary" @click="history">巡检历史</button>
<button size="mini" type="primary" @click="standard(item.standard)">巡检标准</button>
<button size="mini" type="primary" @click="history(item.history)">巡检历史</button>
</view>
<view class="input" :class="item.selectedImg ? 'warp' : ''" v-if="item.tag">
<view :class="item.selectedImg ? '' : 'input'" v-if="item.tag">
<uni-easyinput class="textarea" type="textarea" v-model="item.described" placeholder="请输入" />
<uni-file-picker class="picker" file-mediatype="image" :limit="9" v-model="item.imageValue" @select="selectImg(item)" @delete="deleteImg(e,item)"><uni-icons type="camera-filled" color="#004894" size="50"></uni-icons></uni-file-picker>
<uni-file-picker class="picker" mode="grid" :sourceType="['camera']" file-mediatype="image" limit="9" v-model="item.imageValue" @progress="progress" @select="(e) => selectImg(item, e, collapseIndex, itemIndex)" @delete="deleteImg(item, $event)"><uni-icons type="camera-filled" color="#004894" size="50"></uni-icons></uni-file-picker>
<!-- <WaterMarker :ref="el => { if (el) waterMarkRefs[`waterMarkRef_${collapseIndex}_${itemIndex}`] = el }"/> -->
<hpy-watermark :ref="el => { if (el) waterMarkRefs[`waterMarkRef_${collapseIndex}_${itemIndex}`] = el }" @waterMark="(path) => waterMark(item, path)"></hpy-watermark>
</view>
<view v-else>
<view class="state"><text>状态</text><uni-data-checkbox class="checkbox" mode="button" v-model="item.radio" :localdata="localdata"></uni-data-checkbox></view>
<view v-if="item.radio != 0">
<view class="described">描述<uni-easyinput class="textarea" type="textarea" v-model="item.described" placeholder="请输入" /></view>
<view>照片<uni-file-picker class="picker" limit="9" v-model="item.imageValue"><uni-icons type="camera-filled" color="#004894" size="50"></uni-icons></uni-file-picker></view>
<view>照片<uni-file-picker class="picker" mode="grid" :sourceType="['camera']" file-mediatype="image" limit="9" v-model="item.imageValue" @select="selectImg(item,$event)" @delete="deleteImg(item, $event)"><uni-icons type="camera-filled" color="#004894" size="50"></uni-icons></uni-file-picker></view>
</view>
</view>
</view>
</view>
</uni-collapse-item>
</uni-collapse>
</view>
<uni-popup ref="popup" background-color="#fff" @change="change">
<view class="popup-content" :class="{ 'popup-height': type === 'left' || type === 'right' }"><text
class="text">popup 内容</text></view>
<uni-popup ref="alertDialog" type="dialog">
<uni-popup-dialog type="warning" cancelText="关闭" confirmText="同意" title="通知" content="欢迎使用 uni-popup!" @confirm="dialogConfirm"
@close="dialogClose"></uni-popup-dialog>
</uni-popup>
<uni-popup ref="popup" background-color="#fff" @change="changePopup">
<view class="popup-content"><text class="text">{{ popValue }}</text></view>
</uni-popup>
</view>
</template>
<script setup>
import { reactive } from 'vue';
import { ref, reactive, nextTick, getCurrentInstance } from 'vue';
import ImageWatermarkPicker from "@/components/image-watermark-picker.vue"
import WaterMarker from '@/components/waterMarker.vue'
import HpyWatermark from '@/uni_modules/hpy-watermark/components/hpy-watermark/hpy-watermark.vue'
let popup = ref(null)
let waterMarkRefs = reactive({})
//
let popValue = ref(null)
let alertDialog = ref(null)
let navCurIndex = ref(0)
//
@ -82,27 +93,67 @@ let positionData = reactive([
{ id: 2, title: "右岸" },
{ id: 3, title: "大坝" }
])
let collapseRef = ref(null)
//
let collapseValue = ref('0')
//
let collapsetitle = reactive([
{ id: 1, title: '左岸进水口配电系统' },
{ id: 2, title: '左岸排风竖井配电系统' },
{ id: 3, title: '左岸尾水管配电系统(南端)' },
{ id: 4, title: '左岸尾水管配电系统(北端)' },
{ id: 5, title: '左岸尾水洞出口配电系统' },
{ id: 6, title: '左岸水垫塘渗漏排水配电系统' },
{ id: 7, title: '左岸水垫塘检修排水配电系统' },
{ id: 8, title: '控制管理楼及左岸出线场配电系统' },
])
//
let contentList = ref([
{ title: '环境温湿度(°C/%)', tag: '重点关注', described: '', imageValue: [], selectedImg: false },
{ title: 'Ⅰ段干式变三相绕组温度', tag: '请确认', described: '', imageValue: [], selectedImg: false },
{ title: '带电显示装置及电磁锁正常;铁芯端部无散片、流胶;', radio: 0, described: '', imageValue: [] },
{ title: '盘柜无异常声音、气味;无放电、过热现象外观正常;开关状态及指示灯正常',radio: 0, described: '', imageValue: [] },
{ title: '备自投及指示灯正常' }
//
let collapseData = reactive([
{ id: 1, title: '左岸进水口配电系统', children: [
{ title: '环境温湿度(°C/%)', tag: '重点关注', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: 'Ⅰ段干式变三相绕组温度', tag: '请确认', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: '带电显示装置及电磁锁正常;铁芯端部无散片、流胶;', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '盘柜无异常声音、气味;无放电、过热现象外观正常;开关状态及指示灯正常',radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '备自投及指示灯正常', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] }
] },
{ id: 2, title: '左岸排风竖井配电系统', children: [
{ title: '环境温湿度(°C/%)', tag: '重点关注', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: 'Ⅰ段干式变三相绕组温度', tag: '请确认', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: '带电显示装置及电磁锁正常;铁芯端部无散片、流胶;', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '盘柜无异常声音、气味;无放电、过热现象外观正常;开关状态及指示灯正常',radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '备自投及指示灯正常', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] }
] },
{ id: 3, title: '左岸尾水管配电系统(南端)', children: [
{ title: '环境温湿度(°C/%)', tag: '重点关注', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: 'Ⅰ段干式变三相绕组温度', tag: '请确认', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: '带电显示装置及电磁锁正常;铁芯端部无散片、流胶;', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '盘柜无异常声音、气味;无放电、过热现象外观正常;开关状态及指示灯正常',radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '备自投及指示灯正常', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] }
] },
{ id: 4, title: '左岸尾水管配电系统(北端)', children: [
{ title: '环境温湿度(°C/%)', tag: '重点关注', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: 'Ⅰ段干式变三相绕组温度', tag: '请确认', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: '带电显示装置及电磁锁正常;铁芯端部无散片、流胶;', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '盘柜无异常声音、气味;无放电、过热现象外观正常;开关状态及指示灯正常',radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '备自投及指示灯正常', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] }
] },
{ id: 5, title: '左岸尾水洞出口配电系统', children: [
{ title: '环境温湿度(°C/%)', tag: '重点关注', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: 'Ⅰ段干式变三相绕组温度', tag: '请确认', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: '带电显示装置及电磁锁正常;铁芯端部无散片、流胶;', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '盘柜无异常声音、气味;无放电、过热现象外观正常;开关状态及指示灯正常',radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '备自投及指示灯正常', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] }
] },
{ id: 6, title: '左岸水垫塘渗漏排水配电系统', children: [
{ title: '环境温湿度(°C/%)', tag: '重点关注', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: 'Ⅰ段干式变三相绕组温度', tag: '请确认', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: '带电显示装置及电磁锁正常;铁芯端部无散片、流胶;', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '盘柜无异常声音、气味;无放电、过热现象外观正常;开关状态及指示灯正常',radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '备自投及指示灯正常', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] }
] },
{ id: 7, title: '左岸水垫塘检修排水配电系统', children: [
{ title: '环境温湿度(°C/%)', tag: '重点关注', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: 'Ⅰ段干式变三相绕组温度', tag: '请确认', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: '带电显示装置及电磁锁正常;铁芯端部无散片、流胶;', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '盘柜无异常声音、气味;无放电、过热现象外观正常;开关状态及指示灯正常',radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '备自投及指示灯正常', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] }
] },
{ id: 8, title: '控制管理楼及左岸出线场配电系统', children: [
{ title: '环境温湿度(°C/%)', tag: '重点关注', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: 'Ⅰ段干式变三相绕组温度', tag: '请确认', described: '', standard: '巡检标准', history: '巡检历史', imageValue: [], selectedImg: false },
{ title: '带电显示装置及电磁锁正常;铁芯端部无散片、流胶;', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '盘柜无异常声音、气味;无放电、过热现象外观正常;开关状态及指示灯正常',radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] },
{ title: '备自投及指示灯正常', radio: 0, described: '', standard: '巡检标准', history: '巡检历史', imageValue: [] }
] },
])
//
@ -117,34 +168,95 @@ const changeTitle = (index) => {
navCurIndex.value = index
}
const change = () => {
//
const changeCollapse = () => {
// alertDialog.value.open()
}
//
const changePopup = () => {
}
//
const dialogConfirm = () => {
}
//
const dialogClose = () => {
}
const waterMark = (item, path) => {
console.log(path);
item.imageValue.push({
name:"file.png",
extname:"png",
url: path,
// ...
});
console.log(item.imageValue);
};
//
const selectImg = (item) => {
console.log(item);
const selectImg = (item, e, collapseIndex, itemIndex) => {
item.selectedImg = true
// ref key
const refKey = `waterMarkRef_${collapseIndex}_${itemIndex}`
const waterMarkerInstance = waterMarkRefs[refKey]
if (waterMarkerInstance) {
// const imgFileArr = waterMarkerInstance.callAddWaterMark(e.tempFilePaths);
// imgFileArr.forEach((el) => {
// item.imageValue.push({
// url: el,
// extname: el.substring(el.lastIndexOf(".") + 1),
// name: el,
// });
// })
console.log(waterMarkerInstance);
var fillTexts = ["人员:张三", "地址广东省珠海市香洲区XXX"];
fillTexts.push("时间:");
//
waterMarkerInstance.addWaterMark({
filePaths: e.tempFilePaths,
fillTexts
});
} else {
console.warn('未找到对应的WaterMarker实例或方法未暴露', refKey, waterMarkerInstance)
}
}
const progress = (item, index) => {
uni.previewImage({
current: index,
urls: item.map((o) => o.url),
});
}
//
const deleteImg = (e,item) => {
console.log(item);
console.log(item.imageValue);
console.log(item.imageValue.length);
const deleteImg = (item,e) => {
item.imageValue.splice(e.index, 1)
console.log(item, e)
// if (item.imageValue.length == 0) {
// item.selectedImg = false
// }
if (item.imageValue.length == 0) {
item.selectedImg = false
}
}
//
const standard = () => {
const standard = (val) => {
popup.value.open()
popValue.value = val
}
const history = () => {
//
const history = (val) => {
popup.value.open()
popValue.value = val
}
</script>
@ -208,9 +320,13 @@ const history = () => {
}
}
}
.warp {
display: block !important;
}
// .warp {
// display: block !important;
// :deep(.uni-file-picker__container) {
// width: auto !important;
// height: auto !important;
// }
// }
.state {
display: flex;
align-items: center;

View File

@ -2,7 +2,7 @@
* @Author: XHC
* @Date: 2025-05-23 16:46:57
* @LastEditors: XHC
* @LastEditTime: 2025-05-29 17:06:12
* @LastEditTime: 2025-06-06 17:41:44
* @Description: 开始巡检
-->
<template>
@ -78,7 +78,7 @@
<view class="inspection-points">
<view class="title">
<uni-section title="巡检点位:" type="line"><text>3/32</text></uni-section>
<uni-icons type="scan" size="26" @click="scanQRCode" ></uni-icons>
<uni-icons type="scan" size="26" @click="scanQRCode" v-if="infoData.state != '已巡检'"></uni-icons>
</view>
<view class="list-box">
<view class="list-item" v-for="(item, index) in listData" :key="index" @click="scanQRCode">
@ -153,8 +153,10 @@ const queryPointList = () => {
//
const scanQRCode = () => {
if (infoData.state == '已巡检') return
uni.scanCode({
//
onlyFromCamera: true,
success: (res) => {
// res.result
console.log('扫描结果:', res.result);

View File

@ -0,0 +1,5 @@
## 1.0.62023-04-08
去掉无用依赖
## 1.0.52023-04-08
- 修复H5有些图片出现半截空白
- 有问题请描述使用场景,最好能把图片贴出来,以便更好的优化兼容性

View File

@ -0,0 +1,198 @@
<template>
<view class="watermark-content">
<canvas canvas-id="watermarkCanvas" id="watermarkCanvas" :style="{width:canvasWidth + 'px', height:canvasHeight + 'px'}"></canvas>
</view>
</template>
<script setup>
// export default {
// name:'hpy-watermark',
import { ref } from "vue";
const emit = defineEmits(['waterMark'])
const props = defineProps({
/**
* 文字文字位置默认左下角可选值左上角topLeft右上角topRight左下角bottomLeft右下角bottomRight
*/
markAlign:{
type:String,
default:function(){
return 'bottomLeft'
}
},
/**
* 设置文本的水平对齐方式默认start文本在指定的位置开始
* end 文本在指定的位置结束
* center 文本的中心被放置在指定的位置
* left 文本左对齐
* right 文本右对齐
*/
textAlign:{
type:String,
default:function(){
return 'start';
}
},
/**
* 设置文本的垂直对齐方式默认alphabetic文本基线是普通的字母基线
* top 文本基线是 em 方框的顶端
* hanging 文本基线是悬挂基线
* middle 文本基线是 em 方框的正中
* ideographic 文本基线是表意基线
* bottom 文本基线是 em 方框的底端
*/
textBaseline:{
type:String,
default:function(){
return 'alphabetic';
}
},
/**
* 文字大小
*/
fontSize:{
type:[Number, String],
default:40
},
/**
* 文字颜色
*/
fontColor:{
type:String,
default:function(){
return '#FFFFFF'
}
},
/**
* 阴影颜色
*/
shadowColor:{
type:String,
default:function(){
return 'rgba(0, 0, 0, 1.0)';
}
},
/**
* 阴影边框大小
*/
shadowWidth:{
type:[Number, String],
default:2
},
/**
* 图片的质量取值范围为 (0, 1]不在范围内时当作1处理
*/
quality:{
type:[Number, String],
default:1
},
/**
* 目标文件的类型只支持 'jpg' 'png'默认为 'png'
*/
fileType:{
type:String,
default:function(){
return 'png'
}
}
})
let canvasWidth = ref(0)
let canvasHeight = ref(0)
/**
* 增加水印
* @param {Object} {filePaths:['图片地址1', '图片地址2'], fillTexts:['水印1', '水印2']}
*/
async function addWaterMark({ filePaths = [], fillTexts = [] }) {
console.log('开始添加水印', filePaths, fillTexts);
uni.showLoading({title:'图片处理中···'});
try{
for (const filePath of filePaths) {
await drawImage(filePath, fillTexts.reverse());
}
}catch(e){
// TODO handle the exception
}finally{
uni.hideLoading();
}
}
/**
* 绘制单个图片
*/
async function drawImage(filePath, fillTexts){
const ctx = uni.createCanvasContext('watermarkCanvas', this);
return new Promise(resolve => {
uni.getImageInfo({
src: filePath,
success: (image) => {
console.log(image);
canvasWidth.value = image.width;
canvasHeight.value = image.height;
ctx.clearRect(0, 0, image.width, image.height);
setTimeout(()=>{
ctx.drawImage(image.path, 0, 0, image.width, image.height);
ctx.setFontSize(props.fontSize);
ctx.setFillStyle(props.fontColor);
//
let shadowWidth = Number(props.shadowWidth + "");
if(shadowWidth > 0){
ctx.shadowColor = props.shadowColor;
ctx.shadowOffsetX = shadowWidth;
ctx.shadowOffsetY = shadowWidth;
}
//
ctx.textAlign = props.textAlign;
//
ctx.textBaseline = props.textBaseline;
const maxText = fillTexts.reduce((text, val) => {
return text.length >= val.length ? text : val;
});
fillTexts.forEach((mark, index) => {
if(props.markAlign == "bottomRight"){
ctx.fillText(mark, image.width - (ctx.measureText(maxText).width+60), image.height - (index*60+60));
}else if(props.markAlign == "topLeft"){
ctx.fillText(mark, 20, (index*60+60));
}else if(props.markAlign == "topRight"){
ctx.fillText(mark, image.width - (ctx.measureText(maxText).width+60), (index*60+60));
}else{
ctx.fillText(mark, 20, image.height - (index*60+60));
}
});
ctx.draw(false, (() => {
setTimeout(()=>{
uni.canvasToTempFilePath({
canvasId: 'watermarkCanvas',
fileType:props.fileType,
quality:Number(props.quality + "" || "1"),
success: (res) => {
console.log(res);
emit('waterMark', res.tempFilePath);
},
fail:(err) => {
console.log(err)
},
complete: () => {
resolve();
}
}, this);
}, 300);
})());
}, 200);
},
fail: (e) => {
resolve();
}
});
});
}
defineExpose({
addWaterMark
});
</script>
<style scoped>
.watermark-content{width: 0;height: 0;overflow: hidden;}
</style>

View File

@ -0,0 +1,83 @@
{
"id": "hpy-watermark",
"displayName": "文字水印",
"version": "1.0.6",
"description": "图片增加文字水印,支持拍照和相册选取多张",
"keywords": [
"水印",
"图片水印",
"文字水印",
"watermark"
],
"repository": "",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": ""
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "component-vue"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@ -0,0 +1,201 @@
## 图片增加文字水印
> **组件名hpy-watermark
> 图片增加文字水印,支持拍照和相册选取多张
## API
## Props
<table>
<tr>
<th>属性名</th>
<th>类型</th>
<th>默认值</th>
<th>说明</th>
</tr>
<tr>
<td>markAlign</td>
<td>String</td>
<td>左下角</td>
<td>文字文字位置默认左下角可选值左上角topLeft、右上角topRight、左下角bottomLeft、右下角bottomRight</td>
</tr>
<tr>
<td>fontSize</td>
<td>Number</td>
<td>40</td>
<td>文字大小默认40</td>
</tr>
<tr>
<td>fontColor</td>
<td>String</td>
<td>白色</td>
<td>文字颜色,默认:#FFFFFF</td>
</tr>
<tr>
<td>quality</td>
<td>Number</td>
<td>1</td>
<td>图片的质量,取值范围为 (0, 1]不在范围内时当作1.0处理</td>
</tr>
<tr>
<td>fileType</td>
<td>String</td>
<td>jpg</td>
<td>目标文件的类型,只支持 'jpg' 或 'png'。默认为 'jpg'</td>
</tr>
<tr>
<td>shadowColor</td>
<td>String</td>
<td>黑色</td>
<td>阴影颜色默认rgba(0, 0, 0, 1.0)</td>
</tr>
<tr>
<td>shadowWidth</td>
<td>Number</td>
<td>2</td>
<td>阴影边框大小默认2</td>
</tr>
<tr>
<td>textAlign</td>
<td>String</td>
<td>start</td>
<td>设置文本的水平对齐方式默认start文本在指定的位置开始。 start、end、center、left、right</td>
</tr>
<tr>
<td>textBaseline</td>
<td>String</td>
<td>alphabetic</td>
<td>设置文本的垂直对齐方式默认alphabetic文本基线是普通的字母基线。top、hanging、middle、ideographic、bottom</td>
</tr>
</table>
## methods
<table>
<tr>
<th>参数名</th>
<th>类型</th>
<th>说明</th>
</tr>
<tr>
<td>addWaterMark</td>
<td>Object</td>
<td>{filePaths:['图片地址1', '图片地址2'], fillTexts:['水印文字1', '水印文字2']}</td>
</tr>
</table>
## 使用示例
```html
<template>
<view>
<button @click="chooseImage">选择照片</button>
<!-- 增加水印 -->
<hpy-watermark ref="watermark" @waterMark="waterMark"></hpy-watermark>
<view class="ul">
<view class="li" v-for="(item, index) in imageList" :key="index">
<image :src="item" class="img" mode="widthFix"></image>
</view>
</view>
</view>
</template>
```
```javascript
<script>
export default {
data() {
return {
imageList:[]
}
},
methods: {
// 选择图片
chooseImage() {
uni.chooseImage({
count: this.limit, // 限制的图片数量
sizeType: ['compressed'], // original 原图compressed 压缩图,默认二者都有
sourceType: ['album', 'camera'],// album 从相册选图camera 使用相机,默认二者都有
success: (res) => {
var imgPathList = res.tempFilePaths;
if(imgPathList.length > 0){
this.addImages(imgPathList);
}
},
fail: (err) => {
console.log('chooseImage fail', err)
if("chooseImage:fail cancel" == err.errMsg){
uni.showToast({
icon:'none',
title:'取消了选择'
});
}else{
// #ifdef MP
uni.getSetting({
success: (res) => {
let authStatus = res.authSetting['scope.album'];
if (!authStatus) {
uni.showModal({
title: '授权失败',
content: '系统上传需要从您的相册获取图片,请在设置界面打开相关权限',
success: (res) => {
if (res.confirm) {
uni.openSetting();
}
}
})
}
}
})
// #endif
}
}
});
},
// 添加图片
addImages(filePaths){
if(filePaths.length > 0){
var fillTexts = ["人员:张三", "地址广东省珠海市香洲区XXX"];
fillTexts.push("时间:" + this.getNowTime());
// 添加水印
this.$refs.watermark.addWaterMark({
filePaths,
fillTexts
});
}
},
/**
* 水印添加回调在H5平台下filePath 为 base64
*/
waterMark(filePath){
this.imageList.push(filePath);
},
/**
* 获取当前时间
*/
getNowTime(){
var date = new Date(),
year = date.getFullYear(),
month = date.getMonth() + 1,
day = date.getDate(),
hour = date.getHours() < 10 ? "0" + date.getHours() : date.getHours(),
minute = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes(),
second = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds();
month >= 1 && month <= 9 ? (month = "0" + month) : "";
day >= 0 && day <= 9 ? (day = "0" + day) : "";
return (year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second);
}
}
}
</script>
```
```style
<style scoped>
.ul{border: rgb(221, 221, 221) solid 1px; text-align: center; margin-right: 12px; position: relative }
.ul .li .img{display:block; width: 80px; height: 80px;}
</style>
```