uni-app-卡片组件

本文详细介绍了基于Uni-app的卡片组件实现,包括数据遍历、内容展示、事件处理、样式定制等多个方面。组件支持多种类型的数据显示,如文件、多字段拼接、图标、图片等,并提供了自定义插槽和状态标签的功能。此外,还涉及了子级数据的扩展展示及滚动加载等交互特性。
摘要由CSDN通过智能技术生成

Uni-app 卡片组件

<template>
    <view class="card-content">
        <span v-if="cardData.length === 0 && needNoData" class="no-data"
            >No data</span
        >
        <block v-for="(item, itemIndex) of cardData" :key="itemIndex">
            <view
                class="card-item"
                @click="contentClickHandle(item, itemIndex)"
            >
                <view class="title">
                    <span @click.stop="codeNameClick(item)" class="titleName">
                        <u-icon
                            v-if="codeNameLink"
                            name="attach"
                            color="#6dbee2"
                            size="30"
                        ></u-icon>
                        {{ codeName === '' ? '' : codeFormat(item[codeName]) }}
                    </span>
                    <span v-show="showCreateDate">
                        {{
                            subCodeName && item[subCodeName]
                                ? `${item[subCodeName]}`
                                : item.CreateDate
                                ? formatDate(item.CreateDate)
                                : item[createDateName]
                                ? formatDate(item[createDateName])
                                : ''
                        }}
                    </span>
                </view>

                <view class="card-data">
                    <block v-for="(i, index) of attributesList" :key="index">
                        <span
                            class="card-attribute"
                            :style="{
                                padding: i.type === 'extends' ? '0' : '10rpx',
                            }"
                        >
                            <span
                                :style="{ width: `${labelWidth}rpx` }"
                                @click.stop="showToast(i.label)"
                                v-if="!(i.type === 'extends')"
                            >
                                {{ i.label }}
                            </span>
                            <!-- 文件下载 
                                type: file
                                urlName: 文件 url 对应字段
                            -->
                            <span
                                @click="downloadFile(item[i.urlName], item)"
                                v-if="i.type === 'file' && i.urlName"
                                class="file"
                                @click.stop="stopPropagation"
                                download
                            >
                                <u-icon name="file-text" size="50"></u-icon>
                            </span>
                            <!-- 多字段拼接 
                                type: multiField
                                names: 字段数组,[string, {
                                    name: string,
                                    format: boolean,
                                    formatFunc: Function
                                }]
                                separator: 拼接分隔符
                            -->
                            <span
                                v-else-if="i.type === 'multiField' && i.names"
                                @click.stop="
                                    showToast(
                                        multiFieldHandle(
                                            item,
                                            i.names, // 字段数组
                                            i.separator // 拼接分隔符
                                        )
                                    )
                                "
                            >
                                {{
                                    multiFieldHandle(item, i.names, i.separator)
                                }}
                            </span>
                            <!-- 使用符号表示
                                useIcon 为 true
                                iconMap: {
                                    item[i.name]: 字段的值,例如(true/false): {
                                        name: 对应 u-icon 的 name
                                        color: 对应 u-icon 的 color
                                        size: 对应 u-icon 的 size
                                    }
                                }
                            -->
                            <span v-else-if="i.type === 'icon' && i.iconMap">
                                <u-icon
                                    :name="i.iconMap.get(item[i.name]).name"
                                    :color="i.iconMap.get(item[i.name]).color"
                                    :size="i.iconMap.get(item[i.name]).size"
                                />
                            </span>
                            <!-- 使用图片显示
                                type: image
                                item[i.name]: 图片 url
                                height: 图片高都 默认 60rpx
                                mode: 图片展示类型 根据 uni-app image 默认 heightFix
                            -->
                            <span
                                v-else-if="i.type === 'image'"
                                @click.stop="showImage(item[i.name])"
                            >
                                <span
                                    v-if="
                                        item[i.name] !== '' &&
                                            item[i.name] != null
                                    "
                                    :style="{
                                        height: `${
                                            i.height ? i.height : 60
                                        }rpx`,
                                    }"
                                >
                                    <u-image
                                        :mode="i.mode ? i.mode : 'heightFix'"
                                        :height="i.height ? i.height : 60"
                                        :src="item[i.name]"
                                    ></u-image>
                                </span>
                                <span v-else>No Image</span>
                            </span>
                            <!-- 格式化 
                                type: format
                                formatFunc: 格式化函数 返回文本或html
                            -->
                            <span
                                v-else-if="
                                    i.type === 'format' &&
                                        i.formatFunc &&
                                        typeof i.formatFunc === 'function'
                                "
                            >
                                <span
                                    v-html="i.formatFunc(item[i.name])"
                                ></span>
                            </span>
                            <!-- 
                                type: formatDate
                                日期格式化
                            -->
                            <span v-else-if="i.type === 'formatDate'">
                                <span v-html="formatDate(item[i.name])"></span>
                            </span>
                            <!-- 插槽
                                type: slot
                                slotName 插槽名称
                            -->
                            <span v-else-if="i.type === 'slot' && i.slotName">
                                <span>
                                    <slot
                                        :name="i.slotName"
                                        :row="item"
                                        :index="itemIndex"
                                    ></slot>
                                </span>
                            </span>
                            <!-- app不支持动态插槽,使用固定名称 -->
                            <span v-else-if="i.type === 'slot1'">
                                <span>
                                    <slot
                                        name="slot1"
                                        :row="item"
                                        :index="itemIndex"
                                    ></slot>
                                </span>
                            </span>
                            <span v-else-if="i.type === 'slot2'">
                                <span>
                                    <slot
                                        name="slot2"
                                        :row="item"
                                        :index="itemIndex"
                                    ></slot>
                                </span>
                            </span>
                            <span v-else-if="i.type === 'slot3'">
                                <span>
                                    <slot
                                        name="slot3"
                                        :row="item"
                                        :index="itemIndex"
                                    ></slot>
                                </span>
                            </span>
                            <!-- 
                                type: extends
                                嵌套机构
                                childrensMark: 子集名称
                                getChildrenApi: 子集获取api
                            -->
                            <span
                                v-else-if="
                                    i.type === 'extends' &&
                                        item[i.childrensMark] &&
                                        i.getChildrenApi
                                "
                                :style="{ 'padding-right': 0 }"
                            >
                                <span
                                    @click.stop="
                                        getChildrenData(i.getChildrenApi, item)
                                    "
                                    class="children-extends-btn"
                                    style="margin: 10rpx 0"
                                >
                                    <u-icon
                                        name="plus"
                                        style="display: inline-block"
                                        color="#fff"
                                    ></u-icon>
                                </span>
                            </span>
                            <!-- 默认显示模式 -->
                            <span
                                v-else
                                class="default"
                                @click.stop="showToast(item[i.name])"
                                >{{ item[i.name] }}</span
                            >
                        </span>
                    </block>
                </view>
                <view class="footer">
                    <!-- 自定义 footer
                        hasFooter 为 true
                        slot: footer
                    -->
                    <view v-if="hasFooter">
                        <slot name="footer" :item="item"></slot>
                    </view>
                    <!-- 默认 footer -->
                    <view
                        v-if="!hasFooter"
                        class="default"
                        :style="
                            !(needStatusTag && (item.StatusId || item.Status))
                                ? { width: '100%', padding: 0 }
                                : {}
                        "
                    >
                        <!-- 需要显示 Status -->
                        <l-steps
                            v-if="
                                needStatusTag &&
                                    (item.StatusId || item.Status) &&
                                    !customStatus
                            "
                            :id="item[`${idName}`]"
                            :detail="{
                                StatusName:
                                    item.StatusName || item.StatusLabel || null,
                                StatusId: item.StatusId || item.Status,
                                Blacklist: item.Blacklist,
                                IsSent: item.IsSent,
                                IsActive: item.IsActive || null,
                            }"
                        ></l-steps>
                        <slot
                            v-if="customStatus"
                            name="customStatus"
                            :item="item"
                        ></slot>
                        <!-- action 按钮设置 -->
                        <span
                            :style="
                                !(
                                    needStatusTag &&
                                    (item.StatusId || item.Status)
                                )
                                    ? {
                                          width: '100%',
                                      }
                                    : {}
                            "
                        >
                            <u-button
                                @click.stop="ActionClickHandle(item, itemIndex)"
                                type="warning"
                                size="mini"
                                v-if="hasAction"
                                :style="
                                    !(
                                        needStatusTag &&
                                        (item.StatusId || item.Status)
                                    )
                                        ? {
                                              'border-radius':
                                                  '0 0 15rpx 15rpx',
                                              height: '70rpx',
                                              width: '100%',
                                          }
                                        : {}
                                "
                                >Action</u-button
                            >
                        </span>
                    </view>
                </view>
            </view>
        </block>
        <u-popup
            v-model="showChildrens"
            class="card-childrens-popup"
            v-if="!isChildren"
        >
            <view class="childrens-header">
                <span>Childrens Data</span>
                <span>
                    <u-icon name="close" @click="closeChildrensData"> </u-icon>
                </span>
            </view>
            <u-tabs
                :list="titleList"
                :current="current"
                @change="change"
                active-color="#f08300"
                bg-color="#e7e7e7"
                :is-scroll="true"
            ></u-tabs>
            <c-scroll-view-back-top height="calc(100vh - 185rpx)">
                <view slot="content" class="card-childrens">
                    <c-card
                        :codeName="codeName"
                        :cardData="cardChildrenData"
                        :attributesList="attributesList"
                        @actionClick="ActionClickHandle"
                        @click="childrenClick"
                        :labelWidth="labelWidth"
                        :needStatusTag="needStatusTag"
                        :hasFooter="hasFooter"
                        :hasAction="hasAction"
                        :needNoData="false"
                        :isChildren="true"
                        :customStatus="customStatus"
                    >
                        <span slot="slot1" slot-scope="scope">
                            <slot
                                name="slot1"
                                :row="scope.row"
                                :index="scope.index"
                            ></slot>
                        </span>
                        <span slot="slot2" slot-scope="scope">
                            <slot
                                name="slot2"
                                :row="scope.row"
                                :index="scope.index"
                            ></slot>
                        </span>
                        <span slot="slot3" slot-scope="scope">
                            <slot
                                name="slot3"
                                :row="scope.row"
                                :index="scope.index"
                            ></slot>
                        </span>
                        <span slot="customStatus" slot-scope="scope">
                            <slot name="customStatus" :item="scope.item"></slot>
                        </span>
                    </c-card>
                </view>
            </c-scroll-view-back-top>
        </u-popup>
        <u-toast ref="uToast" />
    </view>
