APP文件新增

This commit is contained in:
geht
2026-07-20 14:10:39 +08:00
parent 99987d9d2e
commit 395a52b4c6
710 changed files with 227084 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
<template>
<wd-popup v-model="show" position="bottom" @close="handleClose">
<view class="contetn">
<wd-text custom-class="title" v-if="title" :text="title"></wd-text>
<wd-cell-group border>
<wd-cell
v-for="(item, index) in options"
:icon="item.icon"
:label="item.label"
:custom-class="item.color"
clickable
@click="handleClick(item)"
></wd-cell>
</wd-cell-group>
</view>
</wd-popup>
</template>
<script setup lang="ts">
import { ref } from 'vue'
defineOptions({
name: 'BottomOperate',
options: {
styleIsolation: 'shared',
},
})
const eimt = defineEmits(['change', 'close'])
const show = ref(true)
const props = defineProps(['title', 'data', 'options'])
const handleClose = () => {
show.value = false
setTimeout(() => {
eimt('close')
}, 300)
}
const handleClick = (item) => {
eimt('change', { option: item, data: props.data })
handleClose()
}
</script>
<style lang="scss" scoped>
.contetn {
padding-top: 10px;
:deep(.title) {
padding-left: 10px;
padding-right: 10px;
font-size: 14px;
color: #666;
margin-bottom: 10px;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.wd-cell) {
padding-left: 10px;
--wot-cell-icon-size: 15px;
--wot-cell-label-color: #444;
--wot-cell-label-fs: 14px;
.wd-icon {
margin-right: 10px;
}
.wd-cell__label {
margin-top: 0;
}
&.red {
color: red;
--wot-cell-label-color: red;
}
}
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<div class="breadcrumb">
<template v-for="(item, index) in items" :key="index">
<span
class="breadcrumb-item"
:class="{ 'is-link': index < items.length - 1 }"
@click="handleClick(item, index)"
>
<slot :name="'item-' + index" :item="item">
{{ item.title }}
</slot>
</span>
<span v-if="index < items.length - 1" class="breadcrumb-separator">{{ separator }}</span>
</template>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'Breadcrumb',
})
interface BreadcrumbItem {
title: string
[key: string]: any
}
const props = defineProps({
items: {
type: Array as PropType<BreadcrumbItem[]>,
default: () => [],
},
separator: {
type: String,
default: '/',
},
})
const emit = defineEmits(['click'])
const handleClick = (item: BreadcrumbItem, index: number) => {
if (index < props.items.length - 1) {
emit('click', item, index)
}
}
</script>
<style lang="scss" scoped>
.breadcrumb {
display: flex;
align-items: center;
font-size: 14px;
line-height: 1.5;
padding: 8px;
&-item {
&.is-link {
color: rgba(0, 0, 0, 0.45);
cursor: pointer;
transition: color 0.3s;
}
&:last-child {
color: rgba(0, 0, 0, 0.75);
}
}
&-separator {
margin: 0 8px;
color: rgba(0, 0, 0, 0.45);
}
}
</style>

View File

