SOURCE

console 命令行工具 X clear

                    
>
console
/**
 * PetiteVue 组件化示例
 * 
 * 适用范围:
 * - 写一些简单活动/消息/贺词等页面
 * - 想要使用数据双向绑定
 * - 不想引入整个vue或脚手架
 * - 无需编译,也可以走vite编译
 * - 想把组件直接拷到别的项目用
 * - 不想用虚拟DOM
 * 
 */

import { createApp, reactive, nextTick } from 'https://ax.minicg.com/pvue.es.js';
const { log, dir, table, clear, warn, error } = console; clear();

// 内部全局状态
const State = reactive({
    events: new EventTarget(),
    keywords: '',
    filteredData: null,
})

// 组件化
createApp({
    State,
    Root,
    Header,
    Search,
    News,
}).mount()

function Root(props={}) {
    const { appName, api } = props
    return {
        $template: `
            <div class="flex flex-col w-[375px] h-[667px] bg-white rounded-3xl overflow-hidden shadow-xl" @vue:mounted="mounted">
                <div v-scope="Header({ title:appName })"></div>
                <div v-scope="Search()"></div>
                <div v-scope="News({api:'${api}'})" v-if="showNews" class="flex-1 overflow-y-auto pb-12" ref="news"></div>
                <img v-else class="m-auto pointer-events-none" src="https://ax.minicg.com/no-data.svg" />
                <button class="w-full p-4 text-sm text-blue-500 bg-white/90 backdrop-blur shadow !outline-none hover:underline active:bg-gray-50" @click="toggleNews">显示/隐藏新闻</button>
            </div>
        `,
        appName, /* 通过参数传递变量到子组件 */
        showNews: true,
        toggleNews() {
            /* 测试卸载News组件 */
            this.showNews = !this.showNews
        },
        mounted() {
            log('Root mounted', this)
        }
    }
}

function Header(props={}) {
    const { title } = props
    return {
        $template: `
            <div class="flex justify-between items-start px-4 pt-6 pb-2 font-medium text-2xl text-black" @vue:mounted="mounted">
                <div class="flex flex-col justify-center gap-1">
                    <h2>{{title}}</h2>
                    <p class="text-xs text-black/40">Latest updated: {{new Date().toISOString().split('T')[0]}}</p>
                </div>
                <i class="ri-refresh-line text-xl px-1 cursor-pointer duration-200 hover:rotate-180 active:text-blue-500" @click="refresh"></i>
            </div>
        `,
        title,
        refresh() {
            State.events.dispatchEvent( new Event('REFRESH_NEWS') )
        },
        mounted() {
            log('Header mounted', this)
        }
    }
}

function Search(props={}) {
    return {
        $template: `
            <div class="px-4 py-2" @vue:mounted="mounted">
                <div class="flex gap-2 px-4 items-center bg-gray-100 rounded-full">
                    <i class="ri-search-line"></i>
                    <input class="w-full px-0 py-3 text-gray-600 border-0 !ring-0 bg-transparent text-sm"
                        type="text" placeholder="Search..."
                        v-model="State.keywords"
                    />
                    <i v-show="State.keywords!==''" class="ri-close-circle-fill text-black/20 duration-200 cursor-pointer hover:text-black" @click="State.keywords=''"></i>
                </div>
            </div>
        `,
        mounted() {
            log('Search mounted', this)
        }
    }
}

function News(props={}) {
    const { api } = props
    return {
        $template: `
            <div class="flex h-full" @vue:mounted="mounted" @vue:unmounted="unmounted">
                <ul v-show="!isLoading" class="flex flex-col text-gray-700 cursor-pointer" v-effect="onFilter(State.keywords)">
                    <li v-for="item in filteredData" class="flex p-4 gap-3 duration-200 hover:bg-gray-50 active:bg-blue-50" @click="window.open(item.url)">
                        <div class="w-[100px] h-[100px] rounded-lg overflow-hidden">
                            <img class="w-full h-full object-cover" :src="item.pic">
                        </div>
                        <div class="flex-1 flex flex-col gap-2 justify-between py-1">
                            <div class="flex flex-col gap-2">
                                <h3 class="line-clamp-1">{{item.title}}</h3>
                                <p class="text-xs text-gray-500 line-clamp-2">{{item.desc}}</p>
                            </div>
                            <p class="text-xs text-gray-400">热度: {{ (Number(item.hot)/10000).toFixed(1) }}万</p>
                        </div>
                    </li>
                </ul>
                <i v-show="isLoading" class="ri-loader-line text-blue-500 text-3xl p-5 m-auto animate-spin"></i>
            </div>
        `,
        api,
        data: [],
        filteredData: [],
        isLoading: false,
        mounted() {
            log('News mounted', this)
            this.loadData()

            State.events.addEventListener('REFRESH_NEWS', this.onRefresh)
        },
        onFilter(keywords) {
            this.filteredData = this.data.filter(item => ['title', 'desc'].some(key => item[key].includes(keywords)))
        },
        onRefresh(e) {
            log('onRefresh')
            this.loadData()
        },
        async loadData(e) {
            this.isLoading = true
            this.data = []
            const resp = await fetch(this.api)
            const data = await resp.json()
            
            // 字段映射(如果api字段不一样的话)
            this.data = data.data.map(item=>{
              return {
                pic:   item.pic,
                title: item.title,
                desc:  item.desc,
                hot:   item.hot
              }
            })
            
            // window.addEventListener('hashchange', () => {
            //     State.keywords = decodeURIComponent(window.location.hash.slice(1))
            // })
            this.filteredData = this.data
            
            this.isLoading = false
        },
        unmounted() {
            log('News unmounted')
            State.events.removeEventListener("REFRESH_NEWS", this.onRefresh);
        }
    }
}
<div v-scope="Root({
    appName: '百度新闻',
    api: 'https://hot.cigh.cn/baidu'
})"></div>
html, body {
    display: flex;
    width: 100%;
    height: 100%;
    justify-content: center;
    align-items: center;
}

本项目引用的自定义外部资源