</template>
<script lang="ts">
import {
    defineComponent,
    reactive,
    PropType,
    ref,
    inject,
} from '@vue/composition-api'
import { formatDate, openFile } from '@/api/utils/util'
import { Attributes } from '@/types'
export interface Attributes {
    label: string
    name?: string
    type?:
    | 'file'
    | 'multiField'
    | 'icon'
    | 'image'
    | 'format'
    | 'slot'
    | 'slot1'
    | 'slot2'
    | 'slot3'
    | 'extends'
    | 'formatDate'
    urlName?: string // 文件 url 对应字段
    names?: [
        string,
        {
            name: string
            format: boolean
            formatFunc: Function
        }
    ] // 字段数组
    separator?: string //拼接分隔符
    iconMap?: Map<
        any,
        {
            // 字段的值,例如(true/false)
            name: string //对应 u-icon 的 name
            color: string //对应 u-icon 的 color
            size: string //对应 u-icon 的 size
        }
    >
    height?: string // 图片高都 默认 60rpx
    mode?: string //图片展示类型 根据 uni-app image 默认 heightFix
    formatFunc?: Function // 格式化函数 返回文本或html
    slotName?: string // 插槽名称
    childrensMark?: string // 子集名称
    getChildrenApi?: Function // 子集获取api
}
export default defineComponent({
    name: 'c-card',
    emits: ['change'],
    props: {
        idName: {
            type: String,
            default: '',
        },
        // 卡片标题字段
        codeName: {
            type: String,
            default: '',
        },
        codeNameLink: {
            type: Boolean,
            default: false,
        },
        subCodeName: {
            type: String,
            default: '',
        },
        // 卡片数据
        cardData: {
            type: Array,
            default: () => [] as any[],
            required: true,
        },
        // 卡片显示字段
        /**
         * {
         *      label: label
         *      name: 对应字段名称
         * }
         */
        attributesList: {
            type: Array as PropType<Attributes[]>,
            default: [] as Attributes[],
            required: true,
        },
        // 是否需要显示 status
        needStatusTag: {
            type: Boolean,
            default: true,
        },
        // label 宽度 默认 200rpx
        labelWidth: {
            type: Number,
            default: 200,
        },
        // 是否存在 footer 插件
        hasFooter: {
            type: Boolean,
            default: false,
        },
        hasAction: {
            type: Boolean,
            default: true,
        },
        needNoData: {
            type: Boolean,
            default: true,
        },
        customStatus: {
            type: Boolean,
            default: false,
        },
        codeFormat: {
            type: Function,
            default: (value: any) => value,
        },
        createDateName: {
            type: String,
            default: '',
        },
        // 显示创建时间
        showCreateDate: {
            type: Boolean,
            default: true,
        },
        isChildren: {
            type: Boolean,
            default: false,
        },
    },
    methods: {
        onChange(type: 'actionClick' | 'click' | 'link', item: any) {
            this.$emit('change', { type, item })
        },
        ActionClickHandle(item: any, index: number) {
            // 触发点击 action 按钮事件
            this.$emit('actionClick', item)
            this.$emit('index', index)
            this.onChange('actionClick', item)
        },
        contentClickHandle(item: any, index: number) {
            // 触发点击 click 按钮事件
            this.$emit('click', item)
            this.$emit('index', index)
            this.onChange('click', item)
        },
        formatDate(date: string) {
            // 日期格式化
            return formatDate(date)
        },
        stopPropagation(event: any) {
            // 阻止冒泡
            event.stopPropagation()
        },
        showToast(title: string) {
            // 显示过长文本完整内容
            ;(this.$refs.uToast as any).show({
                title,
                icon: false,
            })
        },
        // 打开显示文件
        downloadFile(url: string) {
            openFile(url, this)
        },
        // 打开显示图片
        showImage(url: string) {
            if (url) {
                uni.previewImage({ urls: [url] })
            }
        },
        // 多字段拼接处理函数
        multiFieldHandle(
            item: any,
            names: string[],
            separator: string | undefined
        ) {
            let str = ''
            for (let i = 0; i < names.length; i++) {
                if (i !== names.length - 1) {
                    let nameItem = names[i] as any
                    if (typeof nameItem === 'string') {
                        str +=
                            item[nameItem] +
                            (separator != null ? separator : ' ')
                    } else if (
                        typeof nameItem === 'object' &&
                        nameItem != null
                    ) {
                        // 进行格式化处理
                        let str2 = ''
                        if (
                            nameItem.name &&
                            nameItem.format &&
                            nameItem.formatFunc
                        ) {
                            str2 =
                                nameItem.formatFunc(item[nameItem.name]) +
                                (separator != null ? separator : ' ')
                        }
                        str += str2
                    }
                } else {
                    str += item[names[i]]
                }
            }
            return str
        },
        childrenClick() {
            this.$emit('click')
        },
        closeChildrensData() {
            this.titleList.length = 0
            this.showChildrens = false
            this.$emit('overflowScrollingAuto', false)
        },
        codeNameClick(item: any) {
            if (this.codeNameLink) {
                this.onChange('link', item)
            } else {
                this.showToast(
                    this.codeName === ''
                        ? ''
                        : (this.codeFormat as Function)(item[this.codeName])
                )
            }
        },
        change(index: number) {
            this.current = index
            let title = this.titleList[index]
            this.getChildrenData(title.api, title.item)
        },
        getChildrenData(api: Function, item: any) {
            if (this.isChildren) {
                ;(this.childrenGetData as Function).call(this, api, item)
            } else {
                if (!this.showChildrens) {
                    this.showChildrens = true
                }
                this.cardChildrenData.length = 0
                uni.showLoading({
                    title: 'Loading ...',
                })
                api(item).then((data: any) => {
                    uni.hideLoading()
                    if (data.Result == 1) {
                        this.cardChildrenData.push(...data.Data)
                        this.titleListHandle(api, item)
                    }
                })
                this.$emit('overflowScrollingAuto', true)
            }
        },
        titleListHandle(api: Function, item: any) {
            if (this.titleList.length > 0) {
                for (let [index, title] of this.titleList.entries()) {
                    if (title.name === item[this.codeName]) {
                        this.current = index
                        this.titleList.splice(
                            index + 1,
                            this.titleList.length - 1
                        )
                    } else {
                        this.titleList.push({
                            name: item[this.codeName],
                            api,
                            item,
                        })
                        this.current = this.titleList.length - 1
                    }
                }
            } else {
                this.titleList.push({
                    name: item[this.codeName],
                    api,
                    item,
                })
                this.current = this.titleList.length - 1
            }
        },
    },
    provide() {
        return {
            getChildrenData: this.getChildrenData,
        }
    },
    setup(props) {
        let childrenGetData = props.isChildren
            ? inject('getChildrenData')
            : () => {}
        let cardChildrenData = reactive([] as any[])
        let showChildrens = ref(false)
        let current = ref(0)
        let titleList = reactive(
            [] as {
                name: string
                api: Function
                item: any
            }[]
        )
        return {
            childrenGetData,
            cardChildrenData,
            showChildrens,
            titleList,
            current,
        }
    },
})
</script>
<style lang="scss" scoped>
.card-content {
    .no-data {
        width: 100%;
        position: fixed;
        top: 40vh;
        text-align: center;
    }
    .children-extends-btn {
        display: block;
        width: 50rpx;
        height: 50rpx;
        line-height: 50rpx;
        text-align: center;
        border-radius: 10rpx;
        margin: 5rpx;
        background-color: $laxton-type-active;
    }
    .card-childrens-popup {
        transform: translateZ(0);
        .card-childrens {
            padding-bottom: 10rpx;
        }
        .childrens-header {
            display: flex;
            justify-content: space-between;
            height: 100rpx;
            padding: 0 30rpx;
            line-height: 100rpx;
        }
    }
    .card-item {
        background-color: #fff;
        border-radius: 15rpx;
        margin: 30rpx;
        box-shadow: 0rpx 5rpx 10rpx #ececec;
        padding-top: 15rpx;
        flex-direction: column;
        view {
            display: flex;
        }
        .file {
            text-decoration: none;
            color: $laxton-type-active;
            :active {
                color: $laxton-type-active;
            }
            :visited {
                color: $laxton-type-active;
            }
        }
        > :first-child {
            justify-content: space-between;
            height: 70rpx;
            line-height: 70rpx;
            border-bottom: 1px #ececec solid;
            > :first-child {
                font-size: 28rpx;
                font-weight: bold;
            }
        }
        .title {
            padding: 0 30rpx;
            > span:first-child {
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
                flex: 1;
                width: 0;
            }
            > span:last-child {
                width: 150rpx;
                text-align: right;
            }
        }
        .card-data {
            display: flex;
            flex-direction: column;
            padding: 0 30rpx;
            .card-attribute {
                display: flex;
                border-top: 1px #ececec solid;
                align-items: center;
                padding: 10rpx 0;
                > :first-child {
                    padding-right: 20rpx;
                    overflow: hidden;
                    text-overflow: ellipsis;
                    white-space: nowrap;
                }
                > :last-child {
                    flex: 1;
                    width: 0;
                    white-space: normal;
                    word-break: break-all;
                }
            }
            > :first-child {
                border-top: none;
            }
        }
        .footer {
            .default {
                justify-content: space-between;
                border-top: 1px #ececec solid;
                align-items: center;
                width: 100%;
                .u-size-default {
                    height: 60rpx;
                }
                padding: 20rpx 30rpx 30rpx 30rpx;
            }
        }
    }
}
</style>
<!-- c-scroll-view-back-top -->
<template>
    <view>
        <c-back-top :scrollTop="old.scrollTop" @click="back"> </c-back-top>
        <scroll-view
            :scroll-y="scrollY"
            :scroll-x="scrollX"
            :scroll-top="scrollTop"
            :scroll-with-animation="true"
            @scroll="scroll"
            @scrolltolower="scrolltolower"
            :class="[
                'c-scroll-view-back-top',
                overflowScrollingAuto ? 'overflow-scrolling' : '',
            ]"
            :style="{
                height: height ? height : '100vh',
            }"
        >
            <view>
                <slot name="content"></slot>
            </view>
        </scroll-view>
    </view>