@@ -0,0 +1,286 @@
<template>
<view class="CategorySelect">
<view class="pickerArea" :class="{ clear: !!showText }" @click="handleClick">
<wd-input
:placeholder="`请选择${$attrs.label}`"
v-bind="$attrs"
readonly
v-model="showText"
></wd-input>
<view v-if="!!showText && !$attrs.disabled" class="u-iconfont u-icon-close" @click.stop="handleClear"></view>
</view>
<wd-popup v-if="popupShow" position="bottom" v-model="popupShow">
<view class="content">
<view class="operation">
<view class="cancel text-gray-5" @click.stop="cancel">取消</view>
<view class="confrim" @click.stop="confirm">确定</view>
</view>
<scroll-view class="flex-1" scroll-y>
<DaTree
:data="treeData"
labelField="title"
valueField="key"
loadMode
:showCheckbox="multiple"
:showRadioIcon="false"
:checkStrictly="true"
:defaultCheckedKeys="defaultCheckedKeys"
:loadApi="asyncLoadTreeData"
@change="handleTreeChange"
></DaTree>
</scroll-view>
</view>
</wd-popup>
</view>
</template>
<script setup lang="ts">
import { ref, watch, useAttrs } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import DaTree from '@/uni_modules/da-tree/index.vue'
import { isArray } from '@/utils/is'
defineOptions({
name: 'CategorySelect',
})
const props = defineProps({
modelValue: {
type: [Array, String],
},
condition: {
type: String,
default: '',
required: false,
},
// 是否支持多选
multiple: {
type: Boolean,
default: false,
},
pid: {
type: String,
default: '',
required: false,
},
pcode: {
type: String,
default: '',
required: false,
},
})
const emit = defineEmits(['change', 'update:modelValue'])
const toast = useToast()
const api = {
loadDictItem: '/sys/category/loadDictItem/',
loadTreeData: '/sys/category/loadTreeData',
}
const showText = ref('')
const popupShow = ref(false)
const treeData = ref<any[]>([])
const treeValue = ref([])
const attrs = useAttrs()
const defaultCheckedKeys: any = ref([])
const handleClick = () => {
if (!attrs.disabled) {
popupShow.value = true
}
}
const cancel = () => {
popupShow.value = false
}
const confirm = () => {
const titles = treeValue.value.map((item) => item.title)
const keys = treeValue.value.map((item) => item.key).join(',')
showText.value = titles.join(',')
popupShow.value = false
emit('update:modelValue', keys)
emit('change', keys)
}
const handleTreeChange = (value, record) => {
const { originItem, checkedStatus } = record
const { key, title } = originItem
if (checkedStatus) {
// 选中
if (props.multiple) {
treeValue.value.push({ key, title })
} else {
treeValue.value = [{ key, title }]
}
} else {
// 取消
if (props.multiple) {
const findIndex = treeValue.value.findIndex((item) => item.key == key)
if (findIndex != -1) {
treeValue.value.splice(findIndex, 1)
}
} else {
treeValue.value = []
}
}
}
const transformField = (result) => {
for (let i of result) {
i.value = i.key
if (i.leaf == false) {
i.isLeaf = false
} else if (i.leaf == true) {
i.isLeaf = true
}
}
}
// 异步加载
const asyncLoadTreeData = ({ originItem }) => {
return new Promise<void>((resolve) => {
let param = {
pid: originItem.key,
condition: props.condition,
}
http
.get(api.loadTreeData, param)
.then((res: any) => {
if (res.success) {
const { result } = res
transformField(result)
resolve(result)
} else {
resolve(null)
}
})
.catch((err) => resolve(null))
})
}
// 加载根节点
function loadRoot() {
let param = {
pid: props.pid,
pcode: !props.pcode ? '0' : props.pcode,
condition: props.condition,
}
http
.get(api.loadTreeData, param)
.then((res: any) => {
if (res.success) {
const { result } = res
if (result && result.length > 0) {
transformField(result)
treeData.value = result
}
} else {
toast.warning('分类字典书组件根节点数据加载失败~')
}
})
.catch((err) => {
toast.warning('分类字典书组件根节点数据加载失败~')
})
}
// 翻译input内的值
function loadItemByCode() {
let value = props.modelValue
if(value){
if (isArray(props.modelValue)) {
// @ts-ignore
value = value.join()
}
if (value === treeData.value.map((item) => item.key).join(',')) {
// 说明是刚选完,内部已有翻译。不需要再请求
return
}
value = (value as string).trim()
if (value) {
http
.get(api.loadDictItem, { ids: value })
.then((res: any) => {
if (res.success) {
const { result = [] } = res
showText.value = result.join(',')
defaultCheckedKeys.value = value
} else {
}
})
.catch((err) => {})
}
}
}
// 清空
const handleClear = () => {
showText.value = ''
treeValue.value = []
confirm()
}
watch(
() => props.modelValue,
() => {
loadItemByCode()
},
{ deep: true, immediate: true },
)
watch(
() => props.pcode,
() => {
loadRoot()
},
{ deep: true, immediate: true },
)
</script>
<style lang="scss" scoped>
:deep(.wd-popup-wrapper) {
.wd-popup {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
}
.content {
height: 50vh;
width: 100vw;
display: flex;
flex-direction: column;
.operation {
display: flex;
justify-content: space-between;
line-height: 40px;
padding: 0 5px;
position: relative;
&::before {
content: ' ';
position: absolute;
bottom: 0;
left: 8px;
right: 8px;
height: 1px;
background-color: #e5e5e5;
}
.cancel,
.confrim {
font-size: 15px;
height: 40px;
min-width: 40px;
text-align: center;
}
.confrim {
color: var(--wot-color-theme);
}
}
:deep(.da-tree) {
.da-tree-item__checkbox {
// display: none;
}
}
}
.pickerArea {
position: relative;
.u-icon-close {
position: absolute;
right: 15px;
top: calc(14px + 4px);
color: #585858;
font-size: 15px;
}
&.clear {
:deep(.wd-input__body) {
padding-right: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,341 @@
<template>
<view class="DateTime">
<view class="inputArea" :class="{ clear: !!showText }" @click="handleClick">
<wd-input
:placeholder="getPlaceholder($attrs)"
v-bind="$attrs"
readonly
v-model="showText"
></wd-input>
<view v-if="!!showText && !disabled" class="u-iconfont u-icon-close" @click.stop="handleClear"></view>
</view>
<wd-popup v-if="popupShow" position="bottom" v-model="popupShow">
<view class="content">
<view class="operation">
<view class="cancel text-gray-5" @click.stop="cancel">取消</view>
<view class="confrim" @click.stop="confirm">确定</view>
</view>
<picker-view class="picker-view" :value="pickerValue" @change="handleChange">
<!-- -->
<picker-view-column v-if="showYear">
<view class="picker-item" v-for="(item, index) in years" :key="index">
{{ item }}
</view>
</picker-view-column>
<!-- -->
<picker-view-column v-if="showMonth">
<view class="picker-item" v-for="(item, index) in months" :key="index">
{{ item }}
</view>
</picker-view-column>
<!-- -->
<picker-view-column v-if="showDay">
<view class="picker-item" v-for="(item, index) in days" :key="index">{{ item }}</view>
</picker-view-column>
<!-- -->
<picker-view-column v-if="showHour">
<view class="picker-item" v-for="(item, index) in hours" :key="index">
{{ item }}
</view>
</picker-view-column>
<!-- -->
<picker-view-column v-if="showMinute">
<view class="picker-item" v-for="(item, index) in minutes" :key="index">
{{ item }}
</view>
</picker-view-column>
<!-- -->
<picker-view-column v-if="showSecond">
<view class="picker-item" v-for="(item, index) in seconds" :key="index">
{{ item }}
</view>
</picker-view-column>
</picker-view>
</view>
</wd-popup>
</view>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { useToast } from 'wot-design-uni'
import dayjs from 'dayjs'
import { getPlaceholder } from '@/common/uitls'
defineOptions({
name: 'DateTime',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
modelValue: {
type: String,
default: '',
},
// 显示格式year/month/day/hour/minute/second
format: {
type: String,
default: 'YYYY-MM-DD HH:mm:ss',
},
// 开始时间
startTime: {
type: String,
default: '1970-01-01 00:00:00',
},
// 结束时间
endTime: {
type: String,
default: '2100-12-31 23:59:59',
},
// 是否禁用
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['change', 'update:modelValue'])
const toast = useToast()
// 显示控制
const showYear = computed(() => props.format.includes('YYYY'))
const showMonth = computed(() => props.format.includes('MM'))
const showDay = computed(() => props.format.includes('DD'))
const showHour = computed(() => props.format.includes('HH'))
const showMinute = computed(() => props.format.includes('mm'))
const showSecond = computed(() => props.format.includes('ss'))
// 数据源
const years = ref<number[]>([])
const months = ref<number[]>([])
const days = ref<number[]>([])
const hours = ref<number[]>([])
const minutes = ref<number[]>([])
const seconds = ref<number[]>([])
// 当前选中的值
const pickerValue = ref<number[]>([])
const showText = computed(() => props.modelValue)
const popupShow = ref(false)
// 初始化数据
const initData = () => {
const start = dayjs(props.startTime)
const end = dayjs(props.endTime)
// 初始化年份
if (showYear.value) {
years.value = Array.from({ length: end.year() - start.year() + 1 }, (_, i) => start.year() + i)
}
// 初始化月份
if (showMonth.value) {
months.value = Array.from({ length: 12 }, (_, i) => i + 1)
}
// 初始化日期
if (showDay.value) {
const currentYear = props.modelValue ? dayjs(props.modelValue).year() : start.year()
const currentMonth = props.modelValue ? dayjs(props.modelValue).month() + 1 : 1
const daysInMonth = dayjs(`${currentYear}-${currentMonth}`).daysInMonth()
days.value = Array.from({ length: daysInMonth }, (_, i) => i + 1)
}
// 初始化小时
if (showHour.value) {
hours.value = Array.from({ length: 24 }, (_, i) => i)
}
// 初始化分钟
if (showMinute.value) {
minutes.value = Array.from({ length: 60 }, (_, i) => i)
}
// 初始化秒
if (showSecond.value) {
seconds.value = Array.from({ length: 60 }, (_, i) => i)
}
}
// 更新日期
const updateDays = () => {
if (!showYear.value || !showMonth.value) return
const year = years.value[pickerValue.value[0] || 0]
const month = months.value[pickerValue.value[1] || 0]
const daysInMonth = dayjs(`${year}-${month}`).daysInMonth()
days.value = Array.from({ length: daysInMonth }, (_, i) => i + 1)
}
// 初始化pickerValue
const initializePickerValue = () => {
let newPickerValue = []
if (
[
'YYYY-MM-DD HH:mm:ss',
'YYYY-MM-DD HH:mm',
'YYYY-MM-DD HH',
'YYYY',
'YYYY-MM',
'YYYY-MM-DD',
].includes(props.format)
) {
const dateTime = props.modelValue ? dayjs(props.modelValue) : dayjs()
if (showYear.value) {
const yearIndex = years.value.indexOf(dateTime.year())
newPickerValue.push(yearIndex >= 0 ? yearIndex : 0)
}
if (showMonth.value) {
const monthIndex = months.value.indexOf(dateTime.month() + 1)
newPickerValue.push(monthIndex >= 0 ? monthIndex : 0)
}
if (showDay.value) {
const dayIndex = days.value.indexOf(dateTime.date())
newPickerValue.push(dayIndex >= 0 ? dayIndex : 0)
}
if (showHour.value) {
newPickerValue.push(dateTime.hour())
}
if (showMinute.value) {
newPickerValue.push(dateTime.minute())
}
if (showSecond.value) {
newPickerValue.push(dateTime.second())
}
} else if (['HH:mm:ss', 'HH:mm', 'mm:ss', 'HH', 'mm', 'ss'].includes(props.format)) {
const dateTime = props.modelValue
if (dateTime) {
newPickerValue = dateTime.split(':').map((item) => +item)
} else {
// 获取当前时间
const now = dayjs()
if (showHour.value) {
newPickerValue.push(now.hour())
}
if (showMinute.value) {
newPickerValue.push(now.minute())
}
if (showSecond.value) {
newPickerValue.push(now.second())
}
}
}
pickerValue.value = newPickerValue
}
// 处理选择变化
const handleChange = (e: any) => {
const values = e.detail.value
pickerValue.value = values
// 如果选择了年月,需要更新日期
if (showYear.value && showMonth.value && showDay.value) {
updateDays()
}
}
// 点击确认
const confirm = () => {
const values = pickerValue.value
let selectedDate = dayjs()
let index = 0
// 只设置用户选择的时间部分,未选择的保持当前时间
if (showYear.value && values[index] !== undefined) {
selectedDate = selectedDate.year(years.value[values[index]])
index++
}
if (showMonth.value && values[index] !== undefined) {
selectedDate = selectedDate.month(months.value[values[index]] - 1)
index++
}
if (showDay.value && values[index] !== undefined) {
selectedDate = selectedDate.date(days.value[values[index]])
index++
}
if (showHour.value && values[index] !== undefined) {
selectedDate = selectedDate.hour(hours.value[values[index]])
index++
}
if (showMinute.value && values[index] !== undefined) {
selectedDate = selectedDate.minute(minutes.value[values[index]])
index++
}
if (showSecond.value && values[index] !== undefined) {
selectedDate = selectedDate.second(seconds.value[values[index]])
index++
}
popupShow.value = false
const output = selectedDate.format(props.format)
emit('update:modelValue', output)
emit('change', output)
}
// 点击取消
const cancel = () => {
popupShow.value = false
}
// 清空选择
const handleClear = () => {
emit('update:modelValue', '')
emit('change', '')
}
// 点击输入框
const handleClick = () => {
if (!props.disabled) {
initData()
initializePickerValue()
popupShow.value = true
}
}
</script>
<style lang="scss" scoped>
.DateTime {
.picker-view {
width: 100%;
height: 400rpx;
}
.picker-item {
line-height: 88rpx;
text-align: center;
}
.operation {
display: flex;
justify-content: space-between;
padding: 20rpx;
border-bottom: 1rpx solid #eee;
.cancel,
.confrim {
padding: 10rpx 30rpx;
}
.confrim {
color: #2979ff;
}
}
.inputArea {
position: relative;
.u-icon-close {
position: absolute;
right: 15px;
top: calc(14px + 4px);
color: #585858;
font-size: 15px;
}
&.clear {
:deep(.wd-input__body) {
padding-right: 20px;
}
}
}
}
</style>

View File

@@ -0,0 +1,100 @@
<template>
<view
class="wrap"
:style="{
'--borderColor': borderColor,
'--imgWidth': imgWidth,
'--imgHeight': imgHeight,
}"
>
<wd-grid :clickable="clickable" :column="column">
<template v-for="(item, index) in modelValue" :key="item[itemKey]">
<wd-grid-item
:custom-class="getClass(index)"
use-icon-slot
:text="item.text"
@itemclick="handleClik(item, index)"
>
<template #icon>
<wd-img :width="imgWidth" :height="imgHeight" :src="item.img"></wd-img>
</template>
</wd-grid-item>
</template>
</wd-grid>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
defineOptions({
name: 'Grid',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
modelValue: {
type: Array,
default: () => [],
},
column: {
type: Number,
default: 4,
},
itemKey: {
type: String,
default: 'id',
},
imgWidth: {
type: String,
default: '28px',
},
imgHeight: {
type: String,
default: '28px',
},
clickable: {
type: Boolean,
default: true,
},
borderColor: {
type: String,
default: 'rgba(165, 165, 165, 0.1)',
},
})
const emit = defineEmits(['itemClik'])
const getClass = (index) => {
let className = ''
if (index < props.column) {
className = 'first-row'
}
if ((index + 1) % props.column == 0) {
className += ` lastCol`
}
return className
}
const handleClik = (item, index) => {
emit('itemClik', item, index)
}
</script>
<style lang="scss" scoped>
:deep(.wd-grid-item) {
box-sizing: border-box;
border-right: 1px solid var(--borderColor, rgba(165, 165, 165, 0.1));
border-bottom: 1px solid var(--borderColor, rgba(165, 165, 165, 0.1));
&.first-row {
border-top: 1px solid var(--borderColor, rgba(165, 165, 165, 0.1));
}
&.lastCol {
border-right: none;
}
.wd-grid-item__text {
margin-top: 10px;
}
.wd-grid-item__wrapper {
width: var(--imgWidth, 28px) !important;
height: var(--imgHeight, 28px) !important;
}
}
</style>

View File

@@ -0,0 +1,144 @@
<template>
<view class="previewImage" @tap="close">
<view class="page" v-if="urls.length > 0">
<text class="text">{{ current + 1 }} / {{ urls.length }}</text>
</view>
<swiper
class="swiper"
:current="current"
@change="swiperChange"
@touchstart="handleTouchStart"
@touchend="handleTouchEnd"
>
<swiper-item class="swiperItem" v-for="(item, index) in urls" :key="index">
<movable-area class="movable-area" scale-area>
<movable-view
class="movable-view"
direction="all"
:inertia="true"
damping="100"
scale="true"
scale-min="1"
scale-max="4"
:scale-value="scale"
>
<scroll-view scroll-y="true" class="uni-scroll-view">
<view class="scroll-view">
<image
:key="index"
class="image"
:src="item"
mode="widthFix"
@longpress="onLongpress(item)"
/>
</view>
</scroll-view>
</movable-view>
</movable-area>
</swiper-item>
</swiper>
</view>
</template>
<script>
export default {
props: {
urls: {
type: Array,
required: true,
default: () => {
return []
},
},
},
data() {
return {
show: false,
current: 0, //当前页
scale: 1,
isZooming: false, // 是否处于缩放状态
}
},
methods: {
open(current) {
this.current = this.urls.findIndex((item) => item === current)
},
//关闭
close() {
if (!this.isZooming) {
this.show = false
this.current = 0
this.$emit('close')
}
},
//图片改变
swiperChange(e) {
this.current = e.detail.current
},
//监听长按
onLongpress(e) {
this.$emit('onLongpress', e)
},
handleTouchStart() {
this.isZooming = true
},
handleTouchEnd() {
this.isZooming = false
},
},
}
</script>
<style lang="scss" scoped>
.previewImage {
z-index: 9999;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000000;
.swiper {
width: 100%;
height: 100vh;
.swiperItem {
.movable-area {
height: 100%;
width: 100%;
.movable-view {
width: 100%;
min-height: 100%;
.uni-scroll-view {
height: 100vh;
}
.scroll-view {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
.image {
width: 100%;
height: auto;
}
}
}
}
}
}
.page {
position: absolute;
z-index: 9999;
width: 100%;
top: 60rpx;
text-align: center;
.text {
color: #fff;
font-size: 32rpx;
background-color: rgba(0, 0, 0, 0.5);
padding: 3rpx 16rpx;
border-radius: 20rpx;
user-select: none;
}
}
}
</style>

View File

@@ -0,0 +1,49 @@
<template>
<view :style="[{backgroundColor:backgroundColor}]">
<map
v-if="latitude && longitude"
class="map-content"
:latitude="latitude"
:longitude="longitude"
:markers="marker"
:scale="scale"
:circles="circles"
>
</map>
</view>
</template>
<script setup>
import useGeo from '@/hooks/useGeoPosition'
// 定义 props
const props = defineProps({
compLatitude: {
type: Number,
default: 40.009390,
required: false
},
compLongitude: {
type: Number,
default: 116.374322,
required: false
}
});
const emit = defineEmits(['success','fail']);
const backgroundColor = 'transparent';
// 引入地理定位逻辑
let [{inCircle,getAddress,getMyAddress,latitude,longitude,marker,scale,circles},{refreshLocation}] = useGeo({ compLatitude: props.compLatitude, compLongitude: props.compLongitude }, emit);
defineExpose({
inCircle,
getAddress,
getMyAddress,
refreshLocation
});
</script>
<style lang="scss" scoped>
.map-content {
width: 100%;
height: 250px;
}
</style>

View File

@@ -0,0 +1,341 @@
<template>
<view class='t-toptips' :style="{top: top,background: cubgColor}" :class="[show?'t-top-show':'']">
<view v-if="loading" class="flex flex-sub">
<view class="cu-progress flex-sub round striped active sm">
<view :style="{ background: color,width: value + '%'}"></view>
</view>
<text class="margin-left">{{value}}%</text>
</view>
<block v-else>{{msg}}</block>
<!-- #ifdef H5 -->
<view ref="input" class="input"></view>
<!-- #endif -->
</view>
</template>
<script lang="ts" setup>
import { ref, nextTick } from 'vue'
import {useToast} from "wot-design-uni";
import {http} from "@/utils/http";
const props = defineProps({
top: {
type: String,
default: 'auto'
},
bgColor: {
type: String,
default: 'rgba(49, 126, 243, 0.5)',
},
color: {
type: String,
default: '#e54d42',
},
type: {
type: String,
default: 'file'
}
})
const toast = useToast()
const emits = defineEmits(['up-success', 'up-error'])
const cubgColor = ref('')
const loading = ref(false)
const value = ref(5)
const show = ref(false)
const msg = ref('执行完毕~')
const input = ref(null)
const getRequest = (url) => {
let theRequest = {}
let index = url.indexOf("?")
if (index != -1) {
let str = url.substring(index+1)
let strs = str.split("&")
for(let i = 0; i < strs.length; i++) {
theRequest[strs[i].split("=")[0]] = unescape(strs[i].split("=")[1])
}
}
return theRequest
}
/**
*
* @param currentWebview 当前窗口webview对象
* @param url 上传接口地址
* @param name 上传文件key值
* @param header 上传接口请求头
* @param formData body内其他参数
*/
const appChooseFile = ({currentWebview, url, name = 'file', header, ...formData} = {} as any) => {
// #ifdef APP-PLUS
let wv = plus.webview.create("","/hybrid/html/index.html",{
//'uni-app': 'none',
top: '0px',
height: '100%',
background: 'transparent'
},{
url,
header,
formData,
key: name,
})
wv.loadURL("/hybrid/html/index.html")
currentWebview.append(wv)
wv.overrideUrlLoading({mode:'reject'},(e)=>{
let {fileName, id, fileSize} = getRequest(e.url) as any
fileName = unescape(fileName)
id = unescape(id)
fileSize = unescape(fileSize)
console.log("fileSize", fileSize)
return onCommit(
emits('up-success', {fileName, fileSize, data: {id, statusCode: 200}})
)
})
// #endif
}
/**
*
* @param url 上传接口地址
* @param name 上传文件key值
* @param header 上传接口请求头
* @param formData body内其他参数
*/
const h5ChooseFile = ({url, name = 'file', header, ...formData} = {} as any) => {
// #ifdef H5
const inputEl = document.createElement('input');
inputEl.type = 'file'
inputEl.style.display = 'none'
inputEl.id = 'file'
if(props.type === 'image'){
inputEl.accept = 'image/*'
}
input.value.$el.appendChild(inputEl)
document.getElementById("file").click()
inputEl.onchange = (event) => {
uploadH5(event, url, header)
}
// #endif
}
/**
*
* @param url
* @param name
* @param header
* @param formData
*/
const wxChooseFile = ({url, name = 'file', header, ...formData} = {} as any) => {
wx.chooseMessageFile({
count: 1,
type: 'file',
success: ({tempFiles}) => {
let [{path: filePath, name: fileName}] = tempFiles
setDefUI()
console.log("tempFiles==>::", tempFiles)
console.log("filePath==>::", filePath)
console.log("url==>::", url)
return http.upload(url, {
filePath: filePath,
name: 'file'
}).then((res : any) => {
if (res.statusCode === 200) {
console.log("wxChooseFile返回值success::", res)
let data = res.data
return onCommit(emits('up-success', {fileName, data}))
}
}).catch(err => {
console.log("wxChooseFile返回值err", err)
return errorHandler('文件上传失败', upErr)
})
},
fail: () => errorHandler('文件选择失败', upErr)
})
}
/**
* 上传
* @param param
*/
const upload = (param = {} as any) => {
if (!param.url) {
toast.warning('上传地址不正确')
return
}
if (loading.value) {
toast.warning('还有个文件玩命处理中,请稍候..')
return
}
// #ifdef APP-PLUS
return appChooseFile(param)
// #endif
}
/**
* 打开文件
* @param filePath
*/
const open = (filePath) => {
let system = uni.getSystemInfoSync().platform
if(system == 'ios'){
filePath = encodeURI(filePath)
}
uni.openDocument({
filePath,
success: (res) => { console.log('打开文档成功') }
})
}
/**
* type: temporary=返回临时地址local=长期保存到本地
* @param url
* @param type
*/
const download = (url, type = 'temporary') => {
if (loading.value) {
toast.warning('还有个文件玩命处理中,请稍候..')
return
}
setDefUI()
return new Promise((resolve, reject) => {
let downloadTask = uni.downloadFile({
url,
success: ({statusCode, tempFilePath}) => {
if (statusCode === 200) {
if (type == 'local') {
uni.saveFile({
tempFilePath,
success: ({savedFilePath}) => onCommit(resolve(savedFilePath)),
fail: () => errorHandler('下载失败', reject)
})
}
else {
onCommit(resolve(tempFilePath))
}
}
},
fail: () => errorHandler('下载失败', reject)
})
downloadTask.onProgressUpdate(({progress = 0}) => {
if (progress <= 100) {
nextTick(() => {
value.value = progress
})
}
})
})
}
const onCommit = (resolve) => {
msg.value = '执行完毕~'
loading.value = false
cubgColor.value = 'rgba(57, 181, 74, 0.5)'
setTimeout(() => { show.value = false }, 1500)
return resolve
}
// #ifdef H5
const uploadH5 = (event, url, header) => {
let _file = event.target.files[0]
let formData = new FormData()
formData.append("file", _file)
let xhr = new XMLHttpRequest()
xhr.open("post", url, true)
for (let keys in header) {
xhr.setRequestHeader(keys, header[keys])
}
xhr.upload.onloadstart = () => {
toast.loading('上传中...')
}
xhr.upload.onprogress = (res) => {
if(res.loaded / res.total == 1){
setTimeout(() => {
toast.close()
}, 5000)
}
}
xhr.onload = (res:any) => {
console.log("上传完成", res)
console.log(res.target.response)
toast.close()
if (res.target.status === 200) {
let data = JSON.parse(res.target.response)
console.log("data", data)
let fileName = data.message
onCommit(emits('up-success', {fileSize: _file.size, fileName, data}))
} else {
toast.error('文件上传失败')
}
const inputEl = document.getElementById("file");
input.value.$el.removeChild(inputEl)
}
xhr.onerror = (data) => {
toast.close()
toast.error('上传失败')
}
xhr.ontimeout = function(event) {
toast.close()
toast.error('上传超时,请刷新重试')
}
xhr.send(formData)
}
// #endif
const setDefUI = () => {
cubgColor.value = props.bgColor
value.value = 0
loading.value = true
show.value = true
}
const upErr = (errText) => {
emits('up-error', errText)
}
const errorHandler = (errText, reject) => {
msg.value = errText
loading.value = false
cubgColor.value = 'rgba(229, 77, 66, 0.5)'
setTimeout(() => { show.value = false }, 1500)
return reject(errText)
}
defineExpose({
upload,
open,
download
})
</script>
<style scoped>
.t-toptips {
width: 100%;
padding: 18upx 30upx;
box-sizing: border-box;
position: fixed;
z-index: 90;
color: #fff;
font-size: 30upx;
left: 0;
display: flex;
align-items: center;
justify-content: center;
word-break: break-all;
display: none;
transform: translateZ(0) translateY(-100%);
transition: all 0.3s ease-in-out;
}
.t-top-show {
transform: translateZ(0) translateY(0);
display: block;
}
</style>

View File

@@ -0,0 +1,296 @@
<template>
<view class="pageLayout">
<view
v-if="navbarShow"
:class="{ pageNav: true, transparent: navBgTransparent, fixed: navFixed }"
:style="{ height: `${statusBarHeight + navHeight}px` }"
>
<view class="statusBar" :style="{ height: `${statusBarHeight}px` }"></view>
<wd-navbar
:bordered="false"
:title="navTitle"
:leftText="navLeftText"
:leftArrow="navLeftArrow"
:rightText="isMp ? '' : navRightText"
@clickLeft="handleClickLeft"
@clickRight="handleClickRight"
:custom-class="getClass()"
>
<template v-if="isMp === false && $slots.navRight" #right>
<slot name="navRight"></slot>
</template>
<template v-if="isMp" #left>
<!-- 为了在小程序上美观 -->
<template v-if="$slots.navRight && isShowNavRightTextMp">
<view class="btnGroup">
<view class="left" @click.stop="handleClickLeft">{{ navLeftText }}</view>
<view class="right" @click.stop="() => emit('navRightMp')">{{ navRightTextMp }}</view>
</view>
</template>
<template v-else-if="$slots.navLeft">
<slot name="navLeft"></slot>
</template>
<template v-else>
<view class="mpNavLeft" @click.stop="handleClickLeft">
<wd-icon v-if="navLeftArrow" name="thin-arrow-left" size="14px"></wd-icon>
<view>{{ navLeftText }}</view>
</view>
</template>
</template>
</wd-navbar>
</view>
<view class="pageContent">
<view class="content">
<slot></slot>
</view>
<!--微信中导航右侧的在这儿生成-->
<view class="navRight" v-if="isMp && !$slots.navRight && mpNavRightIsBottom && navRightText">
<wd-button @click="handleClickRight">{{ navRightText }}</wd-button>
</view>
</view>
<view class="tabbar"></view>
</view>
</template>
<script setup lang="ts">
import { useSlots } from 'vue'
import { useRouter } from '@/plugin/uni-mini-router'
import { useParamsStore } from '@/store/page-params'
import { isMp } from '@/utils/platform'
// const isMp = true
defineOptions({
name: 'pageLayout',
options: {
// apply-shared当前页面样式会影响到子组件样式.(小程序)
// shared当前页面样式影响到子组件子组件样式也会影响到当前页面.(小程序)
styleIsolation: 'shared',
},
})
const paramsStore = useParamsStore()
const router = useRouter()
const props = defineProps({
backRouteName: {
type: String,
default: '',
},
backRoutePath: {
type: String,
default: '',
},
routeParams: {
type: Object,
default: () => {},
},
routeQuery: {
type: Object,
default: () => {},
},
routeMethod: {
type: String,
default: 'replace',
},
navbarShow: {
type: Boolean,
default: true,
},
navBgTransparent: {
type: Boolean,
default: false,
},
navFixed: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'page', // 'page','popup'
},
navTitle: {
type: String,
default: '',
},
navLeftText: {
type: String,
default: '返回',
},
navLeftArrow: {
typeof: Boolean,
default: true,
},
navRightText: {
typeof: String,
default: '',
},
// 当有右侧有slot时在小程序上不显示slot显示文字 (默认显示)
isShowNavRightTextMp: {
typeof: Boolean,
default: true,
},
// 当有右侧有slot时在小程序上不显示slot显示文字
navRightTextMp: {
typeof: String,
default: '确定',
},
// 右侧的文本在小程序端是否生成在页面底部(否:就得业务中自己写)
mpNavRightIsBottom: {
typeof: Boolean,
default: true,
},
})
const slot = useSlots()
const globalData = getApp().globalData
const { systemInfo, navHeight } = globalData
const { statusBarHeight } = systemInfo
const emit = defineEmits(['navBack', 'navRight', 'navRightMp'])
const handleClickLeft = () => {
emit('navBack')
// 只有在页面中才默认返回,弹层中不返回
if (props.type === 'page') {
const pages = getCurrentPages()
if (props.backRouteName || props.backRoutePath) {
const prevPage = pages[pages.length - 2]
if (prevPage) {
const route = prevPage.route
const name = route.split('/').pop()
if (route === props.backRoutePath || props.backRouteName === name) {
router.back()
clearPageParamsCache()
return
}
}
if (props.backRouteName) {
router[props.routeMethod]({ name: props.backRouteName, params: props.routeParams })
clearPageParamsCache()
} else {
router[props.routeMethod]({ name: props.backRoutePath, query: props.routeQuery })
clearPageParamsCache()
}
} else {
router.back()
clearPageParamsCache()
}
}
}
const clearPageParamsCache = () => {
// 清除页面传参缓存
const pages = getCurrentPages()
const curPage = pages[pages.length - 1]
const curRoute = curPage.route
const name = curRoute.split('/').pop()
paramsStore.clearPageParams(name)
}
const handleClickRight = () => {
emit('navRight')
}
const getClass = () => {
const cls = `nav ${isMp ? 'mp' : ''} ${slot.navRight ? 'slot_navRight' : ''}`
return cls
}
console.log('props:', props)
</script>
<style lang="scss" scoped>
.pageLayout {
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
.pageNav {
background-image: linear-gradient(45deg, #0081ff, #1cbbb4);
&.transparent {
background-image: none;
}
&.fixed {
position: fixed;
top: 0;
left: 0;
z-index: 1;
width: 100%;
}
.statusBar {
width: 100%;
height: 0;
}
:deep(.wd-navbar) {
background-color: transparent;
--wot-navbar-title-font-weight: 400;
--wot-navbar-arrow-size: 18px;
--wot-navbar-desc-font-size: 14px;
--wot-navbar-title-font-size: 16px;
&.mp {
&.slot_navRight {
padding-right: 90px;
.wd-navbar__text {
display: none;
}
.wd-navbar__content {
display: flex;
}
.wd-navbar__left,
.wd-navbar__title,
.wd-navbar__right {
position: static;
}
.wd-navbar__title {
max-width: none;
}
}
}
}
}
.pageContent {
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
background-color: #f1f1f1;
.content {
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
}
.navRight {
padding: 15px;
padding-top: 10px;
display: flex;
margin-bottom: calc(constant(safe-area-inset-bottom));
margin-bottom: calc(env(safe-area-inset-bottom));
justify-content: flex-end;
:deep(.wd-button) {
--wot-button-medium-height: 38px;
width: 70%;
display: block;
margin: 0 auto;
}
}
}
.tabbar {
/* #ifdef H5 */
height: var(--window-bottom);
/* #endif */
}
.btnGroup {
display: flex;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.6);
height: 28px;
line-height: 28px;
color: #fff;
align-items: center;
.left {
border-right: 1px solid rgba(255, 255, 255, 0.6);
}
.left,
.right {
padding: 0 8px;
font-size: 14px;
}
}
.mpNavLeft {
display: flex;
color: #fff;
}
}
</style>

View File

@@ -0,0 +1,110 @@
<template>
<view :style="{ height: addUnit(height) }">
<view :class="`wd-navbar ${customClass} ${fixed ? 'is-fixed' : ''} ${bordered ? 'is-border' : ''}`" :style="rootStyle">
<view class="wd-navbar__content">
<view class="wd-navbar__capsule" v-if="$slots.capsule">
<slot name="capsule" />
</view>
<view :class="`wd-navbar__left ${leftDisabled ? 'is-disabled' : ''}`" @click="handleClickLeft" v-else-if="!$slots.left">
<wd-icon v-if="leftArrow" name="arrow-left" custom-class="wd-navbar__arrow" />
<view v-if="leftText" class="wd-navbar__text">{{ leftText }}</view>
</view>
<view v-else :class="`wd-navbar__left ${leftDisabled ? 'is-disabled' : ''}`" @click="handleClickLeft">
<slot name="left" />
</view>
<view class="wd-navbar__title">
<slot name="title" />
<block v-if="!$slots.title && title">{{ title }}</block>
</view>
<view :class="`wd-navbar__right ${rightDisabled ? 'is-disabled' : ''}`" @click="handleClickRight" v-if="$slots.right || rightText">
<slot name="right" />
<view v-if="!$slots.right && rightText" class="wd-navbar__text" hover-class="wd-navbar__text--hover" :hover-stay-time="70">
{{ rightText }}
</view>
</view>
</view>
</view>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-navbar',
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { type CSSProperties, computed, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue'
import { getRect, addUnit, isDef, objToStyle } from 'wot-design-uni/components/common/util'
import { navbarProps } from './types'
const props = defineProps(navbarProps)
const emit = defineEmits(['click-left', 'click-right'])
const height = ref<number | ''>('') // 占位高度
const { statusBarHeight } = uni.getSystemInfoSync()
watch(
[() => props.fixed, () => props.placeholder],
() => {
setPlaceholderHeight()
},
{ deep: true, immediate: false }
)
const rootStyle = computed(() => {
const style: CSSProperties = {}
if (props.fixed && isDef(props.zIndex)) {
style['z-index'] = props.zIndex
}
if (props.safeAreaInsetTop) {
style['padding-top'] = addUnit(statusBarHeight || 0)
}
return `${objToStyle(style)};${props.customStyle}`
})
onMounted(() => {
if (props.fixed && props.placeholder) {
nextTick(() => {
setPlaceholderHeight()
})
}
})
function handleClickLeft() {
if (!props.leftDisabled) {
emit('click-left')
}
}
function handleClickRight() {
if (!props.rightDisabled) {
emit('click-right')
}
}
const { proxy } = getCurrentInstance() as any
function setPlaceholderHeight() {
if (!props.fixed || !props.placeholder) {
return
}
getRect('.wd-navbar', false, proxy).then((res) => {
height.value = res.height as number
})
}
</script>
<style lang="scss">
@import './index.scss';
</style>

View File

@@ -0,0 +1,93 @@
@import "wot-design-uni/components/common/abstracts/_mixin.scss";
@import "wot-design-uni/components/common/abstracts/variable.scss";
.wot-theme-dark {
@include b(navbar) {
background-color: $-dark-background;
@include e(title) {
color: $-dark-color;
}
@include e(text) {
color: $-dark-color;
}
:deep(.wd-navbar__arrow) {
color: $-dark-color;
}
}
}
@include b(navbar) {
position: relative;
text-align: center;
user-select: none;
height: $-navbar-height;
line-height: $-navbar-height;
background-color: $-navbar-background;
box-sizing: content-box;
@include e(content) {
position: relative;
height: 100%;
width: 100%;
}
@include e(title) {
max-width: 60%;
height: 100%;
margin: 0 auto;
color: $-navbar-color;
font-weight: $-navbar-title-font-weight;
font-size: $-navbar-title-font-size;
@include lineEllipsis();
}
@include e(text) {
display: inline-block;
vertical-align: middle;
color: $-navbar-desc-font-color;
}
@include e(left, right, capsule) {
position: absolute;
top: 0;
bottom: 0;
font-size: $-navbar-desc-font-size;
display: flex;
align-items: center;
padding: 0 12px;
@include when(disabled) {
opacity: $-navbar-disabled-opacity;
}
}
@include e(left, capsule) {
left: 0;
}
@include e(right) {
right: 0;
}
@include edeep(arrow) {
font-size: $-navbar-arrow-size;
color: $-navbar-color;
}
@include when(border) {
@include halfPixelBorder('bottom');
}
@include when(fixed) {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 500;
}
}

View File

@@ -0,0 +1,52 @@
import type { ExtractPropTypes } from 'vue'
import { baseProps, makeBooleanProp, makeNumberProp } from 'wot-design-uni/components/common/props'
export const navbarProps = {
...baseProps,
/**
* 标题文字
*/
title: String,
/**
* 左侧文案
*/
leftText: String,
/**
* 右侧文案
*/
rightText: String,
/**
* 是否显示左侧箭头
*/
leftArrow: makeBooleanProp(false),
/**
* 是否显示下边框
*/
bordered: makeBooleanProp(true),
/**
* 是否固定到顶部
*/
fixed: makeBooleanProp(false),
/**
* 固定在顶部时,是否在标签位置生成一个等高的占位元素
*/
placeholder: makeBooleanProp(false),
/**
* 导航栏 z-index
*/
zIndex: makeNumberProp(500),
/**
* 是否开启顶部安全区适配
*/
safeAreaInsetTop: makeBooleanProp(false),
/**
* 是否禁用左侧按钮,禁用时透明度降低,且无法点击
*/
leftDisabled: makeBooleanProp(false),
/**
* 是否禁用右侧按钮,禁用时透明度降低,且无法点击
*/
rightDisabled: makeBooleanProp(false)
}
export type NavbarProps = ExtractPropTypes<typeof navbarProps>

View File

@@ -0,0 +1,142 @@
<template>
<view class="Popup">
<view class="inputArea" :class="{ clear: !!showText }" @click="handleClick">
<wd-input
:placeholder="`请选择${$attrs.label}`"
type="text"
readonly
v-model="showText"
clearable
v-bind="$attrs"
/>
<view v-if="!!showText && !$attrs.disabled" class="u-iconfont u-icon-close" @click.stop="handleClear"></view>
</view>
<popupReportModal
v-if="reportModal.show"
:code="code"
:showFiled="reportModal.showFiled"
:multi="multi"
@close="handleClose"
@change="handleChange"
></popupReportModal>
</view>
</template>
<script setup lang="ts">
import { ref, watch, useAttrs } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import popupReportModal from './components/popupReportModal.vue'
defineOptions({
name: 'Popup',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
code: {
type: String,
required: true,
default: '',
},
fieldConfig: {
type: Array,
required: true,
default: () => [],
},
setFieldsValue: {
type: Function,
required: true,
default: () => {},
},
modelValue: {
type: String,
default: '',
},
multi: {
type: Boolean,
default: false,
},
spliter: {
type: String,
default: ',',
},
})
const emit = defineEmits(['change', 'update:modelValue'])
const toast = useToast()
const showText = ref('')
const attrs: any = useAttrs()
const reportModal = reactive({
show: false,
showFiled: props.fieldConfig.map((item) => item['source']).join(','),
})
if (!props.code || props.fieldConfig.length == 0) {
toast.error('popup参数未正确配置!')
}
/**
* 监听value数值
*/
watch(
() => props.modelValue,
(val) => {
showText.value = val && val.length > 0 ? val.split(props.spliter).join(',') : ''
},
{ immediate: true },
)
function callBack(rows) {
let fieldConfig: any = props.fieldConfig
//匹配popup设置的回调值
let values = {}
let labels = []
for (let item of fieldConfig) {
let val = rows.map((row) => row[item.source])
val = val.length == 1 ? val[0] : val.join(',')
item.target.split(',').forEach((target) => {
values[target] = val
})
}
showText.value = labels.join(',')
props.setFieldsValue(values)
emit('change', values)
// emit('update:modelValue', values)
}
// 清空
const handleClear = () => {
showText.value = ''
handleChange([])
}
const handleClick = () => {
if (!attrs.disabled) {
reportModal.show = true
}
}
const handleClose = () => {
reportModal.show = false
}
const handleChange = (data) => {
console.log('选中的值:', data)
callBack(data)
}
</script>
<style lang="scss" scoped>
.inputArea {
position: relative;
.u-icon-close {
position: absolute;
right: 15px;
top: calc(14px + 4px);
color: #585858;
font-size: 15px;
}
&.clear {
:deep(.wd-input__body) {
padding-right: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,303 @@
<template>
<wd-popup position="bottom" v-model="show">
<PageLayout
:navTitle="navTitle"
type="popup"
navRightText="确定"
@navRight="handleConfirm"
@navBack="handleCancel"
>
<view class="wrap">
<z-paging
ref="paging"
:fixed="false"
v-model="dataList"
@query="queryList"
:default-page-size="15"
>
<template #top>
<wd-search
hide-cancel
:placeholder="search.placeholder"
v-model="search.keyword"
@search="handleSearch"
@clear="handleClear"
/>
</template>
<template v-if="multi">
<wd-checkbox-group shape="square" v-model="checkedValue">
<template v-for="(item, index) in dataList" :key="index">
<view class="list" @click="hanldeCheck(index)">
<view class="left text-gray-5">
<template v-for="(cItem, cIndex) in columns" :key="cIndex">
<view class="row">
<text class="label">{{ cItem.title }}</text>
<text class="value">{{ item[cItem.dataIndex] }}</text>
</view>
</template>
</view>
<view class="right" @click.stop>
<wd-checkbox ref="checkboxRef" :modelValue="index"></wd-checkbox>
</view>
</view>
</template>
</wd-checkbox-group>
</template>
<template v-else>
<wd-radio-group shape="dot" v-model="checkedValue">
<template v-for="(item, index) in dataList" :key="index">
<wd-cell>
<view class="list" @click="hanldeCheck(index)">
<view class="left text-gray-5">
<template v-for="(cItem, cIndex) in columns" :key="cIndex">
<view class="row">
<text class="label">{{ cItem.title }}</text>
<text class="value">{{ item[cItem.dataIndex] }}</text>
</view>
</template>
</view>
<view class="right" @click.stop>
<wd-radio :value="index"></wd-radio>
</view>
</view>
</wd-cell>
</template>
</wd-radio-group>
</template>
</z-paging>
</view>
</PageLayout>
</wd-popup>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import { isArray } from '@/utils/is'
defineOptions({
name: 'popupReportModal',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
code: {
type: String,
default: '',
required: true,
},
showFiled: {
type: String,
default: '',
required: true,
},
multi: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['change', 'close'])
const toast = useToast()
const show = ref(true)
const api = {
getColumns: '/online/cgreport/api/getRpColumns',
getData: '/online/cgreport/api/getData',
getQueryInfo: '/online/cgreport/api/getQueryInfo',
}
console.log('props:::', props)
const navTitle = ref('')
const paging = ref(null)
const dataList = ref([])
// 报表id
let rpConfigId = null
let loadedColumns = false
const dictOptions = ref([])
const columns = ref([])
const checkedValue: any = ref(props.multi ? [] : '')
const checkboxRef = ref(null)
const search = reactive({
keyword: '',
placeholder: '',
field: '',
})
const handleClose = () => {
setTimeout(() => {
emit('close')
}, 400)
}
const handleConfirm = () => {
if (checkedValue.value.length == 0) {
toast.warning('还没选择~')
return
}
const result = []
let value = checkedValue.value
if (!Array.isArray(checkedValue.value)) {
value = [checkedValue.value]
}
value.forEach((index) => {
result.push(dataList.value[index])
})
show.value = false
emit('change', result)
handleClose()
}
const handleCancel = () => {
show.value = false
handleClose()
console.log('取消了~')
}
// 搜索
function handleSearch() {
paging.value.reload()
}
// 清除搜索条件
function handleClear() {
search.keyword = ''
handleSearch()
}
const hanldeCheck = (index) => {
if (props.multi) {
if (Array.isArray(checkboxRef.value)) {
checkboxRef.value[index].toggle()
}
} else {
checkedValue.value = index
}
}
const getQueryInfo = () => {
const analysis = (data = []) => {
if (data.length) {
search.placeholder = `请输入${data[0].label}`
search.field = data[0].field
} else {
const item = columns[0] ?? {}
search.placeholder = `请输入${item.title}`
search.field = item.dataIndex
}
}
http
.get(`${api.getQueryInfo}/${rpConfigId}`)
.then((res: any) => {
if (res.success) {
analysis(res.result)
} else {
analysis()
}
})
.catch((err) => {
analysis()
})
}
const getRpColumns = () => {
return new Promise<void>((resolve, reject) => {
if (loadedColumns) {
resolve()
} else {
http
.get(`${api.getColumns}/${props.code}`)
.then((res: any) => {
if (res.success) {
loadedColumns = true
const { result } = res
navTitle.value = result.cgRpConfigName
dictOptions.value = result.dictOptions
rpConfigId = result.cgRpConfigId
const fileds = props.showFiled.split(',')[0]
result.columns?.forEach((item) => {
if (fileds.includes(item.dataIndex)) {
columns.value.push(item)
}
})
// 最少展示三个字段(字段够多时)
const minNum = 3
if (columns.value.length < minNum) {
result.columns?.forEach((item) => {
const findItem = columns.value.find((o) => o.dataIndex == item.dataIndex);
if (!findItem) {
if (columns.value.length < minNum) {
columns.value.push(item)
}
}
})
}
getQueryInfo()
resolve()
} else {
reject()
}
})
.catch((err) => {
reject()
})
}
})
}
const queryList = (pageNo, pageSize) => {
const pararms = { pageNo, pageSize }
if (search.keyword) {
pararms[search.field] = `*${search.keyword}*`
}
getRpColumns()
.then(() => {
http
.get(`${api.getData}/${rpConfigId}`, pararms)
.then((res: any) => {
if (res.success && res.result.records) {
paging.value.complete(res.result.records ?? [])
} else {
paging.value.complete(false)
}
})
.catch((err) => {})
})
.catch((err) => {})
}
</script>
<style lang="scss" scoped>
:deep(.wd-cell) {
--wot-color-white: tranparent;
--wot-cell-padding: 0;
.wd-cell__wrapper {
--wot-cell-wrapper-padding: 0;
}
.wd-cell__left {
display: none;
}
}
:deep(.wd-checkbox-group) {
--wot-checkbox-bg: tranparent;
}
:deep(.wd-radio-group) {
--wot-radio-bg: tranparent;
}
.list {
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
line-height: 1.8;
padding: 16px;
margin-top: 16px;
.left {
display: flex;
justify-content: center;
flex-direction: column;
.row {
display: flex;
}
}
.right {
:deep(.wd-checkbox) {
margin-bottom: 0;
}
}
}
.wrap {
height: 100%;
}
</style>

View File

@@ -0,0 +1,219 @@
<template>
<view class="PopupDict">
<view class="inputArea" :class="{ clear: !!showText }" @click.stop="handleClick">
<wd-select-picker
v-model="showText"
:columns="options"
readonly
:type="multi ? 'checkbox' : 'radio'"
@click="handleClick"
v-bind="$attrs"
></wd-select-picker>
<view v-if="!!showText && !disabled" class="u-iconfont u-icon-close" @click.stop="handleClear"></view>
</view>
<popupReportModal
v-if="reportModal.show"
:code="code"
:showFiled="labelFiled"
:multi="multi"
@close="handleClose"
@change="handleChange"
></popupReportModal>
</view>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import popupReportModal from '@/components/Popup/components/popupReportModal.vue'
defineOptions({
name: 'PopupDict',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
dictCode: {
type: String,
required: true,
default: '',
},
modelValue: {
type: String,
default: '',
},
multi: {
type: Boolean,
default: false,
},
spliter: {
type: String,
default: ',',
},
disabled: {
type: Boolean,
default: false,
required: false,
},
})
const emit = defineEmits(['change', 'update:modelValue'])
const toast = useToast()
const showText = ref<any>(props.multi ? [] : '')
const options = ref<any>([])
const cgRpConfigId = ref('')
const code = ref(props.dictCode.split(',')[0])
const labelFiled = ref(props.dictCode.split(',')[1])
const valueFiled = ref(props.dictCode.split(',')[2])
const reportModal = reactive({
show: false,
})
//定义请求url信息
const configUrl = reactive({
getColumns: '/online/cgreport/api/getRpColumns/',
getData: '/online/cgreport/api/getData/',
})
if (!code.value || !valueFiled.value || !labelFiled.value) {
toast.error('popupDict参数未正确配置!')
}
/**
* 监听value数值
*/
watch(
() => props.modelValue,
(val) => {
const callBack = () => {
if (props.multi) {
showText.value = val && val.length > 0 ? val.split(props.spliter) : []
} else {
showText.value = val ?? ''
}
}
if (props.modelValue) {
if (cgRpConfigId.value) {
loadData({ callBack })
} else {
loadColumnsInfo({ callBack })
}
} else {
callBack()
}
},
{ immediate: true },
)
watch(
() => showText.value,
(val) => {
let result
if (props.multi) {
result = val.join(',')
} else {
result = val
}
nextTick(() => {
emit('change', result)
emit('update:modelValue', result)
})
},
)
/**
* 加载列信息
*/
function loadColumnsInfo({ callBack }) {
let url = `${configUrl.getColumns}${code.value}`
http
.get(url)
.then((res: any) => {
if (res.success) {
cgRpConfigId.value = res.result.cgRpConfigId
loadData({ callBack })
}
})
.catch((err) => {
callBack?.()
})
}
function loadData({ callBack }) {
let url = `${configUrl.getData}${unref(cgRpConfigId)}`
http
.get(url, { ['force_' + valueFiled.value]: props.modelValue })
.then((res: any) => {
let data = res.result
if (data.records?.length) {
options.value = data.records.map((item) => {
return { value: item[valueFiled.value], label: item[labelFiled.value] }
})
}
})
.finally(() => {
callBack?.()
})
}
/**
* 传值回调
*/
function callBack(rows) {
const dataOptions: any = []
const dataValue: any = []
let result
rows.forEach((item) => {
dataOptions.push({ value: item[valueFiled.value], label: item[labelFiled.value] })
dataValue.push(item[valueFiled.value])
})
options.value = dataOptions
if (props.multi) {
showText.value = dataValue
result = dataValue.join(props.spliter)
} else {
showText.value = dataValue[0]
result = dataValue[0]
}
nextTick(() => {
emit('change', result)
emit('update:modelValue', result)
})
}
// 清空
const handleClear = () => {
if (!props.disabled) {
showText.value = ''
handleChange([])
}
}
const handleClick = () => {
if (!props.disabled) {
reportModal.show = true
}
}
const handleClose = () => {
reportModal.show = false
}
const handleChange = (data) => {
console.log('选中的值:', data)
callBack(data)
}
</script>
<style lang="scss" scoped>
.inputArea {
position: relative;
.u-icon-close {
position: absolute;
right: 15px;
top: calc(14px + 4px);
color: #585858;
font-size: 15px;
}
&.clear {
:deep(.wd-input__body) {
padding-right: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<view class="ProgressMap">
<view v-if="title.length" class="title">{{ title }}</view>
<view
:class="{
stepBox: true,
active: item.activeStep,
'u-iconfont': true,
'u-icon-clock': !item.activeStep,
'u-icon-success': item.activeStep,
}"
v-for="(item, index) in dataSource"
:key="index"
>
<view :class="{ stepContent: true, active: item.activeStep }">
<template v-for="(inItem, inIndex) in item.data" :key="inIndex">
<view v-if="inItem.label" class="item">
{{ inItem.label }}
</view>
</template>
</view>
<view v-if="item.file" class="file">
<template v-for="(file, inIndex) in item.file" :key="inIndex">
<view :data-url="file.filePath" class="item" @click="handClick(file)">
<view class="u-iconfont u-icon-link"></view>
<view class="text ellipsis">{{ file.fileName }}</view>
</view>
</template>
</view>
</view>
<ImgPreview
v-if="imgPreview.show"
:urls="imgPreview.urls"
@close="() => (imgPreview.show = false)"
></ImgPreview>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { downloadFile } from '@/common/uitls'
import { getFileAccessHttpUrl } from '@/common/uitls'
const props: any = defineProps({
title: {
type: String,
default: '',
},
dataSource: {
type: Array,
default: () => [{ activeStep: false, data: [] }],
},
})
const imgPreview = reactive({
show: false,
urls: [],
})
const handClick = (file) => {
const suffix = file.filePath.split('.').pop()
if (['gif', 'png', 'jpg', 'jpeg'].includes(suffix)) {
imgPreview.urls = [getFileAccessHttpUrl(file.filePath)]
imgPreview.show = true
} else {
downloadFile(file.filePath)
}
}
</script>
<style lang="scss" scoped>
.ProgressMap {
background-color: #fff;
}
.title {
padding: 20upx;
font-weight: bold;
}
.stepBox {
position: relative;
line-height: 1;
padding: 32upx 32upx 32upx 120upx;
color: #aaaaaa;
&.active {
color: #39b54a;
}
&::before {
position: absolute;
left: 38upx;
background-color: #fff;
z-index: 9;
top: 50upx;
border-radius: 50%;
margin: 8upx;
}
&::after {
content: '';
display: block;
position: absolute;
width: 0.5px;
background-color: #ddd;
left: 60upx;
height: 100%;
top: 0;
z-index: 8;
}
.stepContent {
padding: 10upx 50upx 10upx 30upx;
border-radius: 8upx;
background-color: #f0f0f0;
color: #333333;
font-size: 13px;
line-height: 1.6;
&.active {
font-weight: 500;
background-color: #39b54a;
color: #fff;
box-shadow: 0 0 5px #39b54a;
}
}
.file {
padding-top: 10px;
.item {
margin-bottom: 5px;
display: flex;
font-size: 14px;
align-items: center;
color: var(--color-blue);
line-height: 1.2;
.u-iconfont {
font-size: 14px;
margin-right: 5px;
color: #666;
}
}
}
}
</style>

View File

@@ -0,0 +1,68 @@
<template>
<wd-popup v-model="show" position="right" @close="handleClose">
<view class="content">
<wd-text v-if="title" :text="title"></wd-text>
<wd-cell-group border>
<wd-radio-group v-model="checked">
<template v-for="(item, index) in options">
<wd-cell :title="item.title" clickable @click="handleSelected(item)">
<wd-radio :value="item.key"></wd-radio>
</wd-cell>
</template>
</wd-radio-group>
</wd-cell-group>
</view>
</wd-popup>
</template>
<script setup lang="ts">
import { hasRoute, cache } from '@/common/uitls'
import { ref } from 'vue'
const eimt = defineEmits(['change', 'close'])
const show = ref(true)
const props = defineProps(['title', 'data', 'options', 'checked'])
const checked = ref(props.checked)
const handleClose = () => {
show.value = false
setTimeout(() => {
eimt('close')
}, 300)
}
const handleSelected = (item) => {
checked.value = item.key
eimt('change', { option: item, data: props.data })
handleClose()
}
</script>
<style lang="scss" scoped>
.content {
max-width: 200px;
padding: 10px;
.wd-text.is-default {
font-size: 14px;
color: #666;
}
.wd-cell {
padding-left: 0;
--wot-cell-label-color: #444;
--wot-cell-label-fs: 14px;
&.red {
color: red;
--wot-cell-label-color: red;
}
}
.wd-cell-group {
:deep(.wd-cell__wrapper) {
align-items: center;
.wd-cell__right {
flex: none;
width: 24px;
}
.wd-radio {
margin-top: 0;
}
}
}
}
</style>

View File

@@ -0,0 +1,285 @@
<template>
<view class="CategorySelect">
<view class="pickerArea" :class="{ clear: !!showText }" @click.stop="handleClick">
<wd-input
:placeholder="getPlaceholder($attrs)"
v-bind="$attrs"
v-model="showText"
clearable
readonly
></wd-input>
<view v-if="!!showText && !disabled" class="u-iconfont u-icon-close" @click.stop="handleClear"></view>
</view>
<wd-popup v-if="popupShow" position="bottom" v-model="popupShow">
<view class="content">
<view class="operation">
<view class="cancel text-gray-5" @click.stop="cancel">取消</view>
<view class="confrim" @click.stop="confirm">确定</view>
</view>
<scroll-view class="flex-1" scroll-y>
<DaTree
:data="treeData"
:labelField="labelKey"
:valueField="rowKey"
loadMode
:showCheckbox="multiple"
:showRadioIcon="false"
:checkStrictly="true"
:defaultCheckedKeys="defaultCheckedKeys"
:loadApi="asyncLoadTreeData"
@change="handleTreeChange"
></DaTree>
</scroll-view>
</view>
</wd-popup>
</view>
</template>
<script setup lang="ts">
import { ref, watch, useAttrs } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import DaTree from '@/uni_modules/da-tree/index.vue'
import { isArray } from '@/utils/is'
import { getPlaceholder } from '@/common/uitls'
defineOptions({
name: 'SelectDept',
})
const props = defineProps({
modelValue: {
type: [Array, String],
},
// 是否支持多选
multiple: {
type: Boolean,
default: true,
},
rowKey: {
type: String,
default: 'key',
},
labelKey: {
type: String,
default: 'title',
},
disabled: {
type: Boolean,
required: false,
default: false,
},
})
const emit = defineEmits(['change', 'update:modelValue'])
const toast = useToast()
const api = {
loadDictItem: '/sys/category/loadDictItem/',
queryDepartTreeSync: '/sys/sysDepart/queryDepartTreeSync',
}
const showText = ref('')
const popupShow = ref(false)
const treeData = ref<any[]>([])
const treeValue = ref([])
const defaultCheckedKeys: any = ref(props.multiple ? [] : '')
const handleClick = () => {
if (!props.disabled) {
popupShow.value = true
}
}
const cancel = () => {
popupShow.value = false
}
const confirm = () => {
const titles = treeValue.value.map((item) => item.title)
const keys = treeValue.value.map((item) => item.key).join(',')
showText.value = titles.join(',')
popupShow.value = false
emit('update:modelValue', keys)
emit('change', keys)
}
const handleTreeChange = (value, record) => {
const { originItem, checkedStatus } = record
if (checkedStatus) {
// 选中
if (props.multiple) {
treeValue.value.push({ key: originItem[props.rowKey], title: originItem[props.labelKey] })
} else {
treeValue.value = [{ key: originItem[props.rowKey], title: originItem[props.labelKey] }]
}
} else {
// 取消
if (props.multiple) {
const findIndex = treeValue.value.findIndex(
(item) => item[props.rowKey] == originItem[props.rowKey],
)
if (findIndex != -1) {
treeValue.value.splice(findIndex, 1)
}
} else {
treeValue.value = []
}
}
}
const transformField = (result) => {
for (let i of result) {
i.value = i.key
if (i.isLeaf == false) {
i.leaf = false
} else if (i.isLeaf == true) {
i.leaf = true
}
}
}
// 异步加载
const asyncLoadTreeData = ({ originItem }) => {
return new Promise<void>((resolve) => {
let param = {
pid: originItem.key,
primaryKey: props.rowKey,
}
http
.get(api.queryDepartTreeSync, param)
.then((res: any) => {
if (res.success) {
const { result } = res
transformField(result)
resolve(result)
} else {
resolve(null)
}
})
.catch((err) => resolve(null))
})
}
// 加载根节点
function loadRoot() {
let param = {
primaryKey: props.rowKey,
}
http
.get(api.queryDepartTreeSync, param)
.then((res: any) => {
if (res.success) {
const { result } = res
if (result && result.length > 0) {
transformField(result)
treeData.value = result
}
} else {
console.error('部门组件加载根节点数据失败~')
}
})
.catch((err) => {
console.error('部门组件加载根节点数据失败~')
})
}
// 翻译input内的值
function loadItemByCode() {
let value = props.modelValue
if(value){
console.log('部门组件翻译props.modelValue', props.modelValue)
if (isArray(props.modelValue)) {
// @ts-ignore
value = value.join(',')
}
if (value === treeData.value.map((item) => item.key).join(',')) {
// 说明是刚选完,内部已有翻译。不需要再请求
return
}
value = (value as string).trim()
if (value) {
http
.get(api.queryDepartTreeSync, { ids: value })
.then((res: any) => {
if (res.success) {
const { result = [] } = res
showText.value = result.map((item) => item[props.labelKey]).join(',')
if (props.multiple) {
defaultCheckedKeys.value = typeof value === 'string' ? value.split(',') : [value]
} else {
defaultCheckedKeys.value = value
}
} else {
}
})
.catch((err) => {})
}
}
}
// 清空
const handleClear = () => {
showText.value = ''
treeValue.value = []
confirm()
}
watch(
() => props.modelValue,
() => {
loadItemByCode()
},
{ deep: true, immediate: true },
)
loadRoot()
</script>
<style lang="scss" scoped>
:deep(.wd-popup-wrapper) {
.wd-popup {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
}
.content {
height: 50vh;
width: 100vw;
display: flex;
flex-direction: column;
.operation {
display: flex;
justify-content: space-between;
line-height: 40px;
padding: 0 5px;
position: relative;
&::before {
content: ' ';
position: absolute;
bottom: 0;
left: 8px;
right: 8px;
height: 1px;
background-color: #e5e5e5;
}
.cancel,
.confrim {
font-size: 15px;
height: 40px;
min-width: 40px;
text-align: center;
}
.confrim {
color: var(--wot-color-theme);
}
}
:deep(.da-tree) {
.da-tree-item__checkbox {
// display: none;
}
}
}
.pickerArea {
position: relative;
.u-icon-close {
position: absolute;
right: 15px;
top: calc(14px + 4px);
color: #585858;
font-size: 15px;
}
&.clear {
:deep(.wd-input__body) {
padding-right: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,268 @@
<template>
<view class="SelectUser">
<template v-if="showType === 'form'">
<view class="inputArea" :class="{ clear: !!showText }" @click.stop="handleClick">
<wd-input
:placeholder="getPlaceholder($attrs)"
v-bind="$attrs"
readonly
v-model="showText"
></wd-input>
<view
v-if="!!showText && !disabled"
class="u-iconfont u-icon-close"
@click.stop="handleClear"
></view>
</view>
</template>
<template v-else>
<view class="list">
<template v-for="(item, index) in selectedUser" :key="item.id">
<view
v-if="maxShowUser === -1 || index < maxShowUser"
class="user"
@click="handleRemove(index)"
>
<view v-if="isDelUser" class="u-iconfont u-icon-close"></view>
<wd-img
v-if="getAvatar(item.avatar)"
radius="50%"
width="40px"
height="40px"
:src="getAvatar(item.avatar)"
></wd-img>
<view class="name">{{ item.realname }}</view>
</view>
</template>
<view v-if="isAddUser" class="u-iconfont u-icon-newAdd" @click="handleClick"></view>
</view>
</template>
<SelectUserModal
v-if="modalShow"
:selected="modelValue"
:defaultSelectedValue="defaultValue"
:selectedUser="selectedUser"
:modalTitle="modalTitle"
:maxSelectCount="maxSelectCount"
:multi="!isRadioSelection"
:rowKey="rowKey"
:readonlyUser="readonlyUser"
@change="handleChange"
@close="() => (modalShow = false)"
></SelectUserModal>
</view>
</template>
<script setup lang="ts">
import { ref, watch, useAttrs } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import DaTree from '@/uni_modules/da-tree/index.vue'
import { isArray, isString, isNumber } from '@/utils/is'
import SelectUserModal from './components/SelectUserModal.vue'
import { getPlaceholder, getFileAccessHttpUrl } from '@/common/uitls'
import defaultAvatar from '@/static/default-avatar.png'
defineOptions({
name: 'SelectUser',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
modelValue: {
type: [Array, String],
default: '',
},
defaultValue: {
type: [Array, String],
default: '',
},
labelKey: {
type: String,
default: 'realname',
},
rowKey: {
type: String,
default: 'username',
},
isRadioSelection: {
type: Boolean,
default: false,
},
modalTitle: {
type: String,
default: '选择用户',
},
maxSelectCount: {
type: Number,
},
showType: {
type: String,
default: 'form', // form、card
},
// showType为card时有效
isDelUser: {
type: Boolean,
default: true,
},
// showType为card时有效
isAddUser: {
type: Boolean,
default: true,
},
// showType为card时有效
maxShowUser: {
type: Number,
default: -1,
},
// 只读用户(添加的用户不可取消)
readonlyUser: {
type: Array,
default: [],
},
disabled: {
type: Boolean,
required: false,
default: false,
},
})
const emit = defineEmits(['update:modelValue', 'change', 'getSelectdAllData'])
const api = {
list: '/sys/user/list',
}
const showText = ref('')
const modalShow = ref(false)
const selectedUser = ref([])
const getAvatar = (url) => {
let result = getFileAccessHttpUrl(url)
if (result.length) {
return result
} else {
return defaultAvatar
}
}
const handleRemove = (index?) => {
if (props.isDelUser) {
if (isNumber(index)) {
selectedUser.value.splice(index, 1)
} else {
selectedUser.value.pop()
}
handleChange(selectedUser.value)
}
}
// 翻译
const transform = () => {
let value = props.modelValue
let len
if (isArray(value) || isString(value)) {
if (isArray(value)) {
len = value.length
value = value.join(',')
} else {
len = value.split(',').length
}
value = value.trim()
if (value) {
const params = { isMultiTranslate: true, pageSize: len, [props.rowKey]: value }
http.get(api.list, params).then((res: any) => {
if (res.success) {
const records = res.result?.records ?? []
showText.value = records.map((item) => item[props.labelKey]).join(',')
selectedUser.value = records
} else {
console.log('翻译失败~')
}
})
}
} else {
showText.value = ''
}
}
// 打开popup
const handleClick = () => {
console.log('handleClick', !props.disabled)
console.log('handleClick', props)
if (!props.disabled) {
modalShow.value = true
}
}
// 清空
const handleClear = () => {
showText.value = ''
handleChange([])
}
const handleChange = (data) => {
const rowkey = data.map((item) => item[props.rowKey]).join(',')
const labelKey = data.map((item) => item[props.labelKey]).join(',')
showText.value = labelKey
selectedUser.value = data
emit('update:modelValue', rowkey)
emit('change', rowkey)
emit('getSelectdAllData', data)
}
watch(
() => props.modelValue,
() => {
transform()
},
{ immediate: true },
)
</script>
<style lang="scss" scoped>
.list {
display: flex;
align-items: center;
flex-wrap: wrap;
color: #666;
.user {
padding: 7px;
text-align: center;
position: relative;
.u-iconfont {
font-size: 14px;
position: absolute;
right: 4px;
top: 7px;
z-index: 1;
/* background: white; */
width: 14px;
height: 14px;
border-radius: 50%;
line-height: 1;
}
.name {
font-size: 12px;
}
}
.u-iconfont {
font-size: 22px;
}
.u-icon-newAdd {
margin-left: 8px;
}
.u-icon-remove {
color: var(--color-red);
}
}
.inputArea {
position: relative;
.u-icon-close {
position: absolute;
right: 15px;
top: calc(14px + 4px);
color: #585858;
font-size: 15px;
}
&.clear {
:deep(.wd-input__body) {
padding-right: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,338 @@
<template>
<wd-popup position="bottom" v-model="show">
<PageLayout
:navTitle="modalTitle"
type="popup"
navRightText="确定"
@navRight="handleConfirm"
@navBack="handleCancel"
>
<view class="wrap">
<z-paging
ref="paging"
:fixed="false"
v-model="dataList"
@query="queryList"
:default-page-size="15"
>
<template #top>
<wd-search
hide-cancel
:placeholder="search.placeholder"
v-model="search.keyword"
@search="handleSearch"
@clear="handleClear"
/>
</template>
<template v-if="multi">
<wd-checkbox-group shape="square" v-model="checkedValue">
<template v-for="(item, index) in dataList" :key="index">
<view class="list" @click="hanldeCheck(index, item[rowKey])">
<view class="left text-gray-5">
<wd-img
custom-class="avatar"
radius="50%"
height="40"
width="40"
:src="getAvatar(item.avatar)"
></wd-img>
<view class="subContent">
<text>账号{{ item.username }}</text>
<text>姓名{{ item.realname }}</text>
</view>
</view>
<view class="right" @click.stop>
<wd-checkbox
ref="checkboxRef"
:disabled="readonlyUser.includes(item[rowKey])"
:modelValue="item[rowKey]"
></wd-checkbox>
</view>
</view>
</template>
</wd-checkbox-group>
</template>
<template v-else>
<wd-radio-group shape="dot" v-model="checkedValue">
<template v-for="(item, index) in dataList" :key="index">
<wd-cell>
<view class="list" @click="hanldeCheck(index, item[rowKey])">
<view class="left text-gray-5">
<wd-img
custom-class="avatar"
radius="50%"
height="40"
width="40"
:src="getAvatar(item.avatar)"
></wd-img>
<view class="subContent">
<text>账号{{ item.username }}</text>
<text>姓名{{ item.realname }}</text>
</view>
</view>
<view class="right" @click.stop>
<wd-radio :value="item[rowKey]"></wd-radio>
</view>
</view>
</wd-cell>
</template>
</wd-radio-group>
</template>
</z-paging>
</view>
</PageLayout>
</wd-popup>
</template>
<script setup lang="ts">
import { ref, reactive, nextTick } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import { isArray, isString } from '@/utils/is'
import { cache, getFileAccessHttpUrl } from '@/common/uitls'
import defaultAvatar from '@/static/default-avatar.png'
defineOptions({
name: 'SelectUserModal',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
multi: {
type: Boolean,
default: true,
},
modalTitle: {
type: String,
default: '选择用户',
},
maxSelectCount: {
type: Number,
},
// 这是用户id
selected: {
type: [Array, String],
default: '',
},
// 这是用户id只是默认勾选在弹窗中勾选
defaultSelectedValue: {
type: [Array, String],
default: '',
},
// 这是用户全数据项包含idusername、realname
selectedUser: {
type: Array,
default: () => [],
},
rowKey: {
type: String,
default: 'username',
},
// 只读用户
readonlyUser: {
type: Array,
default: [],
},
})
const emit = defineEmits(['change', 'close'])
const toast = useToast()
const show = ref(true)
const api = {
selectUserList: '/sys/user/selectUserList',
userlist: '/sys/user/list',
}
const paging = ref(null)
const dataList = ref([])
const checkedValue: any = ref(props.multi ? [] : '')
const checkboxRef = ref(null)
const search = reactive({
keyword: '',
placeholder: '输入姓名可搜索',
field: 'realname',
})
const handleClose = () => {
setTimeout(() => {
emit('close')
}, 400)
}
const handleConfirm = () => {
if (checkedValue.value.length == 0) {
toast.warning('还没选择用户~')
return
}
const result = []
let value = checkedValue.value
if (!Array.isArray(checkedValue.value)) {
value = [checkedValue.value]
}
value.forEach((rowKey, index) => {
const findIndex = dataList.value.findIndex((item) => item[props.rowKey] === rowKey)
if (findIndex == -1) {
// 传进来选中的用户可能在第二页(还没加载进来)
const index = props.selectedUser.findIndex((item) => item[props.rowKey] === rowKey)
if (index != -1) {
result.push(props.selectedUser[index])
} else {
// 传进来defaultSelectedValue的用户可能在第二页还没加载进来
if (isArray(props.defaultSelectedValue)) {
const index = props.defaultSelectedValue.findIndex((item) => item === rowKey)
index != -1 && result.push({ [props.rowKey]: props.defaultSelectedValue[index] })
} else {
props.defaultSelectedValue == rowKey && result.push({ [props.rowKey]: rowKey })
}
}
} else {
result.push(dataList.value[findIndex])
}
})
show.value = false
emit('change', result)
handleClose()
}
const handleCancel = () => {
show.value = false
handleClose()
console.log('取消了~')
}
// 搜索
function handleSearch() {
paging.value.reload()
}
// 清除搜索条件
function handleClear() {
search.keyword = ''
handleSearch()
}
const hanldeCheck = (index, username) => {
if (props.multi) {
if (Array.isArray(checkboxRef.value)) {
checkboxRef.value[index].toggle()
nextTick(() => {
if (props.maxSelectCount) {
if (checkedValue.value.length > props.maxSelectCount) {
toast.warning(`最多可选择${props.maxSelectCount}个用户`)
// 超过个数取消
checkboxRef.value[index].toggle()
}
}
})
}
} else {
checkedValue.value = username
}
}
const getAvatar = (url) => {
let result = getFileAccessHttpUrl(url)
if (result.length) {
return result
} else {
return defaultAvatar
}
}
const queryList = (pageNo, pageSize) => {
const pararms = { pageNo, pageSize, column: 'createTime', order: 'desc' }
if (search.keyword) {
pararms[search.field] = `*${search.keyword}*`
}
http
.get(`${api.userlist}`, pararms)
.then((res: any) => {
if (res.success && res.result.records) {
paging.value.complete(res.result.records ?? [])
} else {
paging.value.complete(false)
}
})
.catch((err) => {})
}
const init = () => {
if (props.selected.length) {
if (props.multi) {
if (isArray(props.selected)) {
checkedValue.value = props.selected
} else if (isString(props.selected)) {
checkedValue.value = props.selected.split(',')
}
} else {
if (isString(props.selected)) {
checkedValue.value = props.selected
} else if (isArray(props.selected)) {
checkedValue.value = props.selected.join(',')
}
}
} else {
if (props.defaultSelectedValue.length) {
if (props.multi) {
if (isArray(props.defaultSelectedValue)) {
checkedValue.value = props.defaultSelectedValue
} else if (isString(props.defaultSelectedValue)) {
checkedValue.value = props.defaultSelectedValue.split(',')
}
} else {
if (isString(props.defaultSelectedValue)) {
checkedValue.value = props.defaultSelectedValue
} else if (isArray(props.defaultSelectedValue)) {
checkedValue.value = props.defaultSelectedValue.join(',')
}
}
}
}
}
init()
</script>
<style lang="scss" scoped>
:deep(.wd-cell) {
--wot-color-white: tranparent;
--wot-cell-padding: 0;
.wd-cell__wrapper {
--wot-cell-wrapper-padding: 0;
}
.wd-cell__left {
display: none;
}
}
:deep(.wd-checkbox-group) {
--wot-checkbox-bg: tranparent;
}
:deep(.wd-radio-group) {
--wot-radio-bg: tranparent;
}
.list {
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
padding: 16px;
margin-top: 16px;
.left {
display: flex;
align-items: center;
text-align: left;
:deep(.avatar) {
margin-right: 8px;
background-color: #e9e9e9;
}
.subContent {
display: flex;
flex-direction: column;
}
}
.right {
:deep(.wd-checkbox) {
margin-bottom: 0;
}
}
}
.wrap {
height: 100%;
}
:deep(.wd-popup-wrapper) {
.wd-popup {
top: 100px;
}
}
</style>

View File

@@ -0,0 +1,159 @@
<template>
<view class="SelectUser">
<view class="inputArea" :class="{ clear: !!showText }" @click.stop="handleClick">
<wd-input
:placeholder="getPlaceholder($attrs)"
v-bind="$attrs"
readonly
v-model="showText"
></wd-input>
<view
v-if="!!showText && !disabled"
class="u-iconfont u-icon-close"
@click.stop="handleClear"
></view>
</view>
<SelectUserModal
v-if="modalShow"
:selected="modelValue"
:selectedUser="selectedUser"
:modalTitle="modalTitle"
:rowKey="rowKey"
@change="handleChange"
@close="() => (modalShow = false)"
></SelectUserModal>
</view>
</template>
<script setup lang="ts">
import { ref, watch, useAttrs } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import DaTree from '@/uni_modules/da-tree/index.vue'
import { isArray, isString, isNumber } from '@/utils/is'
import SelectUserModal from './components/SelectUserModal.vue'
import { getPlaceholder, getFileAccessHttpUrl } from '@/common/uitls'
import defaultAvatar from '@/static/default-avatar.png'
defineOptions({
name: 'SelectUserByDepart',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
modelValue: {
type: [Array, String],
default: '',
},
labelKey: {
type: String,
default: 'realname',
},
rowKey: {
type: String,
default: 'username',
},
modalTitle: {
type: String,
default: '选择用户',
},
disabled: {
type: Boolean,
required: false,
default: false,
},
})
const emit = defineEmits(['update:modelValue', 'change'])
const api = {
list: '/sys/user/list',
}
const showText = ref('')
const modalShow = ref(false)
const selectedUser = ref([])
// 翻译
const transform = () => {
let value = props.modelValue
let len
if (isArray(value) || isString(value)) {
if (isArray(value)) {
len = value.length
value = value.join(',')
} else {
len = value.split(',').length
}
value = value.trim()
if (value) {
// 如果value的值在selectedUser中存在则不请求翻译
let isNotRequestTransform = false
isNotRequestTransform = value
.split(',')
.every((value) => !!selectedUser.value.find((item) => item[props.rowKey] === value))
if (isNotRequestTransform) {
return
}
const params = { isMultiTranslate: true, pageSize: len, [props.rowKey]: value }
http.get(api.list, params).then((res: any) => {
if (res.success) {
const records = res.result?.records ?? []
showText.value = records.map((item) => item[props.labelKey]).join(',')
selectedUser.value = records
} else {
console.log('翻译失败~')
}
})
}
} else {
showText.value = ''
}
}
// 打开popup
const handleClick = () => {
console.log('handleClick', !props.disabled)
console.log('handleClick', props)
if (!props.disabled) {
modalShow.value = true
}
}
// 清空
const handleClear = () => {
showText.value = ''
handleChange([])
}
const handleChange = (data) => {
const rowkey = data.map((item) => item[props.rowKey]).join(',')
const labelKey = data.map((item) => item[props.labelKey]).join(',')
showText.value = labelKey
selectedUser.value = data
emit('update:modelValue', rowkey)
emit('change', rowkey)
}
watch(
() => props.modelValue,
() => {
transform()
},
{ immediate: true },
)
</script>
<style lang="scss" scoped>
.inputArea {
position: relative;
.u-icon-close {
position: absolute;
right: 15px;
top: calc(14px + 4px);
color: #585858;
font-size: 15px;
}
&.clear {
:deep(.wd-input__body) {
padding-right: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,841 @@
<template>
<wd-popup position="bottom" v-model="show">
<PageLayout :navTitle="modalTitle" type="popup" @navBack="handleCancel">
<view class="wrap">
<view class="wrap-content">
<wd-search
hide-cancel
placeholder="请输入姓名/部门"
v-model="searchText"
@search="handleSearch"
@clear="handleSearch"
/>
<view class="main-content">
<scroll-view scroll-y>
<template v-if="searchResult.depart.length || searchResult.user.length">
<view class="search-result">
<template v-if="searchResult.user.length">
<view class="search-user solid-top">
<text class="search-user-title">人员</text>
<template v-for="item in searchResult.user" :key="item.id">
<view
class="search-user-item solid-top"
@click="handleSearchUserCheck(item)"
>
<view @click.stop>
<wd-checkbox
shape="square"
v-model="item.checked"
@change="($event) => handleSearchUserCheck(item, $event)"
/>
</view>
<view class="right">
<view class="search-user-item-circle">
<wd-img
width="36"
height="36px"
v-if="item.avatar"
:src="getFileAccessHttpUrl(item.avatar)"
/>
</view>
<view class="search-user-item-info">
<view class="search-user-item-name">{{ item.realname }}</view>
<view class="search-user-item-org">{{ item.orgCodeTxt }}</view>
</view>
</view>
</view>
</template>
</view>
</template>
<template v-if="searchResult.depart.length">
<view class="search-depart solid-top">
<text class="search-depart-title">部门</text>
<template v-for="item in searchResult.depart" :key="item.id">
<view
class="search-depart-item solid-top"
@click="handleSearchDepartClick(item)"
>
<view @click.stop>
<wd-checkbox
shape="square"
v-model="item.checked"
@change="($event) => handleSearchDepartCheck($event, item)"
/>
</view>
<view class="search-depart-item-name">{{ item.departName }}</view>
<wd-icon name="arrow-right" size="16px"></wd-icon>
</view>
</template>
</view>
</template>
</view>
</template>
<template v-else>
<view v-if="breadcrumb.length" class="breadcrumb-wrap solid-top">
<Breadcrumb
separator="/"
:items="[
{ title: '首页', icon: 'home' },
...breadcrumb.map((item) => ({ title: item.departName, ...item })),
]"
@click="(item, index) => handleBreadcrumbClick(index === 0 ? undefined : item)"
>
<template #item-0="{ item }">
<wd-icon name="home" size="18px"></wd-icon>
</template>
</Breadcrumb>
</view>
<div v-if="currentDepartUsers.length">
<!-- 当前部门用户树 -->
<div class="depart-users-tree solid-top">
<div v-if="!currentDepartTree.length" class="allChecked">
<wd-checkbox
shape="square"
v-model="currentDepartAllUsers"
@change="handleAllUsers"
>
全选
</wd-checkbox>
</div>
<template v-for="item in currentDepartUsers" :key="item.id">
<div
class="depart-users-tree-item solid-top"
@click="handleDepartUsersTreeCheck(item)"
>
<view @click.stop>
<wd-checkbox
shape="square"
v-model="item.checked"
@change="($event) => handleDepartUsersTreeCheck(item, $event)"
/>
</view>
<div class="right">
<div class="depart-users-tree-item-circle">
<wd-img
width="36"
height="36px"
v-if="item.avatar"
:src="getFileAccessHttpUrl(item.avatar)"
/>
</div>
<div class="depart-users-tree-item-name">{{ item.realname }}</div>
</div>
</div>
</template>
</div>
</div>
<!-- 部门树 -->
<div v-if="currentDepartTree.length" class="depart-tree">
<template v-for="item in currentDepartTree" :key="item.id">
<div class="depart-tree-item solid-top" @click="handleDepartTreeClick(item)">
<view @click.stop>
<wd-checkbox
shape="square"
v-model="item.checked"
@change="($event) => handleDepartTreeCheck($event, item)"
/>
</view>
<div class="depart-tree-item-name">{{ item.departName }}</div>
<wd-icon name="arrow-right" size="16px"></wd-icon>
</div>
</template>
</div>
<div
v-if="currentDepartTree.length === 0 && currentDepartUsers.length === 0"
class="no-data"
>
<wd-status-tip image="content" tip="暂无内容" />
</div>
</template>
</scroll-view>
<view v-if="showSelectedUser" class="selected-user">
<SelectedUser :selectedUsers="selectedUsers" @del="handleDelUser"></SelectedUser>
</view>
</view>
</view>
<view class="wrap-footer">
<view class="text" @click="() => (showSelectedUser = !showSelectedUser)">
<view>已选</view>
<view class="num">{{ selectedUsers.length }}</view>
<view></view>
<text class="tip">(查看选中用户)</text>
</view>
<wd-button type="primary" @click="handleConfirm">确定</wd-button>
</view>
</view>
</PageLayout>
<wd-toast :selector="selector"></wd-toast>
</wd-popup>
</template>
<script setup lang="ts">
import { ref, reactive, nextTick } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import { isArray, isString } from '@/utils/is'
import { cache, getFileAccessHttpUrl, uuid } from '@/common/uitls'
import defaultAvatar from '@/static/default-avatar.png'
import SelectedUser from './SelectedUser.vue'
defineOptions({
name: 'SelectUserModal',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
multi: {
type: Boolean,
default: true,
},
modalTitle: {
type: String,
default: '选择用户',
},
maxSelectCount: {
type: Number,
},
// 这是用户全数据项包含idusername、realname
selectedUser: {
type: Array,
default: () => [],
},
rowKey: {
type: String,
default: 'username',
},
})
const api = {
selectUserList: '/sys/user/selectUserList',
userlist: '/sys/user/list',
queryTreeList: '/sys/sysDepart/queryTreeList',
getTableList: '/sys/user/queryUserComponentData',
}
const emit = defineEmits(['change', 'close'])
const selector = uuid()
const toast = useToast(selector)
const show = ref(true)
// 搜索文本
const searchText = ref('')
const breadcrumb = ref<any[]>([])
// 部门树(整颗树)
const departTree = ref([])
// 当前部门树
const currentDepartTree = ref<any[]>([])
// 选中的部门节点
const checkedDepartIds = ref<string[]>([])
// 当前部门用户
const currentDepartUsers = ref([])
// 已选用户
const selectedUsers = ref<any[]>([])
// 全选
const currentDepartAllUsers = ref(false)
// 搜索结构
const searchResult: any = reactive({
depart: [],
user: [],
})
// 映射部门和人员的关系
const cacheDepartUser = {}
// 是否显示已选用户
const showSelectedUser = ref(false)
const handleClose = () => {
setTimeout(() => {
emit('close')
}, 400)
}
const handleConfirm = () => {
if (selectedUsers.value.length == 0) {
toast.warning('还没选择用户~')
return
}
show.value = false
let result = []
let value = selectedUsers.value
if (!Array.isArray(selectedUsers.value)) {
value = [selectedUsers.value]
}
result = value
emit('change', result)
handleClose()
}
const handleCancel = () => {
show.value = false
handleClose()
console.log('取消了~')
}
const getAvatar = (url) => {
let result = getFileAccessHttpUrl(url)
if (result.length) {
return result
} else {
return defaultAvatar
}
}
// 搜索人员/部门
const handleSearch = () => {
if (searchText.value) {
http
.get(`/sys/user/listAll`, {
column: 'createTime',
order: 'desc',
pageNo: 1,
pageSize: 100,
realname: `*${searchText.value}*`,
})
.then((res: any) => {
if (res.success) {
res.result.records?.forEach((item) => {
const findItem = selectedUsers.value.find((user) => user.id == item.id)
if (findItem) {
// 能在右侧找到说明选中了,左侧同样需要选中。
item.checked = true
} else {
item.checked = false
}
})
searchResult.user = res.result.records ?? []
} else {
toast.warning(res.message)
}
})
searchResult.depart = getDepartByName(searchText.value) ?? []
showSelectedUser.value = false
} else {
searchResult.user = []
searchResult.depart = []
}
}
// 面包屑
const handleBreadcrumbClick = (item?) => {
// 先清空
currentDepartUsers.value = []
if (item) {
const findIndex = breadcrumb.value.findIndex((o) => o.id === item.id)
if (findIndex != -1) {
breadcrumb.value = breadcrumb.value.filter((item, index) => {
console.log(item)
return index <= findIndex
})
}
const data = getDepartTreeNodeById(item.id, departTree.value)
currentDepartTree.value = data.children
} else {
// 根节点
currentDepartTree.value = departTree.value
breadcrumb.value = []
}
}
// 点击部门树复选框触发
const handleDepartTreeCheck = ({ value }, item) => {
const target = { checked: value }
if (target.checked) {
// 选中
getUsersByDeptId(item['id']).then((users) => {
addUsers(users)
})
checkedDepartIds.value.push((item as any).id)
// 检查父节点下所有子节点是否选中
const parentItem = getDepartTreeParentById(item.id)
if (parentItem?.children) {
const isChildAllChecked = parentItem.children.every((item) => item.checked)
if (isChildAllChecked) {
parentItem.checked = true
} else {
parentItem.checked = false
}
}
} else {
// 取消选中
const findIndex = checkedDepartIds.value.findIndex((o: any) => o.id === item.id)
if (findIndex != -1) {
checkedDepartIds.value.splice(findIndex, 1)
}
// 如果父节点是选中,则需要取消
const parentItem = getDepartTreeParentById(item.id)
if (parentItem) {
parentItem.checked = false
}
getUsersByDeptId(item['id']).then((users) => {
users.forEach((item) => {
const findIndex = selectedUsers.value.findIndex((user) => user.id === item.id)
if (findIndex != -1) {
selectedUsers.value.splice(findIndex, 1)
}
})
})
}
}
// 点击部门树节点触发
const handleDepartTreeClick = (item) => {
breadcrumb.value = [...breadcrumb.value, item]
if (item.children) {
// 有子节点,则显示部门
if (item.checked) {
// 父节点勾选,则子节点全部勾选
item.children.forEach((item) => {
item.checked = true
})
}
currentDepartTree.value = item.children
http
.get('/sys/sysDepart/getUsersByDepartId', {
id: item['id'],
})
.then((res: any) => {
const result = res.result ?? []
if (item.checked) {
// 父节点勾选,则默认勾选
result.forEach((item) => {
item.checked = true
})
}
currentDepartUsers.value = result
})
} else {
// 没有子节点,则显示用户
currentDepartTree.value = []
getTableList({
departId: item['id'],
}).then((res: any) => {
if (res.success) {
if (res?.result.records) {
let checked = true
res.result.records.forEach((item) => {
const findItem = selectedUsers.value.find((user) => user.id == item.id)
if (findItem) {
// 能在右侧找到说明选中了,左侧同样需要选中。
item.checked = true
} else {
item.checked = false
checked = false
}
})
currentDepartAllUsers.value = checked
currentDepartUsers.value = res.result.records
} else {
toast.warning(res.message)
}
}
})
}
}
// 点击部门用户树复选框触发
const handleDepartUsersTreeCheck = (item, e?) => {
if (e) {
// 点击复选框时
item.checked = e.value
} else {
// 点击整条数据时
item.checked = !item.checked
}
if (item.checked) {
addUsers(item)
} else {
selectedUsers.value = selectedUsers.value.filter((user) => user.id !== item.id)
}
if (item.checked == false) {
// 有一个是false则全选false
currentDepartAllUsers.value = false
}
}
// 全选
const handleAllUsers = ({ target }) => {
const { checked } = target
if (checked) {
currentDepartUsers.value.forEach((item: any) => (item.checked = true))
addUsers(currentDepartUsers.value)
} else {
currentDepartUsers.value.forEach((item: any) => (item.checked = false))
selectedUsers.value = selectedUsers.value.filter((user) => {
const userId = user.id
const findItem = currentDepartUsers.value.find((item: any) => item.id === userId)
if (findItem) {
return false
} else {
return true
}
})
}
}
// 删除人员
const handleDelUser = (item) => {
const findIndex = selectedUsers.value.findIndex((user) => user.id === item.id)
if (findIndex != -1) {
selectedUsers.value.splice(findIndex, 1)
}
const findItem: any = currentDepartUsers.value.find((user: any) => user.id === item.id)
if (findItem) {
findItem.checked = false
currentDepartAllUsers.value = false
}
}
// 点击搜索用户复选框
const handleSearchUserCheck = (item, e?) => {
if (!e) {
item.checked = !item.checked
}
if (item.checked) {
addUsers(item)
} else {
selectedUsers.value = selectedUsers.value.filter((user) => user.id !== item.id)
}
}
// 点击搜索部门复选框
const handleSearchDepartCheck = (e, item) => {
handleDepartTreeCheck(e, item)
}
// 点击搜索部门
const handleSearchDepartClick = (item) => {
searchResult.depart = []
searchResult.user = []
breadcrumb.value = getPathToNodeById(item.id)
handleDepartTreeClick(item)
}
// 添加人员到右侧
const addUsers = (users) => {
let newUsers: any = []
if (isArray(users)) {
// selectedUsers里面没有才添加防止重复
newUsers = users.filter((user: any) => !selectedUsers.value.find((item) => item.id === user.id))
} else {
if (!selectedUsers.value.find((user) => user.id === users.id)) {
// selectedUsers里面没有才添加防止重复
newUsers = [users]
}
}
selectedUsers.value = [...selectedUsers.value, ...newUsers]
const result = currentDepartUsers.value.every((item: any) => !!item.checked)
currentDepartAllUsers.value = result
}
// 解析参数
const parseParams = (params) => {
return params
}
const getQueryTreeList = (params?) => {
params = parseParams(params)
queryTreeList({ ...params }).then((res: any) => {
if (res.success) {
departTree.value = res.result
currentDepartTree.value = res.result
} else {
toast.warning(res.message)
}
})
}
// 根据部门id获取用户
const getTableList = (params) => {
params = parseParams(params)
return getTableListOrigin({ ...params })
}
const getUsersByDeptId = (id) => {
return new Promise<any[]>((resolve) => {
if (cacheDepartUser[id]) {
resolve(cacheDepartUser[id])
} else {
getTableList({
departId: id,
}).then((res: any) => {
if (res.success) {
cacheDepartUser[id] = res?.result?.records ?? []
if (res?.result?.records?.length) {
resolve(res.result.records ?? [])
}
} else {
toast.warning(res.message)
}
})
}
})
}
// 根据id获取根节点到当前节点路径
const getPathToNodeById = (id: string, tree = departTree.value, path = []): any[] => {
for (const node of tree) {
if ((node as any).id === id) {
return [...path]
}
if ((node as any).children) {
const foundPath = getPathToNodeById(id, (node as any).children, [...path, node])
if (foundPath.length) {
return foundPath
}
}
}
return []
}
// 根据id获取部门树父节点数据
const getDepartTreeParentById = (id: string, tree = departTree.value, parent = null): any => {
for (const node of tree) {
if ((node as any).id === id) {
return parent
}
if ((node as any).children) {
const found = getDepartTreeParentById(id, (node as any).children, node)
if (found) {
return found
}
}
}
return null
}
// 通过名称搜索部门支持模糊
const getDepartByName = (name: string, tree = departTree.value): any[] => {
const result: any[] = []
const search = (nodes: any[]) => {
for (const node of nodes) {
if (node.departName?.toLowerCase().includes(name.toLowerCase())) {
result.push(node)
}
if (node.children?.length) {
search(node.children)
}
}
}
search(tree)
return result
}
// 根据id获取部门树当前节点数据
const getDepartTreeNodeById = (id: string, tree = departTree.value): any => {
for (const node of tree) {
if ((node as any).id === id) {
return node
}
if ((node as any).children) {
const found = getDepartTreeNodeById(id, (node as any).children)
if (found) {
return found
}
}
}
return null
}
// 获取部门树列表
const queryTreeList = (params = {}) => {
return http.get(api.queryTreeList, { ...params })
}
// 根据部门id获取用户列表包含子孙部门
const getTableListOrigin = (params = {}) => {
return http.get(api.getTableList, { ...params })
}
// 初始化
const init = () => {
if (props.selectedUser.length) {
// 编辑时,传进来已选中的数据
selectedUsers.value = props.selectedUser
}
getQueryTreeList()
}
init()
</script>
<style lang="scss" scoped>
.wrap {
height: 100vh;
display: flex;
flex-direction: column;
}
.wrap-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.main-content {
flex: 1;
position: relative;
overflow: hidden;
color: var(--color-grey);
.breadcrumb-wrap {
background-color: #fff;
}
.selected-user {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #fff;
}
.no-data {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.depart-tree {
.depart-tree-item {
background-color: #fff;
padding: 0 16px;
line-height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
&:hover {
background-color: #f4f6fa;
}
}
.depart-tree-item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0 8px;
}
}
.depart-users-tree {
.allChecked {
padding: 0 16px;
padding-bottom: 16px;
padding-top: 12px;
background-color: #fff;
:deep(.ant-checkbox-wrapper) {
font-size: 12px;
}
}
.depart-users-tree-item {
line-height: 50px;
padding: 0 16px;
display: flex;
align-items: center;
cursor: pointer;
background-color: #fff;
&:hover {
background-color: #f4f6fa;
}
.right {
flex: 1;
display: flex;
align-items: center;
margin: 0 8px;
}
.depart-users-tree-item-circle {
width: 36px;
height: 36px;
border-radius: 50%;
background-color: #aaa;
overflow: hidden;
:deep(image) {
display: block;
width: 100%;
height: 100%;
}
}
.depart-users-tree-item-name {
margin-left: 8px;
}
}
}
.search-depart {
margin-bottom: 8px;
.search-depart-title {
padding-left: 16px;
line-height: 50px;
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
color: var(--UI-FG-1);
}
.search-depart-item {
line-height: 50px;
display: flex;
align-items: center;
padding: 0 16px;
cursor: pointer;
&:hover {
background-color: #f4f6fa;
}
.search-depart-item-name {
margin-left: 8px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.search-result {
background-color: #fff;
min-height: 100%;
}
.search-user {
margin-bottom: 8px;
.search-user-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
padding-left: 16px;
line-height: 50px;
color: var(--UI-FG-1);
}
.search-user-item {
display: flex;
align-items: center;
padding: 8px 16px;
cursor: pointer;
&:hover {
background-color: #f4f6fa;
}
.right {
flex: 1;
display: flex;
align-items: center;
margin: 0 8px;
}
.search-user-item-info {
display: flex;
flex-direction: column;
justify-content: center;
margin-left: 8px;
}
.search-user-item-circle {
width: 36px;
height: 36px;
border-radius: 50%;
overflow: hidden;
background-color: #aaa;
}
.search-user-item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-user-item-org {
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.wrap-footer {
padding: 10px;
padding-bottom: calc(constant(safe-area-inset-bottom) + 10px);
padding-bottom: calc(env(safe-area-inset-bottom) + 10px);
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #e5e5e5;
background-color: #fff;
.text {
display: flex;
align-items: center;
.num {
font-size: 18px;
margin: 0 5px;
color: var(--wot-color-theme);
}
}
.tip {
font-size: 11px;
color: #999;
}
}
.home-icon {
margin-right: 4px;
font-size: 16px;
}
</style>

View File

@@ -0,0 +1,115 @@
<route lang="json5" type="page">
{
layout: 'default',
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
}
</route>
<template>
<scroll-view scroll-y class="selected-user-container solid-top">
<view class="user-container">
<template v-if="selectedUsers.length">
<view class="user-item" v-for="item in selectedUsers" :key="item.id">
<view class="user-item-avatar" @click="handledelUser(item)">
<wd-img
width="36px"
height="36px"
radius="50%"
:src="getFileAccessHttpUrl(item.avatar)"
></wd-img>
<view class="u-iconfont u-icon-close"></view>
</view>
<view class="user-item-name ellipsis">{{ item.realname }}</view>
</view>
</template>
<template v-else>
<view class="empty-data">
<wd-status-tip image="content" tip="无选中用户" />
</view>
</template>
</view>
</scroll-view>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import { onShow, onHide, onLoad, onReady } from '@dcloudio/uni-app'
import { useMessage, useToast } from 'wot-design-uni'
import { useRouter } from '@/plugin/uni-mini-router'
import { useUserStore } from '@/store/user'
import { http } from '@/utils/http'
import { useParamsStore } from '@/store/page-params'
import { cache, getFileAccessHttpUrl } from '@/common/uitls'
defineOptions({
name: 'User',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps(['selectedUsers'])
const emit = defineEmits(['del'])
const router = useRouter()
const paramsStore = useParamsStore()
const userStore = useUserStore()
const toast = useToast()
const message = useMessage()
// 删除用户
const handledelUser = (item: any) => {
emit('del', item)
}
// 获取用户名的最后一个字符
const getLastCharacterOfName = (name: string): string => {
if (!name) return ''
return name.charAt(name.length - 1)
}
</script>
<style lang="scss" scoped>
//
.selected-user-container {
height: 100%;
}
.user-container {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
align-content: flex-start;
padding: 10px;
min-height: 100%;
.user-item {
position: relative;
display: flex;
width: 20%;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 10px;
.user-item-avatar {
position: relative;
background-color: #f5f5f5;
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 5px;
}
.u-icon-close {
position: absolute;
top: -3px;
right: -5px;
border-radius: 50%;
}
}
.empty-data {
width: 100%;
min-height: 100%;
display: flex;
align-items: center;
}
}
</style>

View File

@@ -0,0 +1,386 @@
<template>
<view class="TreeSelect">
<view class="pickerArea" :class="{ clear: !!showText }" @click="handleClick">
<wd-input
:placeholder="`请选择${$attrs.label}`"
v-bind="$attrs"
readonly
v-model="showText"
></wd-input>
<view v-if="!!showText && !$attrs.disabled" class="u-iconfont u-icon-close" @click.stop="handleClear"></view>
</view>
<wd-popup position="bottom" v-model="popupShow">
<view class="content">
<view class="operation">
<view class="cancel text-gray-5" @click.stop="cancel">取消</view>
<view class="confrim" @click.stop="confirm">确定</view>
</view>
<scroll-view class="flex-1" scroll-y>
<DaTree
v-if="popupShow"
:data="treeData"
labelField="title"
valueField="key"
loadMode
:showCheckbox="multiple"
:showRadioIcon="false"
:checkStrictly="true"
:loadApi="asyncLoadTreeData"
:defaultCheckedKeys="defaultCheckedKeys"
@change="handleTreeChange"
></DaTree>
</scroll-view>
</view>
</wd-popup>
</view>
</template>
<script setup lang="ts">
import { ref, watch, useAttrs } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import DaTree from '@/uni_modules/da-tree/index.vue'
import { isArray } from '@/utils/is'
defineOptions({
name: 'TreeSelect',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
modelValue: {
type: [Array, String],
},
dict: {
type: String,
default: 'id',
},
pidValue: {
type: String,
default: '',
},
pidField: {
type: String,
default: 'pid',
},
hasChildField: {
type: String,
default: '',
},
condition: {
type: String,
default: '',
required: false,
},
converIsLeafVal: {
type: Number,
default: 1,
},
// 是否支持多选
multiple: {
type: Boolean,
default: true,
},
hiddenNodeKey: {
type: String,
default: '',
},
// url: {
// type: String,
// default: '',
// },
// params: {
// type: Object,
// default: () => ({}),
// },
})
const emit = defineEmits(['change', 'update:modelValue'])
const toast = useToast()
const api = {
loadTreeData: '/sys/dict/loadTreeData',
view: '/sys/dict/loadDictItem/',
}
const showText = ref('')
const popupShow = ref(false)
const treeData = ref<any[]>([])
const treeValue = ref([])
const tableName = ref<any>('')
const text = ref<any>('')
const code = ref<any>('')
const attrs = useAttrs()
const defaultCheckedKeys: any = ref(props.multiple ? [] : '')
const handleClick = () => {
if (!attrs.disabled) {
popupShow.value = true
}
}
const cancel = () => {
popupShow.value = false
}
const confirm = () => {
const titles = treeValue.value.map((item) => item.title)
const keys = treeValue.value.map((item) => item.key).join(',')
showText.value = titles.join(',')
popupShow.value = false
emit('update:modelValue', keys)
emit('change', keys)
}
const handleTreeChange = (value, record) => {
const { originItem, checkedStatus } = record
const { key, title } = originItem
if (checkedStatus) {
// 选中
if (props.multiple) {
treeValue.value.push({ key, title })
} else {
treeValue.value = [{ key, title }]
}
} else {
// 取消
if (props.multiple) {
const findIndex = treeValue.value.findIndex((item) => item.key == key)
if (findIndex != -1) {
treeValue.value.splice(findIndex, 1)
}
} else {
treeValue.value = []
}
}
}
const transformField = (result) => {
for (let i of result) {
i.value = i.key
if (i.leaf == false) {
i.isLeaf = false
} else if (i.leaf == true) {
i.isLeaf = true
}
}
}
// 异步加载
const asyncLoadTreeData = ({ originItem }) => {
return new Promise<void>((resolve) => {
let param = {
pid: originItem.key,
pidField: props.pidField,
hasChildField: props.hasChildField,
converIsLeafVal: props.converIsLeafVal,
condition: props.condition,
tableName: unref(tableName),
text: unref(text),
code: unref(code),
}
http
.get(api.loadTreeData, param)
.then((res: any) => {
if (res.success) {
const { result } = res
transformField(result)
resolve(result)
} else {
resolve(null)
}
})
.catch((err) => resolve(null))
})
}
// 加载根节点
function loadRoot() {
let param = {
pid: props.pidValue,
pidField: props.pidField,
hasChildField: props.hasChildField,
condition: props.condition,
converIsLeafVal: props.converIsLeafVal,
tableName: unref(tableName),
text: unref(text),
code: unref(code),
}
http
.get(api.loadTreeData, param)
.then((res: any) => {
if (res.success) {
const { result } = res
if (result && result.length > 0) {
transformField(result)
handleHiddenNode(result)
treeData.value = result
}
} else {
toast.warning('自定义树组件根节点数据加载失败~')
}
})
.catch((err) => {
toast.warning('自定义树组件根节点数据加载失败~')
})
}
// 翻译input内的值
function loadItemByCode() {
let value = props.modelValue
if(value){
if (isArray(props.modelValue)) {
// @ts-ignore
value = value.join()
}
if (value === treeData.value.map((item) => item.key).join(',')) {
// 说明是刚选完,内部已有翻译。不需要再请求
return
}
value = (value as string).trim()
if (value) {
http
.get(`${api.view}${props.dict}`, { key: value })
.then((res: any) => {
if (res.success) {
const { result = [] } = res
showText.value = result.join(',')
if (props.multiple) {
defaultCheckedKeys.value = typeof value === 'string' ? value.split(',') : [value]
} else {
defaultCheckedKeys.value = value
}
} else {
}
})
.catch((err) => {})
}
}
}
const initDictInfo = () => {
let arr = props.dict?.split(',')
tableName.value = arr[0]
text.value = arr[1]
code.value = arr[2]
}
const handleHiddenNode = (data) => {
if (props.hiddenNodeKey && data?.length) {
for (let i = 0, len = data.length; i < len; i++) {
const item = data[i]
if (item.key == props.hiddenNodeKey) {
data.splice(i, 1)
i--
len--
return
}
}
}
}
const validateProp = () => {
let mycondition = props.condition
return new Promise<void>((resolve, reject) => {
if (!mycondition) {
resolve()
} else {
try {
let test = JSON.parse(mycondition)
if (typeof test == 'object' && test) {
resolve()
} else {
toast.error('组件TreeSelect-condition传值有误需要一个json字符串!')
reject()
}
} catch (e) {
toast.error('组件TreeSelect-condition传值有误需要一个json字符串!')
reject()
}
}
})
}
// 清空
const handleClear = () => {
showText.value = ''
treeValue.value = []
confirm()
}
watch(
() => props.modelValue,
() => loadItemByCode(),
{ deep: true, immediate: true },
)
watch(
() => props.dict,
() => {
initDictInfo()
loadRoot()
},
)
watch(
() => props.hiddenNodeKey,
() => {
if (treeData.value?.length && props.hiddenNodeKey) {
handleHiddenNode(treeData.value)
treeData.value = [...treeData.value]
}
},
)
// 初始化
validateProp().then(() => {
initDictInfo()
loadRoot()
loadItemByCode()
})
</script>
<style lang="scss" scoped>
:deep(.wd-popup-wrapper) {
.wd-popup {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
}
.content {
height: 50vh;
width: 100vw;
display: flex;
flex-direction: column;
.operation {
display: flex;
justify-content: space-between;
line-height: 40px;
padding: 0 5px;
position: relative;
&::before {
content: ' ';
position: absolute;
bottom: 0;
left: 8px;
right: 8px;
height: 1px;
background-color: #e5e5e5;
}
.cancel,
.confrim {
font-size: 15px;
height: 40px;
min-width: 40px;
text-align: center;
}
.confrim {
color: var(--wot-color-theme);
}
}
:deep(.da-tree) {
.da-tree-item__checkbox {
// display: none;
}
}
}
.pickerArea {
position: relative;
.u-icon-close {
position: absolute;
right: 15px;
top: calc(14px + 4px);
color: #585858;
font-size: 15px;
}
&.clear {
:deep(.wd-input__body) {
padding-right: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,22 @@
declare module 'vue' {
export interface GlobalComponents {
BottomOperate: typeof import('./BottomOperate/BottomOperate.vue')['default']
Breadcrumb: typeof import('./Breadcrumb/Breadcrumb.vue')['default']
CategorySelect: typeof import('./CategorySelect/CategorySelect.vue')['default']
DateTime: typeof import('./DateTime/DateTime.vue')['default']
Grid: typeof import('./Grid/Grid.vue')['default']
ImgPreview: typeof import('./ImgPreview/ImgPreview.vue')['default']
LFile: typeof import('./LFile/LFile.vue')['default']
PageLayout: typeof import('./PageLayout/PageLayout.vue')['default']
Popup: typeof import('./Popup/Popup.vue')['default']
PopupDict: typeof import('./PopupDict/PopupDict.vue')['default']
ProgressMap: typeof import('./ProgressMap/ProgressMap.vue')['default']
RightConditionFilter: typeof import('./RightConditionFilter/RightConditionFilter.vue')['default']
SelectDept: typeof import('./SelectDept/SelectDept.vue')['default']
SelectUser: typeof import('./SelectUser/SelectUser.vue')['default']
SelectUserByDepart: typeof import('./SelectUserByDepart/SelectUserByDepart.vue')['default']
TreeSelect: typeof import('./TreeSelect/TreeSelect.vue')['default']
}
}
export {}

View File

@@ -0,0 +1,169 @@
// 定义 FormProperty 函数
const FormProperty = (propertyId, formSchema, required = []) => {
// 初始化私有属性
const _propertyId = propertyId;
const _formSchem = formSchema;
const _required = required;
// 定义 formSchema 的 getter 方法
const getFormSchema = () => {
return _formSchem || {};
};
// 定义 key 的 getter 方法
const getKey = () => {
return _propertyId;
};
// 定义 type 的 getter 方法
const getType = () => {
return getFormSchema().view;
};
// 定义 disabled 的 getter 方法
const getDisabled = () => {
if (_formSchem && _formSchem.ui && _formSchem.ui.widgetattrs && _formSchem.ui.widgetattrs.disabled === true) {
return true;
}
return false;
};
// 定义 label 的 getter 方法
const getLabel = () => {
const schema = getFormSchema();
return schema.title || getKey();
};
// 定义 placeholder 的 getter 方法
const getPlaceholder = () => {
const viewType = getType();
const label = getLabel();
if (viewType.indexOf('date') >= 0 || viewType.indexOf('select') >= 0 || viewType.indexOf('list') >= 0) {
return "请选择" + label;
} else if (viewType.indexOf('upload') >= 0 || viewType.indexOf('file') >= 0 || viewType.indexOf('image') >= 0) {
return "请上传" + label;
} else {
return "请输入" + label;
}
};
// 定义 dictStr 的 getter 方法
const getDictStr = () => {
const viewType = getType();
if (viewType === 'sel_search') {
const schema = getFormSchema();
return schema.dictTable + ',' + schema.dictText + ',' + schema.dictCode;
}
return '';
};
// 定义 listSource 的 getter 方法
const getListSource = () => {
const schema = getFormSchema();
if (!schema.enum) {
return [];
}
const arr = [...schema.enum];
for (let a = 0; a < arr.length; a++) {
if (!arr[a].label) {
arr[a].label = arr[a].text;
}
if (schema.type === 'number') {
arr[a].value = parseInt(arr[a].value);
}
}
return arr;
};
// 定义 popupCode 的 getter 方法
const getPopupCode = () => {
return getFormSchema().code;
};
// 定义 dest 的 getter 方法
const getDest = () => {
return getFormSchema().destFields;
};
// 定义 ogn 的 getter 方法
const getOgn = () => {
return getFormSchema().orgFields;
};
// 定义 rules 的 getter 方法
const getRules = () => {
const rules = [];
const isRequired = _required?.includes(getKey()) ?? false;
if (isRequired) {
let msg = getLabel() + '为必填项';
rules.push({ required: true, message: msg });
}
let viewType = getType();
if ('list' === viewType || 'markdown' === viewType || 'pca' === viewType) {
return rules;
}
if (viewType.indexOf('upload') >= 0 || viewType.indexOf('file') >= 0 || viewType.indexOf('image') >= 0) {
return rules;
}
const schema = getFormSchema();
if (schema.pattern) {
if (schema.pattern === 'only') {
// 这里 checkOnlyMethod 未定义,需要根据实际情况补充
rules.push({ validator: () => {} });
} else if (schema.pattern === 'z') {
if (schema.type === 'number' || schema.type === 'integer') {
// 这里 onlyInteger 未定义,需要根据实际情况处理
} else {
rules.push({ pattern: '^-?[1-9]\\d*$', message: '请输入整数' });
}
} else {
let msg = getLabel() + '校验未通过';
rules.push({ pattern: schema.pattern, message: msg });
}
}
return rules;
};
// 返回包含所有 getter 方法的对象
return {
get formSchema() {
return getFormSchema();
},
get key() {
return getKey();
},
get type() {
return getType();
},
get disabled() {
return getDisabled();
},
get label() {
return getLabel();
},
get placeholder() {
return getPlaceholder();
},
get dictStr() {
return getDictStr();
},
get listSource() {
return getListSource();
},
get popupCode() {
return getPopupCode();
},
get dest() {
return getDest();
},
get ogn() {
return getOgn();
},
get rules() {
return getRules();
}
};
};
export default FormProperty;

View File

@@ -0,0 +1,157 @@
<template>
<view class="onlineTableCell">
<!--图片-->
<template v-if="column?.type === 'image'">
<template v-if="record[column.key]">
<wd-img
width="30"
height="30"
:src="getFirstImg(record[column.key])"
@click="handleClickImg"
></wd-img>
<ImgPreview
v-if="imgPreview.show"
:urls="imgPreview.urls"
@close="() => (imgPreview.show = false)"
></ImgPreview>
</template>
<template v-else>
<text>无图片</text>
</template>
</template>
<!--下载-->
<template v-else-if="column?.type === 'file'">
<template v-if="record[column.key]">
<wd-button @click="handleDownload(record[column.key])">下载</wd-button>
</template>
<template v-else>
<text>无文件</text>
</template>
</template>
<template v-else-if="['markdown', 'umeditor'].includes(column?.type)">
<rich-text :nodes="record[column.key]"></rich-text>
</template>
<template v-else-if="column?.type === 'pca'">
<text class="ellipsis-2">{{ getPcaText(record[column.key])}}</text>
</template>
<template v-else-if="['datetime', 'date'].includes(column?.type)">
<text class="ellipsis-2">{{ getFormatDate(record[column.key], column) }}</text>
</template>
<template v-else>
<text class="ellipsis-2">{{ renderVal(record, column)}}</text>
</template>
</view>
</template>
<script setup lang="ts">
import { getFormatDate, filterMultiDictText } from '@/common/uitls'
import { isString } from '@/utils/is'
import { getFileAccessHttpUrl } from '@/common/uitls'
import { getAreaTextByCode } from '@/common/areaData/Area'
defineOptions({
name: 'OnlineSubTableCell',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
columnsInfo: {
type: Object,
default: () => {},
},
column: {
type: Object,
default: () => {},
},
record: {
type: Object,
default: () => {},
},
})
console.log("根据配置动态加载表单record ",props.record );
const imgPreview = ref({
show: false,
urls: [],
})
// 下载
const handleDownload = (text) => {
uni.downloadFile({
url: text,
success: (res) => {
if (res.statusCode === 200) {
console.log('下载成功')
console.log(res);
}
},
})
}
// 省市区
const getPcaText = (code) => {
if (!code) {
return ''
}
return getAreaTextByCode(code)
}
// 列表只显示第一张图
const getFirstImg = (text) => {
if (isString(text)) {
var imgs = text.split(',')
return getFileAccessHttpUrl(imgs[0])
} else {
return ''
}
}
// 点击图时
const handleClickImg = () => {
imgPreview.value.show = true
}
// 渲染值
const renderVal = (record, column) => {
const { type , key } = column
let text = record[key]
if (['date', 'Date'].includes(type)) {
if (!text) {
return ''
}
if (text.length > 10) {
return text.substring(0, 10)
}
return text
} else if (['popup_dict'].includes(type)) {
const dict = record[key + '_dictText']
if (dict != undefined) {
return record[key + '_dictText']
}
return text
}
//字典值翻譯
if(props.columnsInfo?.dictOptions && props.columnsInfo?.dictOptions[key]){
return filterMultiDictText(props.columnsInfo.dictOptions[key], text + '')
}
return text
}
// 初始化
const init = () => {
const field = props.column.dataIndex
if (props.column?.customRender === 'imgSlot') {
const text = props.record[field]
if (isString(text)) {
imgPreview.value.urls = text.split(',').map((item) => getFileAccessHttpUrl(item))
} else {
return ''
}
}
}
init()
</script>
<style lang="scss" scoped>
:deep(.wd-button) {
--wot-button-medium-height: 30px;
--wot-button-medium-fs: 12px;
--wot-button-medium-padding: 8px;
&.is-medium.is-round {
min-width: 80px;
}
}
</style>

View File

@@ -0,0 +1,867 @@
<template>
<wd-popup position="bottom" v-model="show">
<PageLayout
:navTitle="navTitle"
type="popup"
navRightText="确定"
@navRight="handleConfirm"
@navBack="handleCancel"
>
<scroll-view scroll-y class="wrap">
<wd-form ref="form" :model="formData">
<wd-cell-group class="mb-14px" border>
<view
class="onlineLoader-form"
v-for="(item, index) in rootProperties"
:key="index"
:class="{ 'mt-14px': index % 2 == 0 }"
>
<!-- 图片 -->
<wd-cell
v-if="item.type == 'image'"
:name="item.key"
:title="get4Label(item.label)"
:title-width="labelWidth"
:required="fieldRequired(item)"
>
<online-image
v-model:value="formData[item.key]"
:name="item.key"
:disabled="componentDisabled(item)"
:key="index"
></online-image>
</wd-cell>
<!-- 文件 -->
<wd-cell
v-else-if="item.type == 'file'"
:name="item.key"
:title="get4Label(item.label)"
:title-width="labelWidth"
:required="fieldRequired(item)"
>
<view style="text-align: left">
<!-- #ifndef APP-PLUS -->
<online-file
v-model:value="formData[item.key]"
:name="item.key"
:disabled="componentDisabled(item)"
:key="index"
:maxNum="getFieldExtendJson(item, 'uploadnum')"
></online-file>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<online-file-custom
v-model:value="formData[item.key]"
:name="item.key"
:disabled="componentDisabled(item)"
:key="index"
:maxNum="getFieldExtendJson(item, 'uploadnum')"
></online-file-custom>
<!-- #endif -->
</view>
</wd-cell>
<!-- 开关 -->
<wd-cell
v-else-if="['switch', 'checkbox'].includes(item.type)"
:name="item.key"
:title="get4Label(item.label)"
:title-width="labelWidth"
center
:required="fieldRequired(item)"
>
<view style="text-align: left">
<wd-switch
:label="get4Label(item.label)"
:name="item.key"
size="18px"
:disabled="componentDisabled(item)"
v-model="formData[item.key]"
:active-value="switchOpt(item.formSchema?.extendOption, 0)"
:inactive-value="switchOpt(item.formSchema?.extendOption, 1)"
/>
</view>
</wd-cell>
<!-- 日期时间 -->
<DateTime
v-else-if="item.type === 'datetime'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
startTime="1949-01-01 00:00:00"
endTime="2050-01-01 00:00:00"
:type="item.type"
:name="item.key"
format="YYYY-MM-DD HH:mm:ss"
:disabled="componentDisabled(item)"
v-model="formData[item.key]"
:required="fieldRequired(item)"
></DateTime>
<!-- 时间 -->
<DateTime
v-else-if="item.type === 'time'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
format="HH:mm:ss"
:type="item.type"
:name="item.key"
:disabled="componentDisabled(item)"
v-model="formData[item.key]"
:required="fieldRequired(item)"
></DateTime>
<!-- 日期 -->
<online-date
v-else-if="item.type === 'date'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:name="item.key"
:type="getDateExtendType(item.formSchema)"
:disabled="componentDisabled(item)"
v-model:value="formData[item.key]"
:required="fieldRequired(item)"
></online-date>
<!-- 时间 -->
<online-time
v-else-if="item.type === 'time'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:name="item.key"
:disabled="componentDisabled(item)"
v-model:value="formData[item.key]"
:required="fieldRequired(item)"
></online-time>
<!-- 下拉选择 -->
<online-select
v-else-if="item.type === 'list' || item.type === 'sel_search'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:name="item.key"
:type="item.type"
:dict="item.options"
:dictStr="item.dictStr"
:disabled="componentDisabled(item)"
v-model="formData[item.key]"
:required="fieldRequired(item)"
></online-select>
<!-- checkbox -->
<online-checkbox
v-else-if="item.type === 'checkbox'"
:name="item.key"
:type="item.type"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:dict="item.options"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></online-checkbox>
<!-- radio -->
<online-radio
v-else-if="item.type === 'radio'"
:name="item.key"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:type="item.type"
:dict="item.options"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></online-radio>
<!-- 下拉多选 -->
<online-multi
v-else-if="item.type === 'list_multi'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:name="item.key"
:dict="item.options"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></online-multi>
<!-- 省市区 -->
<online-pca
v-else-if="item.type === 'pca'"
:name="item.key"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model:value="formData[item.key]"
></online-pca>
<!-- 数字框 小数 -->
<wd-input
v-else-if="
item.type === 'number' && (!item.onlyInteger || item.onlyInteger == false)
"
:label-width="labelWidth"
v-model="formData[item.key]"
:label="get4Label(item.label)"
:name="item.key"
inputMode="decimal"
:disabled="componentDisabled(item)"
:placeholder="item.placeholder"
:rules="item.validateRules"
/>
<!-- 数字框 整数 -->
<wd-input
v-else-if="item.type === 'number' && item.onlyInteger === true"
:label-width="labelWidth"
:label="get4Label(item.label)"
:name="item.key"
v-model="formData[item.key]"
inputMode="numeric"
:disabled="componentDisabled(item)"
:placeholder="item.placeholder"
:rules="item.validateRules"
/>
<!-- 多行文本 -->
<wd-textarea
v-else-if="['textarea', 'markdown', 'umeditor'].includes(item.type)"
:label-width="labelWidth"
:label="get4Label(item.label)"
:name="item.key"
v-model="formData[item.key]"
clearable
:maxlength="300"
:disabled="componentDisabled(item)"
:placeholder="item.placeholder"
:rules="item.validateRules"
/>
<!-- 密码输入框 -->
<wd-input
v-else-if="item.type === 'password'"
:label-width="labelWidth"
v-model="formData[item.key]"
:disabled="componentDisabled(item)"
:label="get4Label(item.label)"
:name="item.key"
:placeholder="item.placeholder"
:rules="item.validateRules"
show-password
/>
<!-- popup字典 -->
<PopupDict
v-else-if="item.type === 'popup_dict'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
:multi="item.formSchema.popupMulti"
:dictCode="`${item.formSchema.code},${item.formSchema['destFields']},${item.formSchema['orgFields']}`"
></PopupDict>
<!-- popup -->
<Popup
v-else-if="item.type === 'popup'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
:multi="!!item.formSchema?.popupMulti"
:code="`${item.formSchema.popupCode}`"
:setFieldsValue="setFieldsValue"
:fieldConfig="getPopupFieldConfig(item)"
></Popup>
<!-- 关联记录 -->
<online-popup-link-record
v-else-if="item.type === 'link_table'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:name="item.key"
v-model:formSchema="item.formSchema"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model:value="formData[item.key]"
@selected="linkRecordChange"
></online-popup-link-record>
<!-- 他表字段 -->
<wd-input
v-else-if="item.type === 'link_table_field'"
:label-width="labelWidth"
v-model="formData[item.key]"
:disabled="true"
:label="get4Label(item.label)"
:name="item.key"
/>
<!-- 用户选择 -->
<select-user
v-else-if="item.type === 'sel_user'"
:label-width="labelWidth"
:name="item.key"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></select-user>
<!-- 部门选择 -->
<select-dept
v-else-if="item.type === 'sel_depart'"
:label-width="labelWidth"
:name="item.key"
:label="get4Label(item.label)"
labelKey="departName"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></select-dept>
<!-- 分类字典树 -->
<CategorySelect
v-else-if="item.type === 'cat_tree'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
:pid="`${item.formSchema.pidValue}`"
></CategorySelect>
<!-- 自定义树 -->
<TreeSelect
v-else-if="item.type === 'sel_tree'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
:dict="`${item.formSchema.dict}`"
:pidField="`${item.formSchema.pidField}`"
:pidValue="`${item.formSchema.pidValue}`"
:hasChildField="`${item.formSchema.hasChildField}`"
></TreeSelect>
<!-- 普通输入框 -->
<wd-input
v-else-if="item.type !== 'hidden'"
:label-width="labelWidth"
v-model="formData[item.key]"
:disabled="componentDisabled(item)"
:label="get4Label(item.label)"
:name="item.key"
:placeholder="item.placeholder"
:rules="item.validateRules"
clearable
/>
</view>
</wd-cell-group>
</wd-form>
</scroll-view>
</PageLayout>
</wd-popup>
<wd-toast></wd-toast>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useToast } from 'wot-design-uni'
import OnlineCheckbox from '@/components/online/view/online-checkbox.vue'
import OnlineMulti from '@/components/online/view/online-multi.vue'
import SelectUser from '@/components/SelectUser/SelectUser.vue'
import OnlinePopupLinkRecord from '@/components/online/view/online-popup-link-record.vue'
import OnlineSelect from '@/components/online/view/online-select.vue'
import OnlineDate from '@/components/online/view/online-date.vue'
import OnlineTime from '@/components/online/view/online-time.vue'
import OnlineRadio from '@/components/online/view/online-radio.vue'
import SelectDept from '@/components/SelectDept/SelectDept.vue'
import OnlineImage from '@/components/online/view/online-image.vue'
import OnlineFile from '@/components/online/view/online-file.vue'
import OnlineFileCustom from '@/components/online/view/online-file-custom.vue'
import OnlinePca from '../view/online-pca.vue'
import { loadOneFieldDefVal } from '../defaultVal'
import { isArray, isNumber, isString } from '@/utils/is'
import { formatDate } from '@/common/uitls'
import { deepClone } from 'wot-design-uni/components/common/util'
import { duplicateCheck } from '@/service/api'
import { isObject } from '@/common/is'
defineOptions({
name: 'OnlineSubformPopup',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
title: {
type: String,
default: '',
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
schema: {
type: Object,
default: () => ({}) as any,
required: true,
},
})
const emit = defineEmits(['change', 'close'])
const toast = useToast()
const show = ref(true)
//表数据Data
const formData = ref<any>({})
//是否填值规则字段
const hasFillRuleFields = ref([])
//是否必填字段
const hasRequiredFields = ref([])
//字段属性
const rootProperties = ref<any>([])
//表描述
const tableTxt = ref('')
//表名
const tableName = ref('')
//数据Id
const dataId = ref('')
//是否已加载
const loaded = ref(false)
//是否编辑
const isUpdate = ref(false)
//临时数据
const tempData = ref({})
// 标题宽度
const labelWidth = computed(() => {
return '100px'
})
// 导航标题
const get4Label = computed(() => {
return (lable) => {
return `${lable && lable.length > 4 ? lable.substring(0, 4) : lable}`
}
})
// 导航标题
const navTitle = computed(() => {
if (!props.title || props.title.length === 0) {
return tableTxt.value
} else {
return props.title
}
})
/**
* 获取日期控件的扩展类型
* @param formSchema
* @returns {string}
*/
const getDateExtendType = (formSchema: any) => {
if (formSchema.fieldExtendJson) {
let fieldExtendJson = JSON.parse(formSchema.fieldExtendJson)
let mapField = {
month: 'year-month',
year: 'year',
quarter: 'quarter',
week: 'week',
day: 'date',
}
return fieldExtendJson?.picker && mapField[fieldExtendJson?.picker]
? mapField[fieldExtendJson?.picker]
: 'date'
}
return 'date'
}
/**
* 判断是否选中
* @param opts
* @param value
* @returns {boolean|boolean}
*/
const isChecked = (opts: any, value: any) => {
return opts && opts.length > 0 ? value === opts[0] : false
}
/**
* 开关选项
* @param opts
* @param value
* @returns {boolean|boolean}
*/
const switchOpt = (opts: any, index: any) => {
const options = Array.isArray(opts) && opts.length > 0 ? opts : ['Y', 'N']
return options[index] + ''
}
/**
*
* @param item
* @returns {*|boolean}
*/
const componentDisabled = (item: any) => {
if (props.disabled === true) {
return true
}
return item.disabled
}
/**
* 获取扩展字段
* @param item
* @param field
*/
const getFieldExtendJson = (item, field) => {
let json = item.formSchema.fieldExtendJson ?? '{}'
if (isString(json) && json.trim().length === 0) {
json = '{}'
}
json = JSON.parse(json)
const result = json[field]
return result
}
/**
* 判断字段是否必填
* @param item
* @returns {boolean}
*/
const fieldRequired = (item: any) => {
return item?.key && hasRequiredFields.value.includes(item.key)
}
/**
* 关联记录同步修改他表字段
* @param linkRecord
* @param key
*/
const linkRecordChange = (linkRecord, key) => {
let linkFieldArr = rootProperties.value.filter(
(item) => item.type === 'link_table_field' && item?.formSchema?.dictTable == key,
)
console.log('linkRecordChange****》》》》', linkRecordChange)
linkFieldArr.forEach((field) => {
let value = linkRecord.map((record) => record[field.formSchema.dictText]).join(',')
nextTick(() => {
formData.value[field.key] = value
})
})
}
/**
* 校验字段
* @param values
* @returns {boolean}
*/
const fieldCheck = async (values: any) => {
// 校验字段
let flag = false
console.log('一对多子表rootProperties.value', rootProperties.value)
console.log('一对多子表values', values)
for (const item of rootProperties.value) {
// 校验提示
const tip = (msg) => {
// 提示校验未通过
toast.warning(`${msg}`)
flag = true
}
// 校验必填
if (fieldRequired(item) && !values[item.key]) {
tip(`${item.label}不能为空!`)
break
}
// 校验正则
let pattern = item?.formSchema?.pattern || item?.pattern
if (pattern) {
//可能存在是一个对象的情况,把正则表达式赋值一下
if (isObject(pattern)) {
pattern = pattern?.pattern
}
if (pattern == 'only') {
const res: any = await duplicateCheck({
tableName: tableName.value,
fieldName: item.key,
fieldVal: values[item.key],
dataId: dataId.value,
})
if (!res.success) {
tip(`${item.label} ${res.message}`)
break
}
} else {
const regex = compilePattern(pattern)
if (values[item.key] && !regex.test(values[item.key])) {
let errorInfo = item?.formSchema?.errorInfo || '格式不正确!'
tip(`${item.label}${errorInfo}`)
break
}
}
}
}
return flag
}
/**
* 省市区获取最后一位
* @param value
*/
function pcaValue(value) {
if (value.includes(',')) {
const parts = value.split(',')
if (parts.length >= 3) {
// 如果包含至少两个逗号,截取第三部分并去掉最后一位
return parts[2]
}
}
// 如果不包含逗号或不符合条件,返回原值
return value
}
/**
* 处理多选字段
* @param value
*/
function handleMultiOrDateField() {
let finalData = deepClone(formData.value)
//日期字段
let dateFieldArr = rootProperties.value.filter(
(item) => item.type === 'date' || item.type === 'datetime',
)
//省市区字段
let pcaArr = rootProperties.value.filter((item) => item.type === 'pca')
finalData = Object.keys(finalData).reduce((acc, key) => {
let value = finalData[key]
//省市区获取最后一位
if (value && pcaArr.length > 0 && pcaArr.map((item) => item.key).includes(key)) {
value = isArray(value) ? value[2] : pcaValue(value)
}
//是数组的就转换成字符串
if (value && isArray(value)) {
value = value.join(',')
}
//时间戳类型的日期转具体格式字符串
if (dateFieldArr.length > 0) {
const dateField = dateFieldArr.find((obj) => obj.key === key)
if (dateField) {
value =
value && isNumber(value)
? formatDate(
value,
dateField.type === 'datetime' ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd',
)
: value
}
}
acc[key] = value
return acc
}, {})
return finalData
}
//打开弹窗前
function beforeOpen(row, edit) {
isUpdate.value = edit
tempData.value = row
formData.value = { ...row }
}
/**
* 关闭
*/
const handleClose = () => {
setTimeout(() => {
emit('close')
}, 400)
}
/**
* 提交
*/
const handleConfirm = async () => {
// 判断字段必填和正则
if (await fieldCheck(formData.value)) {
return
}
// 处理特殊字段
let finalData = await handleMultiOrDateField()
emit('change', finalData, isUpdate.value)
//关闭弹窗
handleClose()
}
/**
* 取消
*/
const handleCancel = () => {
show.value = false
loaded.value = false
handleClose()
console.log('取消了~')
}
/**
* 设置pop组件数值
* @param data
*/
const setFieldsValue = (data) => {
formData.value = { ...formData.value, ...data }
}
/**
* popup组件配置
* @param item
*/
const getPopupFieldConfig = (item) => {
const { formSchema } = item
const { destFields = '', orgFields = '' } = formSchema
const result = orgFields.split(',').map((oField, index) => {
return {
source: oField,
target: destFields.split(',')[index],
}
})
return result
}
//监听配置变化
watchEffect(() => {
props.schema && !loaded.value && loadTableInfo(props.schema)
})
/**
* 初始化表单数据
*/
function initFormData(row) {
dataId.value = row?.id
formData.value = deepClone(row)
}
/**
* 根据配置动态加载表单
* @param dataID
*/
function loadTableInfo(formSchema: any) {
console.log('===子表many加载表单数据 schema===', formSchema)
createRootProperties(formSchema)
tableTxt.value = formSchema?.describe
if (isUpdate.value) {
initFormData(tempData.value)
} else {
// 新增页面处理表单默认值
handleDefaultValue()
}
loaded.value = true
}
/**
* 创建根属性
* @param formSchema
*/
function createRootProperties(formSchema: any) {
console.log('===子表配置项 formSchema===', formSchema)
formData.value = {}
hasRequiredFields.value = []
hasFillRuleFields.value = formSchema?.hasFillRuleFields ?? []
tableName.value = formSchema.key
const properties = formSchema.columns
let rootProps = []
properties.forEach((item) => {
if (item?.key && item?.key != 'action') {
formData.value[item?.key] = ''
let patternInfo = null
if (item?.validateRules && item?.validateRules.length > 0) {
//字段是否必填
let isRequired = item?.validateRules.some((valid) => valid?.required)
isRequired && hasRequiredFields.value.push(item.key)
//正则校验其他配置信息
patternInfo = item?.validateRules.find((valid) => valid.pattern)
if (patternInfo && patternInfo?.pattern && patternInfo.pattern == '*') {
hasRequiredFields.value.push(item.key)
}
}
rootProps.push({
...item,
type: item?.view || item.type,
label: item.title,
pattern: patternInfo,
formSchema: { ...item, pattern: patternInfo?.pattern, errorInfo: patternInfo?.message },
})
}
})
rootProperties.value = [...rootProps]
console.log('--子表 rootProperties--', rootProps)
}
/**
* 获取默认值
*/
function handleDefaultValue() {
rootProperties.value.forEach((item) => {
let field = item.key
let { defaultValue, type, fieldDefaultValue } = item.formSchema
let defVal = defaultValue || fieldDefaultValue
loadOneFieldDefVal(defVal, type, (value) => {
formData.value[field] = value
})
})
}
/**
* 根据规则表将 value 转换为对应的正则表达式
* @param {string} value 输入的值(如 "n6-16", "s6-18", "*6-16", "url", "e" 等)
* @returns {RegExp} 返回对应的正则表达式
*/
function compilePattern(value) {
let builtIn = patternArr.some((item) => item.value == value)
if (!builtIn) {
return new RegExp(value)
}
// 数字类型n
if (value.startsWith('n')) {
const rangeMatch = value.match(/^n(\d+)-(\d+)$/)
if (rangeMatch) {
const min = rangeMatch[1]
const max = rangeMatch[2]
return new RegExp(`^\\d{${min},${max}}$`)
}
return /^\d+$/ // 默认:纯数字
}
// 字母类型s
if (value.startsWith('s')) {
const rangeMatch = value.match(/^s(\d+)-(\d+)$/)
if (rangeMatch) {
const min = rangeMatch[1]
const max = rangeMatch[2]
return new RegExp(`^[a-zA-Z]{${min},${max}}$`)
}
return /^[a-zA-Z]+$/ // 默认:纯字母
}
// 任意字符(*
if (value.startsWith('*')) {
if (value === '*') return /^.+$/ // 非空
const rangeMatch = value.match(/^\*(\d+)-(\d+)$/)
if (rangeMatch) {
const min = rangeMatch[1]
const max = rangeMatch[2]
return new RegExp(`^.{${min},${max}}$`)
}
}
// 其他预定义规则
const predefinedPatterns = {
url: /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/i,
e: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
m: /^1[3-9]\d{9}$/,
p: /^[1-9]\d{5}$/,
z: /^-?\d+$/,
money: /^-?\d+(\.\d{1,2})?$/,
}
return predefinedPatterns[value] || new RegExp(`^${escapeRegExp(value)}$`)
}
// 辅助函数:转义正则特殊字符
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
const patternArr = [
{ label: '6到16位数字', value: 'n6-16' },
{ label: '6到18位字母', value: 's6-18' },
{ label: '6到16位任意字符', value: '*6-16' },
{ label: '网址', value: 'url' },
{ label: '电子邮件', value: 'e' },
{ label: '手机号码', value: 'm' },
{ label: '邮政编码', value: 'p' },
{ label: '字母', value: 's' },
{ label: '数字', value: 'n' },
{ label: '整数', value: 'z' },
{ label: '非空', value: '*' },
{ label: '金额', value: 'money' },
]
defineExpose({
beforeOpen,
})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,30 @@
/*
*
* 这里填写用户自定义的表达式
* 可用在Online表单的默认值表达式中使用
* 需要外部使用的变量或方法一定要 export否则无法识别
* 示例:
* export const name = '张三'; // const 是常量
* export let age = 17; // 看情况 export const 还是 let ,两者都可正常使用
* export function content(arg) { // export 方法可传参数使用时要加括号值一定要return回去可以返回Promise
* return 'content' + arg;
* }
* export const address = (arg) => content(arg) + ' | 北京市'; // export 箭头函数也可以
*
*/
/** 字段默认值官方示例:获取地址 */
export function demoFieldDefVal_getAddress(arg) {
if (!arg) {
arg = '朝阳区';
}
return `北京市 ${arg}`;
}
/** 自定义JS函数示例 */
export function sayHi(name) {
if (!name) {
name = '张三';
}
return `您好,我叫: ${name}`;
}

View File

@@ -0,0 +1,302 @@
// 移动端不支持自定义表达式设置的默认值
import {http} from "@/utils/http";
import {useUserStore} from "@/store";
import * as CustomExpression from './customExpression';
import dayjs from "dayjs";
// 获取所有用户自定义表达式的Key
const ceKeys = Object.keys(CustomExpression)
// 将key用逗号拼接可以拼接成方法参数a,b,c --> function(a,b,c){}
const ceJoin = ceKeys.join(',')
// 将用户自定义的表达式按key的顺序放到数组中可以使用 apply 传递给方法直接调用
const $CE$ = ceKeys.map(key => CustomExpression[key])
/** 普通规则表达式 #{...} */
const normalRegExp = /#{([^}]+)?}/g
/** 用户自定义规则表达式 {{...}} */
const customRegExp = /{{([^}]+)?}}/g
/** 填值规则表达式 ${...} */
const fillRuleRegExp = /\${([^}]+)?}/g
/** action 类型 */
export const ACTION_TYPES = { ADD: 'add', EDIT: 'edit', DETAIL: 'detail', RELOAD: 'reload' }
/**
* 获取单个字段的默认值-通过回调函数返回值
* @param {Object} defVal
* @param {Object} type
* @param {Object} callback
*/
export async function loadOneFieldDefVal(defVal, type, callback) {
if(hasEffectiveValue(defVal)){
let value = await handleDefaultValue(defVal, ACTION_TYPES.ADD, {});
if ('number' === type && value) {
value = Number.parseFloat(value)
}
callback(value)
}
}
/**
* 判断给定的值是不是有效的
*/
function hasEffectiveValue(val) {
if(val || val === 0){
return true;
}
return false;
}
/**
* 获取默认值
* @param {Object} defVal
* @param {Object} action
* @param {Object} getFormData
*/
async function handleDefaultValue(defVal, action, getFormData) {
if (defVal != null) {
// 检查类型,如果类型错误则不继续运行
if (checkExpressionType(defVal)) {
let value = await getDefaultValue(defVal, action, getFormData)
if (value != null) {
return value
}
}
}
return defVal;
}
/**
* 加载form组件默认值
* @param form Form对象
* @param properties 字段配置
* @param action 操作类型ACTION_TYPES除填值规则外其他表达式只在add下才执行
* @param getFormData 获取数据的方法,用于填值规则向后台传值
*/
export function loadFieldDefVal({ form, properties, action, getFormData }) {
if (Array.isArray(properties) && properties.length > 0) {
properties.forEach(async prop => {
let { defVal, type } = prop._formSchem
// key取值错误导致 树形表 表单默认值未生效 【online】树列表不支持控件默认值表达式配置 (博威)
let key = prop.key
// 2021年5月21日 Tree类型表单系统编码不生效。【issues/I3NR39】
if (!key) {
key = prop._propertyId
}
eachHandler(key, defVal, action, (value) => {
// 处理数字类型如果type=number并且value有值
if ('number' === type && value) {
// parseFloat() 可以直接处理字符串、整数、小数、null和undefined
// 非数字类型直接返回NaN不必担心报错
value = Number.parseFloat(value)
}
form.setFieldsValue({ [key]: value })
}, getFormData)
})
}
}
/** 加载JEditableTable组件默认值 */
export function loadFieldDefValForSubTable({ subForms, subTable, row, action, getFormData }) {
if (subTable && Array.isArray(subTable.columns) && subTable.columns.length > 0) {
subTable.columns.forEach(async column => {
let { key, fieldDefaultValue: defVal } = column
eachHandler(key, defVal, action, (value) => {
if (subForms.form) {
subForms.form.setFieldsValue({ [key]: value })
} else {
// update-begin---author:sunjianlei Date:20200725 foronline功能测试行操作切换成新的行编辑-----------
let v = [{rowKey: row.id, values: {[key]: value}}];
(subForms.jvt || subForms.jet).setValues(v)
// update-end---author:sunjianlei Date:20200725 foronline功能测试行操作切换成新的行编辑------------
}
}, getFormData)
})
}
}
async function eachHandler(key, defVal, action, callback, getFormData) {
if (defVal != null) {
// 检查类型,如果类型错误则不继续运行
if (checkExpressionType(defVal)) {
let value = await getDefaultValue(defVal, action, getFormData)
if (value != null) {
callback(value)
}
} else {
// 不合法的表达式直接返回不解析
callback(defVal)
}
}
}
/**
* 检查表达式类型是否合法,规则:
* 1、填值规则表达式不能和其他表达式混用
* 2、每次只能填写一个填值规则表达式
* 3、普通表达式和用户自定义表达式可以混用
*/
export function checkExpressionType(defVal) {
// 获取各个表达式的数量
let normalCount = 0, customCount = 0, fillRuleCount = 0
defVal.replace(fillRuleRegExp, () => fillRuleCount++)
if (fillRuleCount > 1) {
logWarn(`表达式[${defVal}]不合法:只能同时填写一个填值规则表达式!`)
return false
}
defVal.replace(normalRegExp, () => normalCount++)
defVal.replace(customRegExp, () => customCount++)
// 除填值规则外其他规则的数量
let fillRuleOtherCount = normalCount + customCount
if (fillRuleCount > 0 && fillRuleOtherCount > 0) {
logWarn(`表达式[${defVal}]不合法:填值规则表达式不能和其他表达式混用!`)
return false
}
return true
}
/** 获取所有匹配的表达式 */
function getRegExpMap(text, exp) {
let map = new Map()
if(text && text.length>0){
text.replace(exp, function (match, param, offset, string) {
map.set(match, param.trim())
return match
})
}
return map
}
/** 获取默认值,可以执行表达式,可以执行用户自定义方法,可以异步获取用户信息等 */
async function getDefaultValue(defVal, action, getFormData) {
// 只有在 add 和 reload 模式下才执行填值规则
if (action === ACTION_TYPES.ADD || action === ACTION_TYPES.RELOAD) {
// 判断是否是填值规则表达式,如果是就执行填值规则
if (fillRuleRegExp.test(defVal)) {
return await executeRegExp(defVal, fillRuleRegExp, executeFillRuleExpression, [getFormData])
}
}
// 只有在 add 模式下才执行其他表达式
if (action === ACTION_TYPES.ADD) {
// 获取并替换所有常规表达式
defVal = await executeRegExp(defVal, normalRegExp, executeNormalExpression)
// 获取并替换所有用户自定义表达式
defVal = await executeRegExp(defVal, customRegExp, executeCustomExpression)
return defVal
}
return null
}
async function executeRegExp(defVal, regExp, execFun, otherParams = []) {
let map = getRegExpMap(defVal, regExp)
// @ts-ignore
for (let origin of map.keys()) {
let exp = map.get(origin)
let result = await execFun.apply(null, [exp, origin, ...otherParams])
// 如果只有一个表达式那么就不替换因为一旦替换类型就会被转成String直接返回执行结果保证返回的类型不变
if (origin === defVal) {
return result
}
defVal = replaceAll(defVal, origin, result)
}
return defVal
}
/** 执行【普通表达式】#{xxx} */
async function executeNormalExpression(expression, origin) {
switch (expression) {
case 'date':
return dayjs().format('YYYY-MM-DD');
case 'time':
return dayjs().format('HH:mm:ss');
case 'datetime':
return dayjs().format('YYYY-MM-DD HH:mm:ss');
default:
// 获取当前登录用户的信息
let result = getUserInfoByExpression(expression)
if (result != null) {
return result
}
// 没有符合条件的表达式,返回原始值
return origin
}
}
/** 根据表达式获取相应的用户信息 */
function getUserInfoByExpression(expression) {
let userInfo:any = useUserStore().userInfo;
if (userInfo) {
switch (expression) {
case 'sysUserId':
return userInfo.id
// 当前登录用户登录账号
case 'sysUserCode':
return userInfo.username
// 当前登录用户真实名称
case 'sysUserName':
return userInfo.realname
// 当前登录用户部门编号
case 'sysOrgCode':
return userInfo.orgCode
}
}
return null
}
/**
* 2023-09-04
* liaozhiyang
* 用new Function替换eval
*/
function _eval(str: string) {
return new Function(`return ${str}`)();
}
/** 执行【用户自定义表达式】 {{xxx}} 移动端不支持 */
async function executeCustomExpression(expression, origin) {
// 利用 eval 生成一个方法,这个方法的参数就是用户自定义的所有的表达式
let fn = _eval(`(function (${ceJoin}){ return ${expression} })`);
try {
// 然后调用这个方法,并把表达式传递进去,从而完成表达式的执行
return fn.apply(null, $CE$)
} catch (e) {
// 执行失败,输出错误并返回原始值
logWarn(e)
return origin
}
}
/** 执行【填值规则表达式】 ${xxx} */
async function executeFillRuleExpression(expression, origin, getFormData) {
let url = `/sys/fillRule/executeRuleByCode/${expression}`
let formData = {}
if (typeof getFormData === 'function') {
formData = getFormData()
}
let res:any = await http.put(url, formData)
let { success, message, result } = res;
console.log(success, message, result)
if (success) {
return result
} else {
logError(`填值规则(${expression})执行失败:${message}`)
return origin
}
}
function logWarn(message) {
console.warn('[loadFieldDefVal]:', message)
}
function logError(message) {
console.error('[loadFieldDefVal]:', message)
}
function replaceAll(text, checker, replacer) {
let lastText = text
text = text.replace(checker, replacer)
if (lastText !== text) {
return replaceAll(text, checker, replacer)
}
return text
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,245 @@
<template>
<view class="online-sub-many-container">
<wd-button :disabled="componentDisabled" customClass="add" icon="add" size="small" @click="handleAdd">新增</wd-button>
<wd-table :data="dataList" height="400px">
<template v-for="(col,index) in columns" :key="index">
<wd-table-col :prop="col.key" :label="col.title" align="center" :fixed="col.key == 'action'">
<template #value="value" v-if="col.key == 'action'">
<view class="custom-action">
<wd-button size="small" type="icon" icon="edit" @click="handleEdit(value)"></wd-button>
<wd-button size="small" type="icon" icon="delete" style="color:rgba(255,0,0,0.5)" @click="handleDel(value)"></wd-button>
</view>
</template>
<template #value="{ row, index }" :key="index" v-else>
<online-sub-table-cell
:columnsInfo="columnsInfo"
:record="row"
:column="col"
></online-sub-table-cell>
</template>
</wd-table-col>
</template>
</wd-table>
</view>
<online-subform-popup
v-if="popupShow"
ref="subFormPop"
:title="tableTxt"
:schema="formSchema"
:disabled="disabled"
@close="handleClose"
@change="handleChange"
></online-subform-popup>
</template>
<script lang="ts" setup>
import {useMessage, useToast} from 'wot-design-uni';
import OnlineSubformPopup from './components/online-subform-popup.vue'
import OnlineSubTableCell from './components/online-sub-table-cell.vue'
import { http } from '@/utils/http'
import {deepClone} from "wot-design-uni/components/common/util";
defineOptions({
name: 'online-sub-many',
options: {
styleIsolation: 'shared',
},
})
// 接收 props
const props = defineProps({
tableInfo: {
type: Object,
required: true,
default: () => ({} as any)
},
dataInfo: {
type: Array,
required: false,
default: () => ([])
},
disabled: {
type: Boolean,
default: false,
required: false
},
edit: {
type: Boolean,
default: false,
required: false
},
showFooter: {
type: Boolean,
required: false,
default: true,
}
})
// 定义 emits
const emits = defineEmits(['back', 'success'])
//提示
const toast = useToast()
//pop弹窗
const subFormPop = ref(null)
//表ID
const tableId = ref('')
//表描述
const tableTxt = ref('')
//表名称
const tableName = ref('')
//列信息
const columns = ref([])
//表单配置信息
const formSchema = ref([])
//弹窗显示
const popupShow = ref(false)
//数据列表
const dataList = ref([])
//数据列表
const editIndex = ref(null)
//列信息
const columnsInfo = ref({})
/**
*
* @param item
* @returns {*|boolean}
*/
const componentDisabled = computed(()=>{
console.log("一对多many组件",props.disabled)
console.log("一对多many组件",props.showFooter)
if (props.disabled === true || !props.showFooter) {
return true
}
return false
})
//监听配置修改
watchEffect(()=>{
props.tableInfo && loadTableInfo(props.tableInfo)
})
/**
* 根据配置动态加载表单
* @param dataID
*/
function loadTableInfo(tableInfo: any){
console.log('===子表many加载表单数据 schema===', tableInfo)
formSchema.value = deepClone(tableInfo)
tableId.value = tableInfo.id;
tableName.value = tableInfo.key;
tableTxt.value = tableInfo.describe;
columns.value = tableInfo?.columns || [];
http
.get(`/online/cgform/api/getColumns/${tableId.value}`)
.then((res: any) => {
if (res.success) {
if (res.result?.columns?.length) {
columnsInfo.value = res.result;
}
}
})
let hasAction = columns.value.some(item=>item.key == 'action');
!hasAction && columns.value.unshift({title:'操作', key:'action'});
console.log("根据配置动态加载表单columns.value ",columns.value );
if (props.edit === true) {
initFormData(props.dataInfo)
}
}
/**
* 删除
* @param row
* @param index
*/
function handleDel({index}){
if(componentDisabled.value){
return;
}
uni.showModal({
title: '提示',
content: '确定要删除吗?',
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (res.confirm) {
dataList.value.splice(index,1);
toast.success('删除成功')
}
},
fail: (err) => {
console.log(err)
},
})
}
/**
* 新增
*/
function handleAdd(){
popupShow.value = true;
nextTick(()=>{
subFormPop.value.beforeOpen(null,false)
})
}
/**
* 编辑
* @param row
* @param index
*/
function handleEdit({row,index}){
if(!componentDisabled.value){
editIndex.value = index;
popupShow.value = true;
nextTick(()=>{
subFormPop.value.beforeOpen(row,true)
})
}
}
/**
* 关闭弹窗
* @param row
* @param index
*/
function handleClose(){
popupShow.value = false;
}
/**
* 表单内容修改
* @param row
* @param index
*/
function handleChange(record,isUpdate){
if(isUpdate){
dataList.value[unref(editIndex)] = record;
}else{
dataList.value.push(record);
}
}
/**
* 提交前处理
*/
function beforeSubmit(){
console.log("一对多beforeSubmit",dataList.value);
return {status:true,data:dataList.value}
}
/**
* 初始化表单数据
*/
function initFormData(data) {
if (data && data.length>0)
dataList.value = deepClone(data)
}
defineExpose({
beforeSubmit,
initFormData
})
</script>
<style lang="scss" scoped>
.online-sub-many-container {
min-height: 300px;
.custom-action{
display: flex;
}
:deep(.add) {
margin: 10px;
}
}
</style>

View File

@@ -0,0 +1,808 @@
<template>
<view class="online-sub-one-container">
<view class="form-container">
<wd-form ref="form" :model="formData">
<wd-cell-group border>
<view
class="onlineLoader-form"
v-for="(item, index) in rootProperties"
:key="index"
:class="{ 'mt-14px': index % 2 == 0, 'mt-2px': index == 0 }"
>
<!-- 图片 -->
<wd-cell
v-if="item.type == 'image'"
:name="item.key"
:title="get4Label(item.label)"
:title-width="labelWidth"
:required="fieldRequired(item)"
>
<online-image
v-model:value="formData[item.key]"
:name="item.key"
:disabled="componentDisabled(item)"
:key="index"
></online-image>
</wd-cell>
<!-- 文件 -->
<wd-cell
v-else-if="item.type == 'file'"
:name="item.key"
:title="get4Label(item.label)"
:title-width="labelWidth"
:required="fieldRequired(item)"
>
<view style="text-align: left">
<!-- #ifndef APP-PLUS -->
<online-file
v-model:value="formData[item.key]"
:name="item.key"
:disabled="componentDisabled(item)"
:key="index"
:maxNum="getFieldExtendJson(item, 'uploadnum')"
></online-file>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<online-file-custom
v-model:value="formData[item.key]"
:name="item.key"
:disabled="componentDisabled(item)"
:key="index"
:maxNum="getFieldExtendJson(item, 'uploadnum')"
></online-file-custom>
<!-- #endif -->
</view>
</wd-cell>
<!-- 日期时间 -->
<DateTime
v-else-if="item.type === 'datetime'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
startTime="1949-01-01 00:00:00"
endTime="2050-01-01 00:00:00"
:type="item.type"
:name="item.key"
format="YYYY-MM-DD HH:mm:ss"
:disabled="componentDisabled(item)"
v-model="formData[item.key]"
:required="fieldRequired(item)"
></DateTime>
<!-- 时间 -->
<DateTime
v-else-if="item.type === 'time'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
format="HH:mm:ss"
:type="item.type"
:name="item.key"
:disabled="componentDisabled(item)"
v-model="formData[item.key]"
:required="fieldRequired(item)"
></DateTime>
<!-- 日期 -->
<online-date
v-else-if="item.type === 'date'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:name="item.key"
:type="getDateExtendType(item.formSchema)"
:disabled="componentDisabled(item)"
v-model:value="formData[item.key]"
:required="fieldRequired(item)"
></online-date>
<!-- 时间 -->
<online-time
v-else-if="item.type === 'time'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:name="item.key"
:disabled="componentDisabled(item)"
v-model:value="formData[item.key]"
:required="fieldRequired(item)"
></online-time>
<!-- 下拉选择 -->
<online-select
v-else-if="item.type === 'list' || item.type === 'sel_search'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:name="item.key"
:type="item.type"
:dict="item.listSource"
:dictStr="item.dictStr"
:disabled="componentDisabled(item)"
v-model="formData[item.key]"
:required="fieldRequired(item)"
></online-select>
<!-- checkbox -->
<online-checkbox
v-else-if="item.type === 'checkbox'"
:name="item.key"
:type="item.type"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:dict="item.listSource"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></online-checkbox>
<!-- radio -->
<online-radio
v-else-if="item.type === 'radio'"
:name="item.key"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:type="item.type"
:dict="item.listSource"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></online-radio>
<!-- 下拉多选 -->
<online-multi
v-else-if="item.type === 'list_multi'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:name="item.key"
:dict="item.listSource"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></online-multi>
<!-- 省市区 -->
<online-pca
v-else-if="item.type === 'pca'"
:name="item.key"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model:value="formData[item.key]"
></online-pca>
<!-- 数字框 小数 -->
<wd-input
v-else-if="item.type === 'number' && (!item.onlyInteger || item.onlyInteger == false)"
:label-width="labelWidth"
v-model="formData[item.key]"
:label="get4Label(item.label)"
:name="item.key"
inputMode="decimal"
:disabled="componentDisabled(item)"
:placeholder="item.placeholder"
:rules="item.rules"
/>
<!-- 数字框 整数 -->
<wd-input
v-else-if="item.type === 'number' && item.onlyInteger === true"
:label-width="labelWidth"
:label="get4Label(item.label)"
:name="item.key"
v-model="formData[item.key]"
inputMode="numeric"
:disabled="componentDisabled(item)"
:placeholder="item.placeholder"
:rules="item.rules"
/>
<!-- 开关 -->
<wd-cell
v-else-if="item.type == 'switch'"
:name="item.key"
:title="get4Label(item.label)"
:title-width="labelWidth"
center
:required="fieldRequired(item)"
>
<view style="text-align: left">
<wd-switch
:label="get4Label(item.label)"
:name="item.key"
size="18px"
:disabled="componentDisabled(item)"
v-model="formData[item.key]"
:active-value="switchOpt(item.formSchema?.extendOption, 0)"
:inactive-value="switchOpt(item.formSchema?.extendOption, 1)"
/>
</view>
</wd-cell>
<!-- 多行文本 -->
<wd-textarea
v-else-if="['textarea', 'markdown', 'umeditor'].includes(item.type)"
:label-width="labelWidth"
:label="get4Label(item.label)"
:name="item.key"
v-model="formData[item.key]"
clearable
:maxlength="300"
:disabled="componentDisabled(item)"
:placeholder="item.placeholder"
:rules="item.rules"
/>
<!-- 密码输入框 -->
<wd-input
v-else-if="item.type === 'password'"
:label-width="labelWidth"
v-model="formData[item.key]"
:disabled="componentDisabled(item)"
:label="get4Label(item.label)"
:name="item.key"
:placeholder="item.placeholder"
:rules="item.rules"
show-password
/>
<!-- popup字典 -->
<PopupDict
v-else-if="item.type === 'popup_dict'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
:multi="item.formSchema.popupMulti"
:dictCode="`${item.formSchema.code},${item.formSchema['destFields']},${item.formSchema['orgFields']}`"
></PopupDict>
<!-- popup -->
<Popup
v-else-if="item.type === 'popup'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
:multi="item.formSchema.popupMulti"
:code="`${item.formSchema.code}`"
:setFieldsValue="setFieldsValue"
:fieldConfig="getPopupFieldConfig(item)"
></Popup>
<!-- 关联记录 -->
<online-popup-link-record
v-else-if="item.type === 'link_table'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:name="item.key"
v-model:formSchema="item.formSchema"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model:value="formData[item.key]"
@selected="linkRecordChange"
></online-popup-link-record>
<!-- 他表字段 -->
<wd-input
v-else-if="item.type === 'link_table_field'"
:label-width="labelWidth"
v-model="formData[item.key]"
:disabled="true"
:label="get4Label(item.label)"
:name="item.key"
/>
<!-- 用户选择 -->
<select-user
v-else-if="item.type === 'sel_user'"
:label-width="labelWidth"
:name="item.key"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></select-user>
<!-- 部门选择 -->
<select-dept
v-else-if="item.type === 'sel_depart'"
:label-width="labelWidth"
:name="item.key"
:label="get4Label(item.label)"
labelKey="departName"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></select-dept>
<!-- 分类字典树 -->
<CategorySelect
v-else-if="item.type === 'cat_tree'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
:pid="`${item.formSchema.pidValue}`"
></CategorySelect>
<!-- 自定义树 -->
<TreeSelect
v-else-if="item.type === 'sel_tree'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
:dict="`${item.formSchema.dict}`"
:pidField="`${item.formSchema.pidField}`"
:pidValue="`${item.formSchema.pidValue}`"
:hasChildField="`${item.formSchema.hasChildField}`"
></TreeSelect>
<!-- 普通输入框 -->
<wd-input
v-else-if="item.type !== 'hidden'"
:label-width="labelWidth"
v-model="formData[item.key]"
:disabled="componentDisabled(item)"
:label="get4Label(item.label)"
:name="item.key"
:placeholder="item.placeholder"
:rules="item.rules"
clearable
/>
</view>
</wd-cell-group>
</wd-form>
</view>
</view>
<wd-toast></wd-toast>
</template>
<script lang="ts" setup>
import FormProperty from './FormProperty'
import OnlineImage from '@/components/online/view/online-image.vue'
import OnlineFile from '@/components/online/view/online-file.vue'
import OnlineFileCustom from '@/components/online/view/online-file-custom.vue'
import OnlineSelect from '@/components/online/view/online-select.vue'
import OnlineTime from '@/components/online/view/online-time.vue'
import OnlineDate from '@/components/online/view/online-date.vue'
import OnlineRadio from '@/components/online/view/online-radio.vue'
import OnlineCheckbox from '@/components/online/view/online-checkbox.vue'
import OnlineMulti from '@/components/online/view/online-multi.vue'
import OnlinePca from './view/online-pca.vue'
import OnlinePopupLinkRecord from '@/components/online/view/online-popup-link-record.vue'
import SelectDept from '@/components/SelectDept/SelectDept.vue'
import SelectUser from '@/components/SelectUser/SelectUser.vue'
import { loadOneFieldDefVal } from './defaultVal'
import { useToast, useMessage, useNotify } from 'wot-design-uni'
import { deepClone } from 'wot-design-uni/components/common/util'
import { isArray, isNumber, isString } from '@/utils/is'
import { formatDate } from '@/common/uitls'
import { duplicateCheck } from '@/service/api'
defineOptions({
name: 'online-sub-one',
options: {
// apply-shared当前页面样式会影响到子组件样式.(小程序)
// shared当前页面样式影响到子组件子组件样式也会影响到当前页面.(小程序)
styleIsolation: 'shared',
},
})
// 接收 props
const props = defineProps({
tableInfo: {
type: Object,
required: true,
default: () => ({}) as any,
},
dataInfo: {
type: Array,
required: false,
default: () => [],
},
edit: {
type: Boolean,
default: false,
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
showFooter: {
type: Boolean,
required: false,
default: true,
},
})
// 定义 emits
const emits = defineEmits(['back', 'success'])
//定义提示
const toast = useToast()
//表描述
const tableTxt = ref('')
//表名称
const tableName = ref('')
//表数据Data
const formData = ref<any>({})
//是否填值规则字段
const hasFillRuleFields = ref('')
//是否必填字段
const hasRequiredFields = ref([])
//字段属性
const rootProperties = ref<any>([])
//表单数据ID
const formDataId = ref('')
//表单数据ID
const loaded = ref(false)
// 标题宽度
const labelWidth = computed(() => {
return '100px'
})
// 导航标题
const get4Label = computed(() => {
return (lable) => {
return `${lable && lable.length > 4 ? lable.substring(0, 4) : lable}`
}
})
/**
* 获取日期控件的扩展类型
* @param formSchema
* @returns {string}
*/
const getDateExtendType = (formSchema: any) => {
if (formSchema?.fieldExtendJson) {
let fieldExtendJson = JSON.parse(formSchema.fieldExtendJson)
let mapField = {
month: 'year-month',
year: 'year',
quarter: 'quarter',
week: 'week',
day: 'date',
}
return fieldExtendJson?.picker && mapField[fieldExtendJson?.picker]
? mapField[fieldExtendJson?.picker]
: 'date'
}
return 'date'
}
/**
* 判断是否选中
* @param opts
* @param value
* @returns {boolean|boolean}
*/
const isChecked = (opts: any, value: any) => {
return opts && opts.length > 0 ? value === opts[0] : false
}
/**
* 开关选项
* @param opts
* @param value
* @returns {boolean|boolean}
*/
const switchOpt = (opts: any, index: any) => {
const options = Array.isArray(opts) && opts.length > 0 ? opts : ['Y', 'N']
return options[index] + ''
}
/**
*
* @param item
* @returns {*|boolean}
*/
const componentDisabled = (item: any) => {
if (props.disabled === true || !props.showFooter) {
return true
}
return item.disabled
}
/**
* 判断字段是否必填
* @param item
* @returns {boolean}
*/
const fieldRequired = (item: any) => {
return item?.key && hasRequiredFields.value.includes(item.key)
}
/**
* 获取扩展字段
* @param item
* @param field
*/
const getFieldExtendJson = (item, field) => {
let json = item.formSchema.fieldExtendJson ?? '{}'
if (isString(json) && json.trim().length === 0) {
json = '{}'
}
json = JSON.parse(json)
const result = json[field]
return result
}
/**
* 关联记录同步修改他表字段
* @param linkRecord
* @param key
*/
const linkRecordChange = (linkRecord, key) => {
let linkFieldArr = rootProperties.value.filter(
(item) => item.type === 'link_table_field' && item?.formSchema?.dictTable == key,
)
linkFieldArr.forEach((field) => {
let value = linkRecord.map((record) => record[field.formSchema.dictText]).join(',')
nextTick(() => {
formData.value[field.key] = value
})
})
}
//监听配置修改
watchEffect(() => {
props.tableInfo && !loaded.value && loadTableInfo(props.tableInfo)
})
/**
* 省市区获取最后一位
* @param value
*/
function pcaValue(value) {
if (value.includes(',')) {
const parts = value.split(',')
if (parts.length >= 3) {
// 如果包含至少两个逗号,截取第三部分并去掉最后一位
return parts[2]
}
}
// 如果不包含逗号或不符合条件,返回原值
return value
}
/**
* 处理多选字段
* @param value
*/
function handleMultiOrDateField() {
let finalData = deepClone(formData.value)
//日期字段
let dateFieldArr = rootProperties.value.filter(
(item) => item.type === 'date' || item.type === 'datetime',
)
//省市区字段
let pcaArr = rootProperties.value.filter((item) => item.type === 'pca')
finalData = Object.keys(finalData).reduce((acc, key) => {
let value = finalData[key]
//省市区获取最后一位
if (value && pcaArr.length > 0 && pcaArr.map((item) => item.key).includes(key)) {
value = isArray(value) ? value[2] : pcaValue(value)
}
//是数组的就转换成字符串
if (value && isArray(value)) {
value = value.join(',')
}
//时间戳类型的日期转具体格式字符串
if (dateFieldArr.length > 0) {
const dateField = dateFieldArr.find((obj) => obj.key === key)
if (dateField) {
value =
value && isNumber(value)
? formatDate(
value,
dateField.type === 'datetime' ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd',
)
: value
}
}
acc[key] = value
return acc
}, {})
return finalData
}
/**
* 表单提交事件
* @param e
*/
const beforeSubmit = async () => {
// 判断字段必填和正则
if (await fieldCheck(formData.value)) {
return { status: false, data: [] }
}
// 处理多选字段
let finalData = await handleMultiOrDateField()
console.log('一对一beforeSubmit', finalData)
return { status: true, data: [finalData] }
}
/**
* 校验字段
* @param values
* @returns {boolean}
*/
const fieldCheck = async (values: any) => {
// 校验字段
let flag = false
for (const item of rootProperties.value) {
// 校验提示
const tip = (msg) => {
// 提示校验未通过
toast.warning(`${tableTxt.value}:${msg}`)
flag = true
}
// 校验必填
if (fieldRequired(item) && !values[item.key]) {
tip(`${item.label}不能为空!`)
break
}
// 校验正则
let pattern = item?.formSchema?.pattern
if (pattern) {
if (pattern == 'only') {
const res: any = await duplicateCheck({
tableName: tableName.value,
fieldName: item.key,
fieldVal: values[item.key],
dataId: formDataId.value,
})
if (!res.success) {
tip(`${item.label} ${res.message}`)
break
}
} else {
const regex = new RegExp(pattern)
if (values[item.key] && pattern && !regex.test(values[item.key])) {
let errorInfo = item?.formSchema?.errorInfo || '格式不正确!'
tip(`${item.label}${errorInfo}`)
break
}
}
}
}
return flag
}
/**
* 初始化表单数据
*/
function initFormData(data) {
console.log('一对一子表initFormData', data)
if (data && data.length > 0) {
formData.value = deepClone(data[0])
formDataId.value = formData.value?.id
}
}
/**
* 创建根属性
* @param formSchema
*/
function createRootProperties(formSchema: any) {
formData.value = {}
hasFillRuleFields.value = formSchema?.hasFillRuleFields ?? []
hasRequiredFields.value = formSchema?.required ?? []
const properties = formSchema.properties
let rootProps = []
console.log('===子表one配置项 properties===', properties)
Object.keys(properties).map((key) => {
if (key) {
const item = properties[key]
formData.value[key] = ''
let fp = FormProperty(key, item, formSchema.required)
rootProps.push(fp)
}
})
rootProps.sort((one, next) => {
return one.formSchema.order - next.formSchema.order
})
rootProperties.value = [...rootProps]
console.log('--子表one rootProperties--', rootProps)
}
/**
* 获取字段类型
* @param item
* @returns {string}
*/
const getFieldNumberType = (item: any) => {
return item.onlyInteger === true ? 'digit' : 'number'
}
/**
* 获取默认值
*/
function handleDefaultValue() {
rootProperties.value.forEach((item) => {
let field = item.key
let { defVal, type } = item.formSchema
loadOneFieldDefVal(defVal, type, (value) => {
formData.value[field] = value
})
})
}
/**
* 设置字段数值
* @param data
*/
const setFieldsValue = (data) => {
console.log('一对一子表popup设置值', data)
formData.value = { ...formData.value, ...data }
}
/**
* 获取popup字段配置项
* @param item
*/
const getPopupFieldConfig = (item) => {
const { formSchema } = item
const { destFields = '', orgFields = '' } = formSchema
const result = orgFields.split(',').map((oField, index) => {
return {
source: oField,
target: destFields.split(',')[index],
}
})
return result
}
/**
* 根据配置动态加载表单
* @param dataID
*/
function loadTableInfo(formSchema: any) {
console.log('===子表one加载表单数据 schema===', formSchema)
createRootProperties(formSchema)
tableTxt.value = formSchema?.describe
if (props.edit === true) {
initFormData(props.dataInfo)
} else {
// 新增页面处理表单默认值
handleDefaultValue()
}
loaded.value = true
}
defineExpose({
beforeSubmit,
initFormData,
loadTableInfo,
})
</script>
<style lang="scss" scoped>
.online-sub-one-container {
.form-container {
:deep(.wd-cell-group__body) {
background-color: #f1f1f1;
}
.onlineLoader-form {
:deep(.wd-input__label-inner) {
font-size: 16px;
}
:deep(.wd-picker__label) {
font-size: 16px;
}
:deep(.wd-select-picker__label) {
font-size: 16px;
}
:deep(.wd-cell__title) {
font-size: 16px;
}
:deep(.wd-textarea__label-inner) {
font-size: 16px;
}
:deep(.wd-input__label.is-required) {
padding-left: 0px;
}
:deep(.wd-input__label.is-required::after) {
left: -10px;
}
:deep(.wd-textarea__clear) {
color: #bfbfbf;
}
:deep(.wd-select-picker__clear) {
color: #bfbfbf;
}
:deep(.wd-input__clear) {
color: #bfbfbf;
}
:deep(.wd-upload__close) {
color: #bfbfbf;
}
}
}
.footer {
padding: 12px;
}
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<wd-input
v-bind="$attrs"
:label-width="labelWidth"
v-model="currentText"
:disabled="disabled"
:label="label"
:inputMode="inputMode"
:placeholder="placeholder"
@input="debouncedInput"
/>
</template>
<script lang="ts" setup>
import { debounce } from 'lodash-es'
const props = defineProps({
label: {
type: String,
default: '',
required: false,
},
labelWidth: {
type: String,
default: '80px',
required: false,
},
inputMode: {
type: String,
default: 'text',
required: false,
},
placeholder: {
type: String,
default: '',
required: false,
},
value: {
type: [String, Number],
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
}
})
// 定义 emits
const emit = defineEmits(['input', 'update:value'])
// 定义响应式数据;
const currentText = ref(null)
// 监听 value 的变化
watch(
() => props.value,
(val) => {
currentText.value = val;
},
{
immediate:true
}
)
// 更新 value 的方法
const updateValue = () => {
emit('update:value', currentText.value)
}
// 创建防抖函数
const debouncedInput = debounce(updateValue, 1000)
// 记得在组件卸载时取消防抖
onBeforeUnmount(() => {
debouncedInput.cancel()
})
</script>
<style></style>

View File

@@ -0,0 +1,345 @@
<template>
<wd-popup position="bottom" v-model="show">
<PageLayout
:navTitle="navTitle"
type="popup"
navRightText="确定"
@navRight="handleConfirm"
@navBack="handleCancel"
>
<view class="wrap">
<z-paging
ref="paging"
:fixed="false"
v-model="dataList"
@query="queryList"
:default-page-size="15"
>
<!-- <template #top>-->
<!-- <wd-search-->
<!-- hide-cancel-->
<!-- :placeholder="search.placeholder"-->
<!-- v-model="search.keyword"-->
<!-- @search="handleSearch"-->
<!-- @clear="handleClear"-->
<!-- />-->
<!-- </template>-->
<template v-if="multi">
<wd-checkbox-group shape="square" v-model="checkedValue">
<template v-for="(item, index) in dataList" :key="index">
<view class="list" @click="hanldeCheck(index, item)">
<view class="left text-gray-5">
<view class="cu-avatar lg mr-4" v-if="imageField" :style="[{ backgroundImage: 'url(' + getImage(item[imageField] )+ ')' }]"></view>
<view class="field-content">
<template v-for="(cItem, cIndex) in columns" :key="cIndex">
<view class="row">
<text class="label">{{ cItem.title }}</text>
<text class="value">{{ item[cItem.dataIndex] }}</text>
</view>
</template>
</view>
</view>
<view class="right" @click.stop>
<wd-checkbox ref="checkboxRef" :modelValue="item.id"></wd-checkbox>
</view>
</view>
</template>
</wd-checkbox-group>
</template>
<template v-else>
<wd-radio-group shape="dot" v-model="checkedValue">
<template v-for="(item, index) in dataList" :key="index">
<wd-cell>
<view class="list" @click="hanldeCheck(index, item)">
<view class="left text-gray-5">
<view
class="cu-avatar lg mr-4"
v-if="imageField && item[imageField]"
:style="[{ backgroundImage: 'url(' + item[imageField] + ')' }]"
></view>
<view class="field-content">
<template v-for="(cItem, cIndex) in columns" :key="cIndex">
<view class="row">
<text class="label">{{ cItem.title }}</text>
<text class="value">{{ item[cItem.dataIndex] }}</text>
</view>
</template>
</view>
</view>
<view class="right" @click.stop>
<wd-radio :value="item.id"></wd-radio>
</view>
</view>
</wd-cell>
</template>
</wd-radio-group>
</template>
</z-paging>
</view>
</PageLayout>
</wd-popup>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import { isArray, isString } from '@/utils/is'
import { getFileAccessHttpUrl } from '@/common/uitls'
defineOptions({
name: 'popupReportModal',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
selected: {
type: String,
default: '',
},
dictTable: {
type: String,
required: true,
},
dictCode: {
type: String,
required: true,
},
dictText: {
type: String,
required: true,
},
multi: {
type: Boolean,
required: false,
},
imageField: {
type: String,
required: false,
},
})
const emit = defineEmits(['change', 'close'])
const toast = useToast()
const show = ref(true)
const api = {
getColumns: '/online/cgform/api/getColumns',
getData: '/online/cgform/api/getData',
}
console.log('props:::', props)
const navTitle = ref('')
const paging = ref(null)
const dataList = ref([])
// 报表id
let rpConfigId = null
let loadedColumns = false
const columns = ref([])
const selectArr = ref([])
const checkedValue: any = ref(props.multi ? [] : '')
const checkboxRef = ref(null)
const search = reactive({
keyword: '',
placeholder: '',
field: '',
})
const handleClose = () => {
setTimeout(() => {
emit('close')
}, 400)
}
const beforeOpen = (arr) => {
selectArr.value = arr || []
}
const handleConfirm = () => {
if (checkedValue.value.length == 0) {
toast.warning('还没选择~')
return
}
const result = []
let value = checkedValue.value
if (!Array.isArray(checkedValue.value)) {
value = [checkedValue.value]
}
value.forEach((id, index) => {
const findIndex = dataList.value.findIndex((item) => item.id === id)
result.push(dataList.value[findIndex])
})
show.value = false
emit('change', result)
handleClose()
}
const handleCancel = () => {
show.value = false
handleClose()
console.log('取消了~')
}
// 搜索
function handleSearch() {
paging.value.reload()
}
// 图片
function getImage(src) {
return src?src:'';
}
// 清除搜索条件
function handleClear() {
search.keyword = ''
handleSearch()
}
const hanldeCheck = (index, item) => {
if (props.multi) {
if (Array.isArray(checkboxRef.value)) {
checkboxRef.value[index].toggle()
}
} else {
checkedValue.value = item.id
}
}
const getRpColumns = () => {
return new Promise<void>((resolve, reject) => {
if (loadedColumns) {
resolve()
} else {
let linkTableSelectFields = props.dictCode + ',' + props.dictText
http
.get(`${api.getColumns}/${props.dictTable}?linkTableSelectFields=${linkTableSelectFields}`)
.then((res: any) => {
if (res.success) {
loadedColumns = true
const { result } = res
navTitle.value = result.description
rpConfigId = result.code
result.columns?.forEach((item) => {
if (linkTableSelectFields.includes(item.dataIndex)) {
columns.value.push(item)
}
})
resolve()
} else {
reject()
}
})
.catch((err) => {
reject()
})
}
})
}
const queryList = (pageNo, pageSize) => {
const pararms = { pageNo, pageSize, linkTableSelectFields: '' }
if (search.keyword) {
pararms[search.field] = `*${search.keyword}*`
}
getRpColumns()
.then(() => {
let linkTableSelectFields = props.dictCode + ',' + props.dictText
if (props.imageField) {
linkTableSelectFields = linkTableSelectFields + ',' + props.imageField
}
pararms.linkTableSelectFields = linkTableSelectFields
http
.get(`${api.getData}/${props.dictTable}`, pararms)
.then((res: any) => {
if (res.success && res.result.records) {
let dataRecords = res.result.records
if (dataRecords && dataRecords.length > 0) {
let id = dataRecords[0]['id']
for (let item of dataRecords) {
if (!id) {
item.id = new Date().getTime()
}
if (props.imageField && item[props.imageField]) {
let imgUrlArr = item[props.imageField].split(',')
item[props.imageField] =
imgUrlArr.length > 0 ? getFileAccessHttpUrl(imgUrlArr[0]) : ''
}
}
}
//TODO
if (selectArr.value && isArray(selectArr) && selectArr.length > 0) {
//checkedValue.value = [...selectArr]
}
paging.value.complete(dataRecords ?? [])
} else {
paging.value.complete(false)
}
})
.catch((err) => {})
})
.catch((err) => {})
}
const init = () => {
if (props.selected.length) {
if (props.multi) {
if (isArray(props.selected)) {
checkedValue.value = props.selected
} else if (isString(props.selected)) {
checkedValue.value = props.selected.split(',')
}
} else {
if (isString(props.selected)) {
checkedValue.value = props.selected
} else if (isArray(props.selected)) {
// @ts-ignore
checkedValue.value = props.selected.join(',')
}
}
}
}
init()
defineExpose({
beforeOpen,
})
</script>
<style lang="scss" scoped>
.wrap {
height: 100%;
.mr-4 {
margin-right: 10px;
}
}
:deep(.wd-cell) {
--wot-color-white: tranparent;
--wot-cell-padding: 0;
.wd-cell__wrapper {
--wot-cell-wrapper-padding: 0;
}
.wd-cell__left {
display: none;
}
}
:deep(.wd-checkbox-group) {
--wot-checkbox-bg: tranparent;
}
:deep(.wd-radio-group) {
--wot-radio-bg: tranparent;
}
.list {
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
padding: 16px;
margin-top: 16px;
.left {
display: flex;
justify-content: center;
align-items: center;
.field-content {
.row {
display: flex;
}
}
}
.right {
:deep(.wd-checkbox) {
margin-bottom: 0;
}
}
}
</style>

View File

@@ -0,0 +1,145 @@
<template>
<wd-select-picker
:label-width="labelWidth"
:label="label"
v-model="selected"
filterable
clearable
:columns="options"
:disabled="disabled"
placeholder="请选择"
@change="handleChange"
></wd-select-picker>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue';
import { isArray, isString } from 'lodash';
import {http} from "@/utils/http"; // 假设使用 lodash 来判断类型
// 定义 props
const props = defineProps({
dict: {
type: [Array, String],
default: () => [],
required: true,
},
label: {
type: String,
default: '',
required: false,
},
labelWidth: {
type: String,
default: '80px',
required: false,
},
name: {
type: String,
default: '',
required: false,
},
dictStr: {
type: String,
default: '',
required: false,
},
type: {
type: String,
default: '',
required: false,
},
modelValue: {
type: [Array, String],
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
})
// 定义 emits
const emit = defineEmits(['change', 'update:modelValue'])
// 定义响应式数据
const selected = ref([]);
const options = ref([]);
// 初始化选项
const initSelections = async () => {
options.value = []
if (props.type === 'sel_search' && props.dictStr) {
let temp = props.dictStr
if (temp.indexOf(' ') > 0) {
temp = encodeURI(props.dictStr)
}
try {
const res = await http.get('/sys/dict/getDictItems/' + temp)
if (res.success) {
options.value = res.result
}
} catch (error) {
console.error('请求数据出错:', error)
}
}
else {
if (!props.dict || props.dict.length === 0) {
return
}
if (isString(props.dict)) {
try {
let code = props.dict;
if (code.indexOf(',') > 0 && code.indexOf(' ') > 0) {
// 编码后类似sys_user%20where%20username%20like%20xxx' 是不包含空格的,这里判断如果有空格和逗号说明需要编码处理
code = encodeURI(code);
}
const res = await http.get('/sys/dict/getDictItems/' + code)
if (res.success) {
options.value = res.result
}
} catch (error) {
console.error('请求数据出错:', error)
}
} else {
props.dict.forEach((item) => {
options.value.push(item)
})
}
}
}
// 选择器改变事件处理函数
const handleChange = ({value}) => {
let valueStr='';
if (value && isArray(value)) {
valueStr = value.join(',')
}
emit('update:modelValue', valueStr);
emit('change', valueStr);
}
// 监听 dict 和 value 的变化
watch(() => props.dict, () => {
initSelections();
});
// 监听value 的变化
watchEffect(()=>{
props.modelValue && initVal()
})
//初始化数值
function initVal(){
selected.value = isString(props.modelValue)? props.modelValue.split(',') :[];
}
// 组件挂载时初始化选项
onMounted(() => {
initSelections()
})
</script>
<style lang="scss" scoped>
:deep(.wd-checkbox__shape){
border-radius:0 !important;
}
</style>

View File

@@ -0,0 +1,178 @@
<template>
<view class="pickerArea" :class="{ clear: !!currentTime }">
<!-- 周视图 -->
<wd-calendar
v-if="type == 'week'"
v-model="currentTime"
type="week"
:disabled="disabled"
:default-value="defaultValue"
:labelWidth="labelWidth"
:label="label"
@confirm="handleConfirm" />
<!-- 季度视图 -->
<wd-picker
v-else-if="type == 'quarter'"
v-model="currentQuarter"
:disabled="disabled"
:labelWidth="labelWidth"
:label="label"
:columns="quarterColumns"
@open="quarterOpen"
@confirm="quarterConfirm"/>
<!-- 其他视图 -->
<wd-datetime-picker
v-else
:disabled="disabled"
:minDate="-662716800000"
:maxDate="4102416000000"
:default-value="defaultValue"
:type="type"
:labelWidth="labelWidth"
v-model="currentTime"
:label="label"
@confirm="handleConfirm"
/>
<view v-if="!!currentTime && !disabled" class="u-iconfont u-icon-close" @click.stop="handleClear"></view>
</view>
</template>
<script setup>
// 定义 props
import { isString } from '@/utils/is'
import dayjs from 'dayjs'
import {dateToQuarterStart} from "@/common/uitls";
defineOptions({
name: 'online-date',
options: {
styleIsolation: 'shared',
},
})
//props定义
const props = defineProps({
label: {
type: String,
default: '',
required: false,
},
labelWidth: {
type: String,
default: '80px',
required: false,
},
name: {
type: String,
default: '',
required: false,
},
type: {
type: String,
default: 'date',
required: false,
},
value: {
type: [String, Number, Date],
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
})
// 定义 emits
const emit = defineEmits(['input', 'change', 'update:value'])
// 定义响应式数据;
const currentQuarter = ref('');
// 定义响应式数据;
const currentTime = ref(props.type == 'week' ? null : '');
// 默認
const defaultValue = ref(Date.now())
// 季度选择器列
const quarterColumns = ref([])
// 监听 value 的变化
watch(
() => props.value,
(val) => {
if (val) {
if (props.type == 'quarter' && quarterColumns.value.length == 0) {
generateQuarterOptions()
}
initVal(val)
}
},
{ immediate: true },
)
// 初始化值
function initVal(val){
if (props.type == 'quarter') {
currentQuarter.value = dateToQuarterStart(val);
}else{
if (typeof val == 'object') {
currentTime.value = dayjs(val).valueOf();
} else {
currentTime.value = isString(val) ? new Date(val).getTime() : val;
}
}
}
// 选择器改变事件处理函数
const handleConfirm = (e) => {
emit('update:value', currentTime.value)
emit('change', currentTime.value)
}
// 选择器改变事件处理函数
const quarterOpen = () => {
!quarterColumns.value.length && generateQuarterOptions();
if(!currentQuarter.value) {
currentQuarter.value = dateToQuarterStart(new Date());
}
}
// 选择器改变事件处理函数
const quarterConfirm = (e) => {
emit('update:value', currentQuarter.value)
emit('change', currentQuarter.value)
}
// 清空
const handleClear = () => {
currentTime.value = null
handleConfirm(null)
}
// 生成季度选择器列
function generateQuarterOptions(){
const options = [];
const currentYear = new Date().getFullYear();
const startYear = currentYear - 40;
const endYear = currentYear + 40;
const years = Array.from({ length: endYear - startYear + 1 }, (_, i) => endYear - i).reverse();
years.forEach(year => {
options.push({label: `${year}年 Q1`, value: `${year}-01-01`})
options.push({label: `${year}年 Q2`, value: `${year}-04-01`})
options.push({label: `${year}年 Q3`, value: `${year}-07-01`})
options.push({label: `${year}年 Q4`, value: `${year}-10-01`})
})
quarterColumns.value = [...options];
}
</script>
<style lang="scss" scoped>
.pickerArea {
position: relative;
.u-icon-close {
position: absolute;
right: 15px;
top: calc(10px + 4px);
color: #585858;
font-size: 15px;
}
&.clear {
:deep(.wd-picker__arrow) {
display: none;
}
}
}
</style>

View File

@@ -0,0 +1,239 @@
<template>
<view>
<template v-if="fileList && fileList.length > 0">
<view class="cu-list menu-avatar ol-file-list">
<view
class="cu-item"
:class="modalName == 'move-box-' + index ? 'move-cur' : ''"
v-for="(item, index) in fileList"
:key="index"
@touchstart="ListTouchStart"
@touchmove="ListTouchMove"
@touchend="ListTouchEnd"
:data-target="'move-box-' + index"
>
<view class="flex align-center">
<text
class="mr-4px text-blue"
style="font-size: 18px"
:class="fileType(item.name) ? 'cuIcon-text' : 'cuIcon-pic'"
></text>
{{ item.name }}
</view>
<view class="move">
<view @tap="DelFile" class="bg-red" :data-index="index">删除</view>
</view>
</view>
</view>
</template>
<view class="cu-bar bg-white">
<wd-button @tap="ChooseFile" size="small" :disabled="disabled" plain type="primary">
<text class="cuIcon-upload"></text>
上传
</wd-button>
</view>
<LFile ref="customFile" @up-success="onSuccess"></LFile>
</view>
</template>
<script lang="ts" setup>
import { ref, watch, onMounted } from 'vue';
import LFile from '@/components/LFile/LFile.vue';
import { useUserStore } from '@/store';
import { getEnvBaseUrl } from '@/utils';
import { useToast } from 'wot-design-uni';
import { getFileAccessHttpUrl } from '@/common/uitls';
//图片类型
const imgTypeArr = ['png', 'jpg', 'jpeg', 'bmp']
const userStore = useUserStore()
const props = defineProps({
title: {
type: String,
default: '',
required: false,
},
value: {
type: String,
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
name: {
type: String,
default: '',
required: false,
},
maxNum: {
type: Number,
default: null,
},
})
// 事件
const emits = defineEmits(['change', 'input', 'update:value'])
// 提示
const toast = useToast()
// 组件
const customFile = ref(null)
// 文件数据
const fileList = ref([])
// token
const token = ref(userStore.userInfo.token)
const modalName = ref(null)
const listTouchStart = ref(0)
const listTouchDirection = ref(null)
// ListTouch触摸开始
const ListTouchStart = (e) => {
listTouchStart.value = e.touches[0].pageX
}
// ListTouch计算方向
const ListTouchMove = (e) => {
listTouchDirection.value = e.touches[0].pageX - listTouchStart.value > 0 ? 'right' : 'left'
}
// ListTouch计算滚动
const ListTouchEnd = (e) => {
if (listTouchDirection.value == 'left') {
modalName.value = e.currentTarget.dataset.target
} else {
modalName.value = null
}
listTouchDirection.value = null
}
/**
* 选择文件
* @constructor
*/
const ChooseFile = () => {
if (props.maxNum && fileList.value.length >= props.maxNum) {
toast.warning('最多只能上传' + props.maxNum + '个文件')
return
}
customFile.value.upload({
// #ifdef APP-PLUS
currentWebview: getCurrentPages()[getCurrentPages().length - 1].$getAppWebview(),
// #endif
header: {
'X-Access-Token': token.value,
},
url: `${getEnvBaseUrl()}/sys/common/upload`,
})
}
const onSuccess = (res) => {
console.log('这是上传成功返回数值res:', res)
let fileObj = res.data
// #ifdef APP-PLUS
fileObj = JSON.parse(res.data.id)
// #endif
if (fileObj.success) {
let file = {
name: res.fileName,
path: fileObj.message,
url: getFileAccessHttpUrl(fileObj.message),
}
fileList.value.unshift(file)
changeOnlineFormValue()
}
}
// 获取文件路径
const pathArr = (arg) => {
return fileList.value.map((item) => item[arg])
}
// 改变表单值
const changeOnlineFormValue = () => {
let arr = pathArr('path')
let str = arr.join(',')
emits('change', str)
emits('input', str)
emits('update:value', str)
}
const DelFile = (e) => {
uni.showModal({
title: '提示',
content: '确定要删除吗?',
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (res.confirm) {
fileList.value.splice(e.currentTarget.dataset.index, 1)
changeOnlineFormValue()
}
},
})
}
const loadFile = () => {
if (!props.value || props.value.length == 0) {
return
}
let pathArr = props.value.split(',')
let fileArray = []
pathArr.map((path) => {
let seg = path.lastIndexOf('/')
if (seg < 0) {
seg = 0
}
fileArray.push({
name: formatPath(seg == 0 ? path : path.substr(seg + 1)),
path: path,
url: getFileAccessHttpUrl(path),
})
})
fileList.value = [...fileArray]
}
const formatPath = (path) => {
let seg = path.lastIndexOf('.')
if (seg < 0) {
seg = 0
}
let pathSuffix = path.substr(seg)
if (path.length > 20) {
return path.substring(0, 15) + '(..)' + pathSuffix
} else {
return path
}
}
const fileType = (name) => {
let seg = name.lastIndexOf('.')
if (seg < 0) {
seg = 0
}
let pathSuffix = name.substr(seg + 1)
return imgTypeArr.indexOf(pathSuffix) == -1
}
watch(
() => props.value,
() => {
loadFile()
},
{ immediate: true },
)
</script>
<style scoped>
.ol-file-block {
display: block;
}
.ol-file-block .action {
-webkit-justify-content: flex-start;
justify-content: flex-start;
height: 54px;
line-height: 54px;
color: #333333;
}
.ol-file-list > .cu-item {
-webkit-justify-content: flex-start;
justify-content: flex-start;
height: 45px;
}
</style>

View File

@@ -0,0 +1,171 @@
<template>
<wd-upload
v-model:file-list="fileList"
accept="all"
:upload-method="customUpload"
:disabled="disabled"
:limit="maxNum"
:before-remove="delFile"
:multiple="maxNum && maxNum > 1"
></wd-upload>
</template>
<script lang="ts" setup>
import type { UploadMethod } from 'wot-design-uni/components/wd-upload/types'
import { getEnvBaseUploadUrl,getEnvBaseUrl } from '@/utils'
import { useUserStore } from '@/store'
import { getFileAccessHttpUrl } from '@/common/uitls'
import { isString } from '@/utils/is'
import { useToast } from 'wot-design-uni'
const toast = useToast()
const VITE_UPLOAD_BASEURL = `${getEnvBaseUrl()}/sys/common/upload`
// 接收 props
const props = defineProps({
title: {
type: String,
default: '',
required: false,
},
value: {
type: String,
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
name: {
type: String,
default: '',
required: false,
},
maxNum: {
type: Number,
default: null
},
uploadFileType: {
type: String,
default: 'image',
required: false,
},
})
// 定义 emits
const emit = defineEmits(['change', 'update:value'])
// 定义响应式数据
const fileList = ref([])
/**
* 自定义上传方法
* @param file
* @param formData
* @param options
*/
const customUpload: UploadMethod = (file, formData, options) => {
const userStore = useUserStore()
const uploadTask = uni.uploadFile({
url: VITE_UPLOAD_BASEURL,
header: {
'X-Access-Token': userStore.userInfo.token,
'X-Tenant-Id': userStore.userInfo.tenantId,
...options.header,
},
name: options.name,
fileName: options.name,
fileType: options.fileType,
formData,
filePath: file.url,
success(res: any) {
if (res.statusCode === options.statusCode) {
let data = res.data
if (data && isString(data)) {
data = JSON.parse(data)
}
// 设置上传成功
if (data && data.success) {
const file = {
id: new Date().getTime(),
name: options.name,
path: data.message,
url: getFileAccessHttpUrl(data.message),
}
fileList.value.unshift(file)
changeOnlineFormValue()
}
} else {
// 设置上传失败
options.onError({ ...res, errMsg: res.errMsg || '' }, file, formData)
}
},
fail(err) {
console.info('upload fail', err)
// 设置上传失败
options.onError(err, file, formData)
},
})
// 设置当前文件加载的百分比
uploadTask.onProgressUpdate((res) => {
options.onProgress(res, file)
})
}
const changeOnlineFormValue = () => {
console.log('changeOnlineFormValue fileList.value', fileList)
const arr = fileList.value.map((item) => item['path'])
const str = arr.join(',')
emit('change', str)
emit('update:value', str)
}
const delFile = ({ file, fileList, resolve }) => {
uni.showModal({
title: '提示',
content: '确定要删除吗?',
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (res.confirm) {
console.log('当前删除文件', file)
changeOnlineFormValue()
toast.success('删除成功')
resolve(true)
}
},
fail: (err) => {
console.log(err)
resolve(false)
},
})
}
const loadFile = () => {
if (!props.value || props.value.length === 0) {
return
}
const pathArr = props.value.split(',')
const fileArray = []
pathArr.forEach((path) => {
const seg = path.lastIndexOf('/')
fileArray.push({
name: path.substr(seg < 0 ? 0 : seg),
path: path,
url: getFileAccessHttpUrl(path),
})
})
console.log('当前图片回显数据', fileArray)
fileList.value = [...fileArray]
}
// 监听 value 的变化
watch(
() => props.value,
() => {
loadFile()
},
{ immediate: true },
)
// 组件挂载时加载文件
onMounted(() => {
loadFile()
})
</script>

View File

@@ -0,0 +1,171 @@
<template>
<wd-upload
v-model:file-list="fileList"
accept="image"
:upload-method="customUpload"
:disabled="disabled"
:limit="maxNum"
:before-remove="delFile"
:multiple="maxNum && maxNum > 1"
></wd-upload>
</template>
<script lang="ts" setup>
import type { UploadMethod } from 'wot-design-uni/components/wd-upload/types'
import { getEnvBaseUploadUrl, getEnvBaseUrl } from '@/utils'
import { useUserStore } from '@/store'
import { getFileAccessHttpUrl } from '@/common/uitls'
import { isString } from '@/utils/is'
import { useToast } from 'wot-design-uni'
const toast = useToast()
const VITE_UPLOAD_BASEURL = `${getEnvBaseUrl()}/sys/common/upload`
// 接收 props
const props = defineProps({
title: {
type: String,
default: '',
required: false,
},
value: {
type: String,
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
name: {
type: String,
default: '',
required: false,
},
maxNum: {
type: Number,
default: null,
},
uploadFileType: {
type: String,
default: 'image',
required: false,
},
})
// 定义 emits
const emit = defineEmits(['change', 'update:value'])
// 定义响应式数据
const fileList = ref([])
/**
* 自定义上传方法
* @param file
* @param formData
* @param options
*/
const customUpload: UploadMethod = (file, formData, options) => {
const userStore = useUserStore()
const uploadTask = uni.uploadFile({
url: VITE_UPLOAD_BASEURL,
header: {
'X-Access-Token': userStore.userInfo.token,
'X-Tenant-Id': userStore.userInfo.tenantId,
...options.header,
},
name: options.name,
fileName: options.name,
fileType: options.fileType,
formData,
filePath: file.url,
success(res: any) {
if (res.statusCode === options.statusCode) {
let data = res.data
if (data && isString(data)) {
data = JSON.parse(data)
}
// 设置上传成功
if (data && data.success) {
const file = {
id: new Date().getTime(),
name: options.name,
path: data.message,
url: getFileAccessHttpUrl(data.message),
}
fileList.value.unshift(file)
changeOnlineFormValue()
}
} else {
// 设置上传失败
options.onError({ ...res, errMsg: res.errMsg || '' }, file, formData)
}
},
fail(err) {
console.info('upload fail', err)
// 设置上传失败
options.onError(err, file, formData)
},
})
// 设置当前文件加载的百分比
uploadTask.onProgressUpdate((res) => {
options.onProgress(res, file)
})
}
const changeOnlineFormValue = () => {
console.log('changeOnlineFormValue fileList.value', fileList)
const arr = fileList.value.map((item) => item['path'])
const str = arr.join(',')
emit('change', str)
emit('update:value', str)
}
const delFile = ({ file, fileList, resolve }) => {
uni.showModal({
title: '提示',
content: '确定要删除吗?',
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (res.confirm) {
console.log('当前删除文件', file)
changeOnlineFormValue()
toast.success('删除成功')
resolve(true)
}
},
fail: (err) => {
console.log(err)
resolve(false)
},
})
}
const loadFile = () => {
if (!props.value || props.value.length === 0) {
return
}
const pathArr = props.value.split(',')
const fileArray = []
pathArr.forEach((path) => {
const seg = path.lastIndexOf('/')
fileArray.push({
name: path.substr(seg < 0 ? 0 : seg),
path: path,
url: getFileAccessHttpUrl(path),
})
})
console.log('当前图片回显数据', fileArray)
fileList.value = [...fileArray]
}
// 监听 value 的变化
watch(
() => props.value,
() => {
loadFile()
},
{ immediate: true },
)
// 组件挂载时加载文件
onMounted(() => {
loadFile()
})
</script>

View File

@@ -0,0 +1,150 @@
<template>
<wd-select-picker
:label-width="labelWidth"
:label="label"
v-model="selected"
filterable
clearable
:columns="options"
:disabled="disabled"
placeholder="请选择"
@change="handleChange"
></wd-select-picker>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { isArray, isString } from 'lodash'
import {http} from "@/utils/http";
// 定义 props
const props = defineProps({
dict: {
type: [Array, String],
default: () => [],
required: true,
},
label: {
type: String,
default: '',
required: false,
},
labelWidth: {
type: String,
default: '80px',
required: false,
},
name: {
type: String,
default: '',
required: false,
},
dictStr: {
type: String,
default: '',
required: false,
},
type: {
type: String,
default: '',
required: false,
},
modelValue: {
type: [Array, String],
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
})
// 定义 emits
const emit = defineEmits([ 'change', 'update:modelValue'])
// 定义响应式数据
const selected = ref([]);
const options = ref([]);
// 初始化选项
const initSelections = async () => {
options.value = []
if (props.type === 'sel_search' && props.dictStr) {
let temp = props.dictStr
if (temp.indexOf(' ') > 0) {
temp = encodeURI(props.dictStr)
}
try {
const res = await http.get('/sys/dict/getDictItems/' + temp)
if (res.success) {
options.value = res.result
}
} catch (error) {
console.error('请求数据出错:', error)
}
}
else {
if (!props.dict || props.dict.length === 0) {
return
}
if (isString(props.dict)) {
try {
let code = props.dict;
if (code.indexOf(',') > 0 && code.indexOf(' ') > 0) {
// 编码后类似sys_user%20where%20username%20like%20xxx' 是不包含空格的,这里判断如果有空格和逗号说明需要编码处理
code = encodeURI(code);
}
const res = await http.get('/sys/dict/getDictItems/' + code)
if (res.success) {
options.value = res.result
}
} catch (error) {
console.error('请求数据出错:', error)
}
} else {
props.dict.forEach((item) => {
options.value.push(item)
})
}
}
console.log("options.value ",options.value )
}
// 选择器改变事件处理函数
const handleChange = ({value}) => {
console.log("下拉多选handleChange",value);
let valueStr = "";
if (value && isArray(value)) {
valueStr = value.join(',');
}
emit('update:modelValue', valueStr);
emit('change', valueStr);
}
// 监听 dict 和 value 的变化
watch(() => props.dict, () => {
initSelections();
});
// 监听 value 的变化
watch(
() => props.modelValue,
() => {
if(props?.modelValue){
selected.value = isString(props.modelValue)? props.modelValue.split(','):[];
}else{
selected.value = []
}
},
{ deep: true, immediate: true }
)
// 组件挂载时初始化选项
onMounted(() => {
initSelections()
})
</script>
<style lang="scss" scoped>
:deep(.wd-checkbox__shape){
border-radius:0 !important;
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<view>
<wd-picker
:columns="columns"
:label-width="labelWidth"
:label="label"
:required="required"
:disabled="disabled"
:placeholder="placeholder"
v-model="selected"
:column-change="onChangeDistrict"
@confirm="handleConfirm"
/>
</view>
</template>
<script lang="ts" setup>
import { getAreaArrByCode,getPcaOptionData } from '@/common/areaData/Area'
import {isString} from "@/utils/is";
// 接收 props
const props = defineProps({
label: {
type: String,
default: '',
required: false,
},
labelWidth: {
type: String,
default: '130px',
required: false,
},
value: {
type: [String,Array],
required: false,
},
placeholder: {
type: String,
required: false,
default: '请选择省市区',
},
disabled: {
type: Boolean,
default: false,
required: false,
},
required: {
type: Boolean,
default: false,
required: false,
},
backAll: {
type: Boolean,
default: true,
required: false,
},
})
// 定义 emits
const emits = defineEmits(['input', 'change', 'update:value'])
// 定义响应式数据
const selected = ref([])
const district = { ...getPcaOptionData() }
const columns = ref([
district[0],
district[district[0][0].value],
district[district[district[0][0].value][0].value]
])
const onChangeDistrict = (pickerView, value, columnIndex, resolve) => {
const item = value[columnIndex]
if (columnIndex === 0) {
pickerView.setColumnData(1, district[item.value])
pickerView.setColumnData(2, district[district[item.value][0].value])
} else if (columnIndex === 1) {
pickerView.setColumnData(2, district[item.value])
}
resolve()
}
const handleConfirm = ({value}) => {
if(value){
emits('update:value', props.backAll?value:value[2]);
}
}
// 监听 value 变化
watch(
() => props.value,
async (val) => {
if(props.value && isString(props.value)){
let arr = getAreaArrByCode(props.value);
selected.value = arr;
await initColumnData(arr);
}
},
{ immediate: true },
)
/**
* 初始化列数据
* @param val
*/
function initColumnData(val){
if(val && val.length){
let first = district[0];
let second = district[selected.value[0]];
let third = district[selected.value[1]];
columns.value = [first, second, third]
}
}
</script>

View File

@@ -0,0 +1,175 @@
<template>
<view class="Popup">
<view @click="handleClick">
<wd-input
:placeholder="`请选择${$attrs.label}`"
type="text"
readonly
v-model="showText"
clearable
v-bind="$attrs"
/>
</view>
<LinkRecordsModal
v-if="reportModal.show"
:selected="value"
ref="lrmRef"
:dictCode="dictCode"
:dictTable="dictTable"
:dictText="dictText"
:multi="multi"
:imageField="imageField"
@close="handleClose"
@change="handleChange"
></LinkRecordsModal>
</view>
</template>
<script setup lang="ts">
import { ref, watch, useAttrs } from 'vue'
import { useToast } from 'wot-design-uni'
import LinkRecordsModal from './link-records-modal.vue'
import {http} from "@/utils/http";
defineOptions({
name: 'onlinePopupLinkRecord',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
value: {
type: String,
required: false,
},
name: {
type: String,
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
required: {
type: Boolean,
default: true,
required: false,
},
formSchema: {
type: Object,
required: true,
},
})
const emit = defineEmits(['change', 'update:value','selected'])
const toast = useToast()
const lrmRef = ref()
const showText = ref('')
const selectVal = ref([])
const attrs: any = useAttrs()
const reportModal = reactive({
show: false,
})
//字典code
const dictCode = computed(() => props.formSchema?.dictCode)
//字典table
const dictTable = computed(() => props.formSchema?.dictTable)
//字典文本
const dictText = computed(() => props.formSchema?.dictText)
//是否多选
const multi = computed(() => {
if(props.formSchema?.fieldExtendJson){
const extendJson = JSON.parse(props.formSchema.fieldExtendJson)
return extendJson?.multiSelect
}
return false
})
//图片字段
const imageField = computed(() => {
if(props.formSchema?.fieldExtendJson){
const extendJson = JSON.parse(props.formSchema.fieldExtendJson)
return extendJson?.imageField
}
return ''
})
//首次加载
const firstLoad = ref(true);
/**
* 监听value数值
*/
watch(
() => props.value,
(val) => {
val && loadValue()
},
{ immediate: true },
)
//加载数据
function loadValue(){
console.log('关联记录loadValue',firstLoad.value)
if(!firstLoad.value){
return
}
let linkTableSelectFields = dictCode.value + ',' + dictText.value;
let superQueryParams = [{"field":"id","rule":"in","val": props.value}];
let param = {
linkTableSelectFields,
superQueryMatchType:"and",
superQueryParams: encodeURI(JSON.stringify(superQueryParams))
};
let titleField = props.formSchema?.dictText && props.formSchema?.dictText.split(",")[0];
http.get(`/online/cgform/api/getData/${dictTable.value}`,param).then((res:any)=>{
if(res.success){
let selectedList = res.result.records || [];
let labels = [];
let values = [];
selectedList.forEach(item=>{
if(item.id){
values.push(item.id);
labels.push(item[titleField]);
}
})
showText.value = labels.join(',');
selectVal.value = values;
emit('selected', selectedList,props.name);
}
})
firstLoad.value = false;
}
//回显数值
function callBack(rows) {
//匹配popup设置的回调值
let values = []
let labels = []
let titleField = props.formSchema?.dictText && props.formSchema?.dictText.split(",")[0];
rows.forEach(item=>{
if(item.id){
values.push(item.id);
labels.push(item[titleField]);
}
})
showText.value = labels.join(',')
selectVal.value = values
emit('selected', rows,props.name)
emit('change', values.join(','))
emit('update:value', values.join(','))
}
//点击事件
const handleClick = () => {
if (!props.disabled) {
reportModal.show = true
//lrmRef.value.beforeOpen(selectVal.value)
}
}
//关闭事件
const handleClose = () => {
reportModal.show = false
}
const handleChange = (data) => {
console.log('选中的值:', data)
callBack(data)
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,157 @@
<template>
<!-- #ifdef MP-WEIXIN -->
<wd-picker
:label-width="labelWidth"
:label="label"
clearable
filterable
v-model="selected"
:columns="options"
:disabled="disabled"
placeholder="请选择"
@confirm="handleChange"
></wd-picker>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<wd-select-picker
:label-width="labelWidth"
:show-confirm="false"
:label="label"
v-model="selected"
type="radio"
filterable
clearable
:columns="options"
:disabled="disabled"
placeholder="请选择"
@change="handleChange"
></wd-select-picker>
<!-- #endif -->
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { isArray, isString } from 'lodash'
import { http } from "@/utils/http";
import { isNullOrUnDef } from "@/utils/is"; // 假设使用 lodash 来判断类型
// 定义 props
const props = defineProps({
dict: {
type: [Array, String],
default: () => [],
required: true,
},
label: {
type: String,
default: '',
required: false,
},
labelWidth: {
type: String,
default: '80px',
required: false,
},
name: {
type: String,
default: '',
required: false,
},
dictStr: {
type: String,
default: '',
required: false,
},
type: {
type: String,
default: '',
required: false,
},
modelValue: {
type: [Array, String],
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
})
// 定义 emits
const emit = defineEmits(['input', 'change', 'update:modelValue'])
// 定义响应式数据
const selected = ref('');
const options = ref([]);
// 初始化选项
const initSelections = async () => {
options.value = []
if (props.type === 'sel_search' && props.dictStr) {
let temp = props.dictStr
if (temp.indexOf(' ') > 0) {
temp = encodeURI(props.dictStr)
}
try {
const res = await http.get('/sys/dict/getDictItems/' + temp)
if (res.success) {
options.value = res.result
}
} catch (error) {
console.error('请求数据出错:', error)
}
}
else {
if (!props.dict || props.dict.length === 0) {
return
}
if (isString(props.dict)) {
try {
let code = props.dict;
if (code.indexOf(',') > 0 && code.indexOf(' ') > 0) {
// 编码后类似sys_user%20where%20username%20like%20xxx' 是不包含空格的,这里判断如果有空格和逗号说明需要编码处理
code = encodeURI(code);
}
const res = await http.get('/sys/dict/getDictItems/' + code)
if (res.success) {
options.value = res.result;
}
} catch (error) {
console.error('请求数据出错:', error)
}
} else {
props.dict.forEach((item) => {
options.value.push(item)
})
}
}
}
// 选择器改变事件处理函数
const handleChange = ({value}) => {
emit('update:modelValue', value);
emit('change', value);
}
// 监听 dict 和 value 的变化
watch(() => props.dict, () => {
initSelections();
});
// 监听 value 的变化
watch(
() => props.modelValue,
(val) => {
selected.value = isNullOrUnDef(val)?'':val;
},
{ immediate: true },
)
// 组件挂载时初始化选项
onMounted(() => {
initSelections()
})
</script>
<style></style>

View File

@@ -0,0 +1,163 @@
<template>
<!-- 带搜索 -->
<!-- #ifndef MP-WEIXIN -->
<wd-select-picker
:label-width="labelWidth"
:label="label"
type="radio"
filterable
v-model="selected"
:columns="options"
:disabled="disabled"
:show-confirm="false"
placeholder="请选择"
@change="handleChange"
></wd-select-picker>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<!-- 常规下拉 -->
<wd-picker
:label-width="labelWidth"
:label="label"
filterable
v-model="selected"
:columns="options"
:disabled="disabled"
placeholder="请选择"
@confirm="handleChange"
></wd-picker>
<!-- #endif -->
</template>
<script lang="ts" setup>
import { ref, watch, onMounted } from 'vue'
import { isArray, isString } from 'lodash'
import { http } from '@/utils/http'
import { isNullOrUnDef } from '@/utils/is'
// 定义 props
const props = defineProps({
dict: {
type: [Array, String],
default: () => [],
required: true,
},
label: {
type: String,
default: '',
required: false,
},
labelWidth: {
type: String,
default: '80px',
required: false,
},
name: {
type: String,
default: '',
required: false,
},
dictStr: {
type: String,
default: '',
required: false,
},
type: {
type: String,
default: '',
required: false,
},
modelValue: {
type: [String, Number],
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
})
// 定义 emits
const emit = defineEmits(['input', 'change', 'update:modelValue'])
// 定义响应式数据
const selected = ref<any>('')
const options = ref<any>([])
// 初始化选项
const initSelections = async () => {
options.value = []
if (props.type === 'sel_search' && props.dictStr) {
let temp = props.dictStr
if (temp.indexOf(' ') > 0) {
temp = encodeURI(props.dictStr)
}
try {
const res: any = await http.get('/sys/dict/getDictItems/' + temp)
if (res.success) {
options.value = (res.result ?? []).filter((item) => item !== null)
}
} catch (error) {
console.error('请求数据出错:', error)
}
} else {
if (!props.dict || props.dict.length === 0) {
return
}
if (isString(props.dict)) {
try {
let code = props.dict;
if (code.indexOf(',') > 0 && code.indexOf(' ') > 0) {
// 编码后类似sys_user%20where%20username%20like%20xxx' 是不包含空格的,这里判断如果有空格和逗号说明需要编码处理
code = encodeURI(code);
}
const res: any = await http.get('/sys/dict/getDictItems/' + code)
if (res.success) {
options.value = res.result
}
} catch (error) {
console.error('请求数据出错:', error)
}
} else {
props.dict.forEach((item) => {
options.value.push(item)
})
}
}
}
// 选择器改变事件处理函数
const handleChange = ({value}) => {
emit('update:modelValue', value)
emit('change', value)
}
// 监听 dict 和 value 的变化
watch(
() => props.dict,
() => {
initSelections()
},
)
// 监听 value 的变化
watch(
() => props.modelValue,
(val) => {
selected.value = isNullOrUnDef(props.modelValue) ? '' : val
},
{ immediate: true },
)
// 组件挂载时初始化选项
onMounted(() => {
initSelections()
})
</script>
<style lang="scss" scoped>
:deep(.wd-checkbox__shape) {
border-radius: 0 !important;
}
</style>

View File

@@ -0,0 +1,79 @@
<template>
<wd-datetime-picker :disabled="disabled" type="time" :labelWidth="labelWidth" v-model="currentTime" :label="label" @confirm="handleConfirm" />
</template>
<script setup>
// 定义 props
import dayjs from "dayjs";
const props = defineProps({
label: {
type: String,
default: '',
required: false,
},
labelWidth: {
type: String,
default: '80px',
required: false,
},
name: {
type: String,
default: '',
required: false,
},
type: {
type: String,
default: '',
required: false,
},
value: {
type: [String, Number,Date],
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
})
// 定义 emits
const emit = defineEmits(['input', 'change', 'update:value'])
// 定义响应式数据;
const currentTime = ref('')
// 监听 value 的变化
watch(
() => props.value,
(val) => {
currentTime.value = initData(val);
},
{
immediate:true
}
)
/**
*
* @param val
* @returns {*|string}
*/
function initData(time){
try {
if(typeof time == 'object' ){
time = dayjs(time).format('HH:mm:ss');
}
}catch (e) {
console.log("initData>>e",e);
}
return time?time:'';
}
// 选择器改变事件处理函数
const handleConfirm = (e) => {
emit('update:value', currentTime.value+':00')
emit('change', currentTime.value+':00')
}
</script>
<style></style>

View File

@@ -0,0 +1,109 @@
import type { App } from 'vue';
import StatusTip from '@/pages-work/components/statusTip.vue';
import JBar from '@/pages-work/components/echarts/JBar/index.vue';
import JBackgroundBar from '@/pages-work/components/echarts/JBackgroundBar/index.vue';
import JDynamicBar from '@/pages-work/components/echarts/JDynamicBar/index.vue';
import JStackBar from '@/pages-work/components/echarts/JStackBar/index.vue';
import JMultipleBar from '@/pages-work/components/echarts/JMultipleBar/index.vue';
import JNegativeBar from '@/pages-work/components/echarts/JNegativeBar/index.vue';
import JMixLineBar from '@/pages-work/components/echarts/JMixLineBar/index.vue';
import JProgress from '@/pages-work/components/echarts/JProgress/index.vue';
import JLine from '@/pages-work/components/echarts/JLine/index.vue';
import JMultipleLine from '@/pages-work/components/echarts/JMultipleLine/index.vue';
import JSmoothLine from '@/pages-work/components/echarts/JSmoothLine/index.vue';
import JStepLine from '@/pages-work/components/echarts/JStepLine/index.vue';
import JPie from '@/pages-work/components/echarts/JPie/index.vue';
import JRing from '@/pages-work/components/echarts/JRing/index.vue';
import JFunnel from '@/pages-work/components/echarts/JFunnel/index.vue';
import JPyramidFunnel from '@/pages-work/components/echarts/JPyramidFunnel/index.vue';
import JRadar from '@/pages-work/components/echarts/JRadar/index.vue';
import JCircleRadar from '@/pages-work/components/echarts/JCircleRadar/index.vue';
import JGauge from '@/pages-work/components/echarts/JGauge/index.vue';
import JColorGauge from '@/pages-work/components/echarts/JColorGauge/index.vue';
import JScatter from '@/pages-work/components/echarts/JScatter/index.vue';
import JBubble from '@/pages-work/components/echarts/JBubble/index.vue';
import DoubleLineBar from '@/pages-work/components/echarts/DoubleLineBar/index.vue';
import JRose from '@/pages-work/components/echarts/JRose/index.vue';
import JHorizontalBar from '@/pages-work/components/echarts/JHorizontalBar/index.vue';
import JArea from '@/pages-work/components/echarts/JArea/index.vue';
import JPictorial from '@/pages-work/components/echarts/JPictorial/index.vue';
import JPictorialBar from '@/pages-work/components/echarts/JPictorialBar/index.vue';
import JAreaMap from '@/pages-work/components/echarts/map/JAreaMap/index.vue';
import JBubbleMap from '@/pages-work/components/echarts/map/JBubbleMap/index.vue';
import JBarMap from '@/pages-work/components/echarts/map/JBarMap/index.vue';
import JHeatMap from '@/pages-work/components/echarts/map/JHeatMap/index.vue';
import JFlyLineMap from '@/pages-work/components/echarts/map/JFlyLineMap/index.vue';
import JTotalFlyLineMap from '@/pages-work/components/echarts/map/TotalFlyLineMap/index.vue';
import JTotalBarMap from '@/pages-work/components/echarts/map/TotalBarMap/index.vue';
//非echart组件
import JCarousel from '@/pages-work/components/drag/carousel/index.vue';
import JIframe from '@/pages-work/components/drag/iframe/index.vue';
import JDragEditor from '@/pages-work/components/drag/editor/index.vue';
import JImg from '@/pages-work/components/drag/img/index.vue';
import JNumber from '@/pages-work/components/drag/number/index.vue';
import JText from '@/pages-work/components/drag/text/index.vue';
import JCalendar from '@/pages-work/components/drag/calendar/index.vue';
import JCurrentTime from '@/pages-work/components/drag/currentTime/time.vue';
import JList from '@/pages-work/components/drag/list/index.vue';
import JRadioButton from '@/pages-work/components/drag/radiobutton/index.vue';
import JCommonTable from '@/pages-work/components/drag/table/index.vue';
import JQuickNav from '@/pages-work/components/drag/JQuickNav/index.vue';
import JForm from '@/pages-work/components/drag/form/index.vue';
// 全局注册组件
export function registerGlobComp(app: App) {
app.component('statusTip', StatusTip)
app.component('JBar', JBar)
app.component('JMultipleBar', JMultipleBar)
app.component('JNegativeBar', JNegativeBar)
app.component('JLine', JLine)
app.component('JMultipleLine', JMultipleLine)
app.component('JPie', JPie)
app.component('JRing', JRing)
app.component('JFunnel', JFunnel)
app.component('JPyramidFunnel', JPyramidFunnel)
app.component('JRadar', JRadar)
app.component('JCircleRadar', JCircleRadar)
app.component('JGauge', JGauge)
app.component('JColorGauge', JColorGauge)
app.component('JScatter', JScatter)
app.component('JBubble', JBubble)
app.component('DoubleLineBar', DoubleLineBar)
app.component('JRose', JRose)
app.component('JHorizontalBar', JHorizontalBar)
app.component('JArea', JArea)
app.component('JBackgroundBar', JBackgroundBar)
app.component('JDynamicBar', JDynamicBar)
app.component('JMixLineBar', JMixLineBar)
app.component('JStackBar', JStackBar)
app.component('JStepLine', JStepLine)
app.component('JSmoothLine', JSmoothLine)
app.component('JProgress', JProgress)
app.component('JPictorial', JPictorial)
app.component('JPictorialBar', JPictorialBar)
app.component('JAreaMap', JAreaMap)
app.component('JBubbleMap', JBubbleMap)
app.component('JBarMap', JBarMap)
app.component('JHeatMap', JHeatMap)
app.component('JFlyLineMap', JFlyLineMap)
app.component('JTotalFlyLineMap', JTotalFlyLineMap)
app.component('JTotalBarMap', JTotalBarMap)
//非echart组件
app.component('JCarousel', JCarousel)
app.component('JIframe', JIframe)
app.component('JDragEditor', JDragEditor)
app.component('JImg', JImg)
app.component('JNumber', JNumber)
app.component('JText', JText)
app.component('JCalendar', JCalendar)
app.component('JCurrentTime', JCurrentTime)
app.component('JList', JList)
app.component('JRadioButton', JRadioButton)
app.component('JCommonTable', JCommonTable)
app.component('JQuickNav', JQuickNav)
app.component('JForm', JForm)
}