Bläddra i källkod

feat(组件): metaVideo的增加

gemercheung 2 år sedan
förälder
incheckning
e608f16964

+ 1 - 1
.vscode/settings.json

@@ -1,5 +1,5 @@
 {
-    "cSpell.words": [""],
+    "cSpell.words": ["", "disablepictureinpicture"],
     "typescript.tsdk": "node_modules/typescript/lib",
     "editor.formatOnSave": true,
     "npm.packageManager": "pnpm",

+ 1 - 0
packages/components/advance/index.ts

@@ -0,0 +1 @@
+export * from './tag'

+ 6 - 0
packages/components/advance/tag/index.ts

@@ -0,0 +1,6 @@
+import { withInstall } from '@kankan-components/utils'
+import tags from './src/Tags.vue'
+
+export const UITag = withInstall(tags)
+
+export default UITag

+ 78 - 0
packages/components/advance/tag/src/TagEditor.vue

@@ -0,0 +1,78 @@
+<script setup lang="ts">
+import { type PropType } from 'vue'
+
+interface TagType {
+    x: number
+    y: number
+    sid: string
+    visible: boolean
+}
+const props = defineProps({
+    tag: {
+        type: Object as PropType<TagType>,
+        required: true,
+    },
+})
+</script>
+
+<template>
+    <div :data-tag-id="props.tag.sid" :style="{ transform: `translate(${props.tag.x}px,${props.tag.y}px)`, display: props.tag.visible ? 'block' : 'none' }">
+        <span class="tag-icon animate" style="background-image: url(http://4dkk.4dage.com/v4/sdk/4.2.2/images/tag_icon_default.svg)" />
+    </div>
+</template>
+<style scoped>
+div {
+    position: absolute;
+}
+</style>
+<style>
+[xui_tags_view] {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+}
+[xui_tags_view] > div {
+    pointer-events: all;
+    display: none;
+    position: absolute;
+    width: 48px;
+    height: 48px;
+    margin-left: -24px;
+    margin-top: -24px;
+    z-index: 1;
+}
+
+[xui_tags_view] > div.focus {
+    z-index: 2;
+}
+[xui_tags_view] > div.fixed {
+    z-index: 3;
+}
+[xui_tags_view] > div.active {
+    z-index: 4;
+}
+
+[xui_tags_view] .tag-icon {
+    display: block;
+    width: 48px;
+    height: 48px;
+    background-size: cover;
+    cursor: pointer;
+}
+
+[xui_tags_view] .tag-icon.animate {
+    animation: tag-animate-zoom 3s -1s linear infinite;
+}
+
+@keyframes tag-animate-zoom {
+    0% {
+        transform: scale(1);
+    }
+    50% {
+        transform: scale(0.7);
+    }
+    100% {
+        transform: scale(1);
+    }
+}
+</style>

+ 152 - 0
packages/components/advance/tag/src/TagView.vue

@@ -0,0 +1,152 @@
+<script setup lang="ts">
+import { type PropType, onMounted, ref } from 'vue'
+import ShowTag from './showTag.vue'
+
+const props = defineProps({
+    tag: {
+        type: Object as PropType<TagContent>,
+        required: true,
+    },
+})
+
+const emit = defineEmits(['click'])
+
+const isShow = ref<boolean>(false)
+const isClick = ref<boolean>(false)
+const handleMouseEnter = (event: MouseEvent, tag: any, index?: number | undefined) => {
+    // console.log('in', event)
+    isShow.value = true
+}
+const handleMouseLeave = (event: MouseEvent, tag: any, index?: number | undefined) => {
+    // console.log('out', event)
+    isShow.value = false
+    // isClick.value = false
+}
+
+const handleTagClick = (tagId: string) => {
+    isClick.value = !isClick.value
+    emit('click', {
+        id: tagId,
+        isClick: isClick.value,
+    })
+}
+
+onMounted(() => {
+    console.log('tag', props.tag)
+})
+</script>
+
+<template>
+    <div
+        :data-tag-id="tag.sid"
+        :style="{ transform: `translate(${props.tag.x}px,${props.tag.y}px)`, display: props.tag.visible ? 'block' : 'none' }"
+        @mouseleave.prevent="handleMouseLeave($event, tag)"
+    >
+        <span
+            class="tag-icon animate"
+            style="background-image: url(http://4dkk.4dage.com/v4/sdk/4.2.2/images/tag_icon_default.svg)"
+            @click="handleTagClick(tag.sid)"
+            @mouseenter.prevent="handleMouseEnter($event, tag)"
+        />
+        <div class="content">
+            <div class="trans" :class="{ active: isShow || isClick }">
+                <template v-if="isShow || isClick">
+                    <div :id="`arrow_${tag.sid}`" class="arrow">
+                        <!-- <ui-icon @click.stop="closeTag" v-if="getApp().config.mobile" type="close"></ui-icon> -->
+                    </div>
+                    <ShowTag :id="tag.sid" :title="tag.title" :type="tag.type" :content="tag.content" :media="tag.media" />
+                    <!-- <TagInfo v-if="true" /> -->
+                    <!-- <ShowTag @click.stop="" v-if="!isEdit && hotData" @open="openInfo" /> -->
+                </template>
+            </div>
+        </div>
+    </div>
+</template>
+<style scoped>
+/* div {
+    position: absolute;
+} */
+</style>
+<style>
+[xui_tags_view] {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+}
+[xui_tags_view] > div {
+    pointer-events: all;
+    display: none;
+    position: absolute;
+    width: 48px;
+    height: 48px;
+    margin-left: -24px;
+    margin-top: -24px;
+    z-index: 1;
+}
+
+[xui_tags_view] > div.focus {
+    z-index: 2;
+}
+[xui_tags_view] > div.fixed {
+    z-index: 3;
+}
+[xui_tags_view] > div.active {
+    z-index: 4;
+}
+
+[xui_tags_view] .tag-icon {
+    display: block;
+    width: 48px;
+    height: 48px;
+    background-size: cover;
+    cursor: pointer;
+}
+
+[xui_tags_view] .tag-icon.animate {
+    animation: tag-animate-zoom 3s -1s linear infinite;
+}
+
+@keyframes tag-animate-zoom {
+    0% {
+        transform: scale(1);
+    }
+    50% {
+        transform: scale(0.7);
+    }
+    100% {
+        transform: scale(1);
+    }
+}
+
+[xui_tags_view] .content .trans {
+    padding: 0 40px 0 0;
+    position: absolute;
+    right: 35px;
+    top: 50%;
+    min-width: 420px;
+    min-height: 60px;
+    z-index: 1000;
+    transform: translateY(-50%) scale(0);
+    transform-origin: center right;
+    transition: all 0.3s cubic-bezier(0.35, 0.32, 0.65, 0.63);
+    pointer-events: none;
+    /* background-color: rgba(0, 0, 0, 0.8); */
+}
+
+[xui_tags_view] .content .trans .arrow {
+    pointer-events: auto;
+    content: '';
+    position: absolute;
+    top: 50%;
+    right: 0;
+    width: 0;
+    height: 0;
+    border-top: 15px solid transparent;
+    border-bottom: 15px solid transparent;
+    border-left: 40px solid rgba(27, 27, 28, 0.8);
+    transform: translateY(-50%);
+}
+[xui_tags_view] .content .trans.active {
+    transform: translateY(-50%) scale(1);
+}
+</style>

+ 75 - 0
packages/components/advance/tag/src/Tags.vue

@@ -0,0 +1,75 @@
+<script setup lang="ts">
+import { inject, ref } from 'vue'
+import TagView from './TagView.vue'
+import type { Ref } from 'vue'
+
+const __sdk: any = inject('__sdk')
+
+const tags: Ref<Array<any>> = ref([])
+
+__sdk.TagManager.on('loaded', (data: any) => __sdk.TagManager.load((tags.value = data) && tags.value))
+
+const handleTagview = (data: any) => {
+    console.log('tag', data)
+}
+</script>
+
+<template>
+    <Teleport v-if="tags.length" to=".kankan-plugins">
+        <div xui_tags_view>
+            <TagView v-for="(item, index) in tags" :key="index" :tag="item" @click="handleTagview" />
+        </div>
+    </Teleport>
+</template>
+
+<style scoped>
+[xui_tags_view] {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+}
+[xui_tags_view] > div {
+    pointer-events: all;
+    display: none;
+    position: absolute;
+    width: 48px;
+    height: 48px;
+    margin-left: -24px;
+    margin-top: -24px;
+    z-index: 1;
+}
+
+[xui_tags_view] > div.focus {
+    z-index: 2;
+}
+[xui_tags_view] > div.fixed {
+    z-index: 3;
+}
+[xui_tags_view] > div.active {
+    z-index: 4;
+}
+
+[xui_tags_view] .tag-icon {
+    display: block;
+    width: 48px;
+    height: 48px;
+    background-size: cover;
+    cursor: pointer;
+}
+
+[xui_tags_view] .tag-icon.animate {
+    animation: tag-animate-zoom 3s -1s linear infinite;
+}
+
+@keyframes tag-animate-zoom {
+    0% {
+        transform: scale(1);
+    }
+    50% {
+        transform: scale(0.7);
+    }
+    100% {
+        transform: scale(1);
+    }
+}
+</style>

+ 5 - 0
packages/components/advance/tag/src/metas/demo.vue

@@ -0,0 +1,5 @@
+<template>
+    <div>test</div>
+</template>
+<script lang="ts" setup></script>
+<style></style>

+ 382 - 0
packages/components/advance/tag/src/metas/metasImage.vue

@@ -0,0 +1,382 @@
+<!--  -->
+<template>
+    <div class="pic-box" :class="{ show: viewer }" :style="metasHeight ? `height:${metasHeight}px;` : ''">
+        <div>
+            <div v-if="currentIndex != 0" class="ctrl-btn left-btn" @click.stop="changeImage('pre')">
+                <ui-icon type="left" />
+            </div>
+            <div v-if="currentIndex < data.length - 1" class="ctrl-btn right-btn" @click.stop="changeImage('next')">
+                <ui-icon type="right" />
+            </div>
+        </div>
+        <div class="over-box">
+            <div class="image-list" :style="`transform:translateX(${-100 * currentIndex}%);`">
+                <div v-for="(i, index) in data" :key="index" class="image-item" :style="`transform:translateX(${100 * index}%);background-image:url(${changeUrl(i.src)});`" />
+            </div>
+            <ui-icon class="loading-icon" type="_loading_" />
+            <!-- <div class="del-btn">
+                <ui-icon type="del"></ui-icon>
+            </div> -->
+        </div>
+        <div class="continue">
+            <span class="pic-num">
+                <span class="cur">{{ currentIndex + 1 }}</span>
+                <span><span>&nbsp;</span>/<span>&nbsp;</span></span>
+                <span>{{ data.length }}</span>
+            </span>
+        </div>
+        <!-- 移动端缩放 -->
+    </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, inject, ref } from 'vue'
+import { buildProps } from '@kankan-components/utils'
+import { UIIcon } from '@kankan-components/components'
+import type { PropType } from 'vue'
+
+const currentIndex = ref(0)
+
+const props = buildProps({
+    metasHeight: {
+        type: Number,
+        default: null,
+    },
+    data: {
+        type: Array as PropType<SourceType[]>,
+        default: () => [],
+    },
+    viewer: {
+        type: Boolean,
+        default: false,
+    },
+    scale: {
+        type: Boolean,
+        default: false,
+    },
+})
+
+export default defineComponent({
+    name: 'MetaImage',
+    components: {
+        'ui-icon': UIIcon,
+    },
+    props,
+    setup() {
+        const __sdk: any = inject('__sdk')
+        function changeUrl(name: string, now?: string) {
+            if (name.includes('http')) {
+                return name
+            } else {
+                if ((typeof name === 'string' && name.slice(0, 4) == 'blob') || (typeof name === 'string' && name.slice(0, 10) == 'data:image')) {
+                    return name
+                } else {
+                    return __sdk.resource.getUserResourceURL(name, false, now)
+                }
+            }
+        }
+        const changeImage = (type: 'pre' | 'next') => {
+            if (type == 'pre') {
+                currentIndex.value--
+            } else {
+                currentIndex.value++
+            }
+        }
+
+        return {
+            changeUrl,
+            changeImage,
+            currentIndex,
+        }
+    },
+})
+</script>
+<style lang="scss" scoped>
+.showPicBox {
+    width: 100%;
+    height: 100%;
+    position: fixed;
+    z-index: 10000;
+    background: rgb(24, 22, 22);
+    top: 0;
+    left: 0;
+    .close {
+        position: absolute;
+        top: 10px;
+        right: 10px;
+        width: 20px;
+        height: 20px;
+        z-index: 100;
+        color: #fff;
+        .iconfont {
+            font-size: 20px;
+        }
+    }
+    .loading {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+    }
+    .imgbox {
+        width: 100%;
+        height: 100%;
+        background-repeat: no-repeat;
+        background-size: contain;
+        background-position: center center;
+        #eleImg {
+            // position: absolute;
+
+            // top: 50%;
+            // left: 50%;
+            // transform: translate(-50%, -50%);
+            margin: 0 auto;
+            display: block;
+            &.s {
+                height: 100%;
+                width: auto;
+            }
+            &.h {
+                height: auto;
+                width: 100%;
+            }
+        }
+    }
+}
+.del-btn {
+    width: 24px;
+    height: 24px;
+    background: rgba(0, 0, 0, 0.6);
+    border-radius: 50%;
+    position: absolute;
+    cursor: pointer;
+    top: 10px;
+    right: 10px;
+    z-index: 10;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+.loading-icon {
+    color: var(--editor-main-color);
+    animation: rotate 2s infinite linear;
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    font-size: 30px;
+}
+@keyframes rotate {
+    0% {
+        transform: translate(-50%, -50%) rotate(0deg);
+    }
+    100% {
+        transform: translate(-50%, -50%) rotate(360deg);
+    }
+}
+.pic-box {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    border-radius: 4px;
+    border: 1px solid rgba(255, 255, 255, 0.2);
+    top: 0;
+    left: 0;
+    z-index: 10;
+
+    .over-box {
+        width: 100%;
+        height: 100%;
+        overflow: hidden;
+    }
+    .continue {
+        width: 100%;
+        height: 32px;
+        background: linear-gradient(180deg, rgba(0, 0, 0, 0.1) 0%, #000000 200%);
+        border-radius: 0px 0px 4px 4px;
+        position: absolute;
+        bottom: 0;
+        left: 0;
+
+        .ui-input {
+            width: 100%;
+        }
+        .continue-tips {
+            font-size: 12px;
+            margin-right: 5px;
+        }
+        .edit-pic-num {
+            // position: absolute;
+            // right: 10px;
+            font-size: 12px;
+            .cur {
+                color: var(--editor-main-color);
+            }
+        }
+        .pic-num {
+            position: absolute;
+            right: 10px;
+            top: 50%;
+            transform: translateY(-50%);
+            font-size: 12px;
+            .cur {
+                color: var(--editor-main-color);
+            }
+        }
+    }
+
+    .ctrl-btn {
+        width: 32px;
+        height: 32px;
+        background: rgba(0, 0, 0, 0.2);
+        border-radius: 50%;
+        position: absolute;
+        cursor: pointer;
+        top: 50%;
+        transform: translateY(-50%);
+        z-index: 10;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        .iconfont {
+            font-size: 14px;
+        }
+        &.left-btn {
+            left: 5px;
+        }
+        &.right-btn {
+            right: 5px;
+        }
+    }
+    .image-list {
+        width: 100%;
+        height: 100%;
+        position: relative;
+        transition: all 0.3s linear;
+        .image-item {
+            width: 100%;
+            height: 100%;
+            // background: red;
+            position: absolute;
+            transform: translateX(0);
+            text-align: center;
+            background-repeat: no-repeat;
+            background-size: contain;
+            background-position: center;
+            img {
+                height: 100%;
+                width: auto;
+            }
+        }
+    }
+    &.show {
+        .ctrl-btn {
+            width: 40px;
+            height: 80px;
+            background: rgba(0, 0, 0, 0.6);
+            .iconfont {
+                font-size: 20px;
+            }
+            &.left-btn {
+                left: 0px;
+                border-radius: 0 40px 40px 0;
+                .icon {
+                    margin-right: 5px;
+                }
+            }
+            &.right-btn {
+                right: 0px;
+                border-radius: 40px 0 0 40px;
+                .icon {
+                    margin-left: 8px;
+                }
+            }
+        }
+        .continue {
+            width: 76px;
+            height: 36px;
+            background: rgba(0, 0, 0, 0.6);
+            border-radius: 20px;
+            position: absolute;
+            bottom: -5%;
+            left: 50%;
+            transform: translateX(-50%);
+
+            .pic-num {
+                width: 76px;
+                height: 36px;
+                display: inline-block;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                font-size: 20px;
+                top: 50%;
+                left: 50%;
+                transform: translate(-50%, -50%);
+                span {
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                }
+            }
+        }
+    }
+}
+
+[is-mobile] {
+    .pic-box {
+        &.show {
+            .ctrl-btn {
+                width: 40px;
+                height: 80px;
+                background: rgba(0, 0, 0, 0.6);
+                .iconfont {
+                    font-size: 20px;
+                }
+                &.left-btn {
+                    left: 0px;
+                    border-radius: 0 40px 40px 0;
+                    .icon {
+                        margin-right: 5px;
+                    }
+                }
+                &.right-btn {
+                    right: 0px;
+                    border-radius: 40px 0 0 40px;
+                    .icon {
+                        margin-left: 8px;
+                    }
+                }
+            }
+            .continue {
+                width: 76px;
+                height: 36px;
+                background: rgba(0, 0, 0, 0.6);
+                border-radius: 20px;
+                position: absolute;
+                bottom: -6%;
+                left: 50%;
+                transform: translateX(-50%);
+
+                .pic-num {
+                    width: 76px;
+                    height: 36px;
+                    display: inline-block;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    font-size: 20px;
+                    top: 50%;
+                    left: 50%;
+                    transform: translate(-50%, -50%);
+                    span {
+                        display: flex;
+                        align-items: center;
+                        justify-content: center;
+                    }
+                }
+            }
+        }
+    }
+}
+</style>

+ 132 - 0
packages/components/advance/tag/src/metas/metasVideo.vue

@@ -0,0 +1,132 @@
+<template>
+    <div class="video-box">
+        <video
+            v-for="(i, index) in data"
+            id="video"
+            :key="index"
+            class="video-item"
+            x5-video-player-type="h5-page"
+            controlslist="nodownload"
+            disable-picture-in-picture
+            webkit-playsinline=""
+            x-webkit-airplay=""
+            playsinline
+            :controls="controls"
+            autoplay
+            :src="changeUrl(i.src)"
+            @error="filesError"
+        />
+    </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, inject, nextTick, onMounted, ref } from 'vue'
+import { buildProps } from '@kankan-components/utils'
+import type { PropType } from 'vue'
+
+const props = buildProps({
+    controls: {
+        type: Boolean,
+        default: true,
+    },
+    metasHeight: {
+        type: Number,
+        default: null,
+    },
+    data: {
+        type: Array as PropType<SourceType[]>,
+        default: () => [],
+    },
+})
+const loading = ref(true)
+
+export default defineComponent({
+    name: 'UIMetasVideo',
+    props,
+    setup: () => {
+        const filesError = () => {
+            loading.value = false
+            // Dialog.toast({
+            //     content: '视频文件加载失败',
+            //     type: 'warn',
+            // })
+        }
+        const __sdk: any = inject('__sdk')
+        function changeUrl(name: string, now?: string) {
+            if (name.includes('http')) {
+                return name
+            } else {
+                if ((typeof name === 'string' && name.slice(0, 4) == 'blob') || (typeof name === 'string' && name.slice(0, 10) == 'data:image')) {
+                    return name
+                } else {
+                    return __sdk.resource.getUserResourceURL(name, false, now)
+                }
+            }
+        }
+        onMounted(() => {
+            nextTick(() => {
+                const myVideo: HTMLElement | null = document.querySelector('#video')
+                if (myVideo) {
+                    myVideo.oncanplay = function () {
+                        loading.value = false
+                    }
+                }
+            })
+        })
+
+        return {
+            filesError,
+            changeUrl,
+        }
+    },
+})
+</script>
+<style lang="scss" scoped>
+.del-btn {
+    width: 24px;
+    height: 24px;
+    background: rgba(0, 0, 0, 0.3);
+    border-radius: 50%;
+    position: absolute;
+    cursor: pointer;
+    top: 10px;
+    right: 10px;
+    z-index: 10;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+.video-box {
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    position: absolute;
+    border-radius: 4px;
+    border: 1px solid rgba(255, 255, 255, 0.2);
+    top: 0;
+    left: 0;
+    z-index: 10;
+    .loading-icon {
+        color: var(--editor-main-color);
+        animation: rotate 2s infinite linear;
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        font-size: 30px;
+    }
+    @keyframes rotate {
+        0% {
+            transform: translate(-50%, -50%) rotate(0deg);
+        }
+        100% {
+            transform: translate(-50%, -50%) rotate(360deg);
+        }
+    }
+    .video-item {
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+    }
+}
+</style>

+ 53 - 0
packages/components/advance/tag/src/props.ts

@@ -0,0 +1,53 @@
+import { buildProps } from '@kankan-components/utils'
+// import type { ExtractPropTypes } from 'vue'
+// import type bubble from './bubble.vue'
+
+// isPlay: boolean
+// flying: boolean
+// leaveId: string
+// isEdit: boolean
+// isFixed: boolean
+// isClick: boolean
+// enterVisible: boolean
+// editPosition: boolean
+// toggleIndex: number
+// flyClose?: boolean
+
+export const tagStatusProps = buildProps({
+    isPlay: {
+        type: Boolean,
+        default: false,
+    },
+    flying: {
+        type: Boolean,
+        default: false,
+    },
+    leaveId: {
+        type: String,
+        default: '',
+    },
+    isEdit: {
+        type: Boolean,
+        default: false,
+    },
+    isFixed: {
+        type: Boolean,
+        default: false,
+    },
+    enterVisible: {
+        type: Boolean,
+        default: false,
+    },
+    editPosition: {
+        type: Boolean,
+        default: false,
+    },
+    toggleIndex: {
+        type: Number,
+        default: 0,
+    },
+    flyClose: {
+        type: Boolean,
+        default: false,
+    },
+})

+ 216 - 0
packages/components/advance/tag/src/showTag.vue

@@ -0,0 +1,216 @@
+<template>
+    <div :id="`tagBox_${id}`" class="show-tag">
+        <div class="tag-title">
+            <h2>
+                {{ title }}
+            </h2>
+        </div>
+        <div class="desc">
+            <div class="text" v-html="content" />
+        </div>
+        <div class="tag-metas">
+            <metasImage v-if="type == 'image'" :data="media.image" />
+            <MetasVideo v-if="type == 'video'" :data="media.video" />
+            <!-- <metasImage v-if="hotData.type == 'image'" />
+            <metasVideo v-if="hotData.type == 'video'" />
+            <metasWeb v-if="hotData.type == 'link'" /> -->
+        </div>
+    </div>
+</template>
+<script lang="ts">
+import { type PropType, defineComponent } from 'vue'
+import { buildProps } from '@kankan-components/utils'
+import MetasImage from './metas/metasImage.vue'
+import MetasVideo from './metas/metasVideo.vue'
+// import demo from './metas/demo.vue'
+
+const props = buildProps({
+    id: {
+        type: String,
+        default: '',
+        required: true,
+    },
+    title: {
+        type: String,
+        default: '',
+        required: true,
+    },
+    type: {
+        type: String as PropType<TagType>,
+        default: '',
+        required: true,
+    },
+    media: {
+        type: Object as PropType<TagContent['media']>,
+        default: null,
+    },
+    content: {
+        type: String,
+        default: '',
+        required: true,
+    },
+})
+
+export default defineComponent({
+    name: 'UIShowTag',
+    components: { MetasImage, MetasVideo },
+    props,
+    setup(props) {
+        console.log('')
+    },
+})
+</script>
+
+<style lang="scss" scoped>
+.show-tag {
+    pointer-events: auto;
+    background: rgba(27, 27, 28, 0.8);
+    border-radius: 4px;
+    // border: 1px solid #000000;
+    // backdrop-filter: blur(4px);
+    min-width: 400px;
+    // min-height: 100px;
+    padding: 30px 20px;
+    max-height: 50vh;
+    overflow-y: auto;
+    .edit-btn {
+        margin-top: 20px;
+        text-align: right;
+        span {
+            font-size: 14px;
+            color: var(--editor-font-color);
+            cursor: pointer;
+            &:hover {
+                color: #fff;
+            }
+        }
+    }
+    .tag-metas {
+        width: 100%;
+        height: 224px;
+        background: rgba(255, 255, 255, 0.1);
+        border-radius: 4px;
+        overflow: hidden;
+        position: relative;
+        cursor: -webkit-zoom-in;
+        margin-top: 20px;
+        &.nocursor {
+            cursor: auto;
+        }
+        &.mask {
+            &::after {
+                content: '';
+                position: absolute;
+                top: 0;
+                left: 0;
+                width: 100%;
+                height: 100%;
+                z-index: 100;
+            }
+        }
+    }
+    .tag-title {
+        word-break: break-all;
+        h2 {
+            font-size: 20px;
+            // margin-bottom: 10px;
+            line-height: 30px;
+            color: #ffffff;
+            position: relative;
+            .ui-audio {
+                float: right;
+                &.audio {
+                    display: inline-block;
+                    cursor: pointer;
+                }
+            }
+        }
+    }
+    .desc {
+        margin-top: 10px;
+        .text {
+            font-size: 14px;
+            color: #999999;
+            line-height: 20px;
+            text-align: justify;
+            word-break: break-all;
+        }
+    }
+}
+[is-mobile] {
+    .show-tag {
+        pointer-events: auto;
+        background: rgba(27, 27, 28, 0.8);
+        border-radius: 0.0533rem;
+        // border: 1px solid #000000;
+        // backdrop-filter: blur(0.0533rem);
+        min-width: 7.4667rem;
+        // min-height: 4rem;
+        padding: 0.4rem 0.2667rem;
+
+        .edit-btn {
+            margin-top: 0.2667rem;
+            text-align: right;
+            span {
+                font-size: 0.1867rem;
+                color: var(--editor-font-color);
+                cursor: pointer;
+                &:hover {
+                    color: #fff;
+                }
+            }
+        }
+        .tag-metas {
+            width: 100%;
+            height: 4.2667rem;
+            background: rgba(255, 255, 255, 0.1);
+            border-radius: 0.0533rem;
+            overflow: hidden;
+            position: relative;
+            cursor: -webkit-zoom-in;
+            margin-top: 0.4rem;
+            &.mask {
+                &::after {
+                    content: '';
+                    position: absolute;
+                    top: 0;
+                    left: 0;
+                    width: 100%;
+                    height: 100%;
+                    z-index: 100;
+                }
+            }
+        }
+        .tag-title {
+            h2 {
+                font-size: 0.5333rem;
+                line-height: 0.8rem;
+                color: #ffffff;
+                position: relative;
+                .ui-audio {
+                    float: right;
+                    &.audio {
+                        display: inline-block;
+                        cursor: pointer;
+                    }
+                }
+            }
+        }
+        .desc {
+            margin-bottom: 0.2933rem;
+
+            .text {
+                font-size: 0.3733rem;
+                color: #999999;
+                line-height: 0.2533rem;
+                text-align: justify;
+                line-height: 0.5333rem;
+
+                p {
+                    line-height: 0.5333rem;
+                }
+            }
+        }
+    }
+}
+</style>

+ 42 - 0
packages/components/advance/tag/tag.d.ts

@@ -0,0 +1,42 @@
+type TagType = 'image' | 'video'
+
+type SourceType = {
+    src: string
+}
+
+interface GlobalTag {
+    isPlay: boolean
+    flying: boolean
+    leaveId: string
+    isEdit: boolean
+    isFixed: boolean
+    isClick: boolean
+    enterVisible: boolean
+    editPosition: boolean
+    toggleIndex: number
+    flyClose?: boolean
+}
+
+interface TagContent {
+    content: string
+    createTime: EpochTimeStamp
+    floorIndex: number
+    icon: string
+    panoId: string
+    position: {
+        x: number
+        y: number
+        z: number
+    }
+    media: {
+        image?: SourceType[]
+        video?: SourceType[]
+    }
+    sid: string
+    title: string
+    type: TagType
+    visible: boolean
+    x: number
+    y: number
+    visiblePanos?: any[]
+}

+ 1 - 0
packages/components/index.ts

@@ -1 +1,2 @@
 export * from './basic'
+export * from './advance'

+ 3 - 13
playground/src/App.vue

@@ -1,11 +1,6 @@
 <script setup lang="ts">
 import { onMounted, inject } from 'vue'
-import { UIAudio, UIButton, UIIcon, UIInput } from '@kankan-components/components'
-import Tags from './components/tag'
-// import { buildProps } from '@kankan-components/utils';
-// import { h } from 'vue';
-// import * as KanKanSDK from '@kankan/sdk';
-// console.log('UI', UIAudio, UIIcon)
+import { UITag } from '@kankan-components/components'
 
 onMounted(async () => {
     const __sdk: any = inject('__sdk')
@@ -15,14 +10,9 @@ onMounted(async () => {
 
 <template>
     <div id="scene">
-        <Tags />
-    </div>
-    <div id="scene-front">
-        <!-- <UIAudio src="http://samplelib.com/lib/preview/mp3/sample-3s.mp3" /> -->
-        <!-- <UIButton>djdjddd</UIButton> -->
-        <!-- <UIIcon type="checkbox" :size="129" />
-        <UIInput type="radio" /> -->
+        <UITag />
     </div>
+    <div id="scene-front"></div>
 </template>
 
 <style scoped>