</template>
<script lang="ts">
import { defineComponent, reactive } from '@vue/composition-api'

export default defineComponent({
    props: {
        scrollY: {
            type: Boolean,
            default: true,
        },
        scrollX: {
            type: Boolean,
            default: true,
        },
        scrollWithAnimation: {
            type: Boolean,
            default: true,
        },
        height: {
            type: [String, Number],
        },
        overflowScrollingAuto: {
            type: Boolean,
            default: false,
        },
    },
    methods: {
        scroll(e: any) {
            this.old.scrollTop = e.detail.scrollTop
        },
        back() {
            this.scrollTop = this.old.scrollTop
            this.$nextTick(() => {
                this.scrollTop = 0
            })
        },
        scrolltolower(event: any) {
            this.$emit('scrolltolower', event)
        },
    },
    setup() {
        return reactive({
            scrollTop: 0,
            old: {
                scrollTop: 0,
            },
        })
    },
})
</script>
<style lang="scss" scoped>
.c-scroll-view-back-top {
    width: 750rpx;
    background-color: #f3f4f6;
}
.overflow-scrolling {
    ::v-deep .uni-scroll-view {
        -webkit-overflow-scrolling: auto;
    }
}
</style>
<!-- c-back-top -->
<template>
    <view>
        <c-back-top :scrollTop="old.scrollTop" @click="back"> </c-back-top>
        <scroll-view
            :scroll-y="scrollY"
            :scroll-x="scrollX"
            :scroll-top="scrollTop"
            :scroll-with-animation="true"
            @scroll="scroll"
            @scrolltolower="scrolltolower"
            :class="[
                'c-scroll-view-back-top',
                overflowScrollingAuto ? 'overflow-scrolling' : '',
            ]"
            :style="{
                height: height ? height : '100vh',
            }"
        >
            <view>
                <slot name="content"></slot>
            </view>
        </scroll-view>
    </view>
</template>
<script lang="ts">
import { defineComponent, reactive } from '@vue/composition-api'

export default defineComponent({
    props: {
        scrollY: {
            type: Boolean,
            default: true,
        },
        scrollX: {
            type: Boolean,
            default: true,
        },
        scrollWithAnimation: {
            type: Boolean,
            default: true,
        },
        height: {
            type: [String, Number],
        },
        overflowScrollingAuto: {
            type: Boolean,
            default: false,
        },
    },
    methods: {
        scroll(e: any) {
            this.old.scrollTop = e.detail.scrollTop
        },
        back() {
            this.scrollTop = this.old.scrollTop
            this.$nextTick(() => {
                this.scrollTop = 0
            })
        },
        scrolltolower(event: any) {
            this.$emit('scrolltolower', event)
        },
    },
    setup() {
        return reactive({
            scrollTop: 0,
            old: {
                scrollTop: 0,
            },
        })
    },
})
</script>
<style lang="scss" scoped>
.c-scroll-view-back-top {
    width: 750rpx;
    background-color: #f3f4f6;
}
.overflow-scrolling {
    ::v-deep .uni-scroll-view {
        -webkit-overflow-scrolling: auto;
    }
}
</style>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值