随着最近openAi推出了Iphone版ChatGPT利用APP。标记着chatgpt曾经步入了挪动畛域。

空闲之余使用vite4.x构建了一个mobile版chatgpt聊天实例。

vue3-mobileGPT 反对红色/暗黑两种主题模式。

技术栈

  • 编辑器:Cursor
  • 框架技术:vite4+vue3+vue-router+pinia2
  • 组件库:Vant^4.3.1 + ve-plus^0.3.2
  • 代码格式化:highlight.js^11.7.0
  • markdown组件:vue3-markdown-it
  • 数据存储:pinia-plugin-persistedstate^3.1.0
  • 款式解决:sass^1.62.1

我的项目构造

基于vite4构建我的项目,采纳vue3 setup编码开发。

vue3组件库

vant 一款轻量级、可定制化的挪动端UI组件库。目前 Vant 官网提供了 Vue 2 版本、Vue 3 版本和微信小程序版本。vant4反对vue3版本。

# Vue 3 我的项目,装置最新版 Vantnpm i vant# Vue 2 我的项目,装置 Vant 2npm i vant@latest-v2

https://vant-contrib.gitee.io/vant/#/zh-CN/

入口配置main.js

main.js引入总款式、路由/状态治理、公共组件配置。

import { createApp } from 'vue'import './style.scss'import App from './App.vue'import Router from './router'import Store from './store'import Plugins from './plugins'const app = createApp(App)app.use(Router)app.use(Store)app.use(Plugins)app.mount('#app')

引入公共插件

新建一个plugins/index.js文件,用来引入一些公共配置文件。

/** * 插件配置 * @author YXY */import '@assets/js/fontSize'// 引入Vant4.x组件库import Vant from 'vant'import 'vant/lib/index.css'// 引入ve-plus组件库import VEPlus from 've-plus'import 've-plus/dist/ve-plus.css'// 引入vue3弹框组件import V3Popup from '@/components/v3popup'import Navbar from '@/components/Navbar.vue'const Plugins = (app) => {    app.use(Vant)    app.use(VEPlus)    app.use(V3Popup)    // 注册导航栏组件    app.component('Navbar', Navbar)}export default Plugins

vue3-mobilegpt聊天框

我的项目中聊天框应用了ve-plus组件库中的Input组件,反对type=textarea多行文本。可自定义后缀插槽模板。

<template>    <div class="vgpt__editor">        <div class="vgpt__editor-inner flexbox">            <Input                class="flex1"                ref="editorRef"                v-model="editorText"                type="textarea"                :autosize="{maxRows: 6}"                clearable                placeholder="Prompt..."                @keydown="handleKeydown"                @clear="handleClear"                style="margin: 0 5px;"            >                <template #suffix>                    <Button class="btn" type="link" @click.stop>                        <Icon name="ve-icon-image" size="16" cursor />                        <input ref="uploadImgRef" type="file" title="" accept="image/*" @change="handleUploadImage" />                    </Button>                    <van-popover v-model:show="showPopover" placement="top">                        <template #reference>                            <Button class="btn" type="link" icon="ve-icon-audio"></Button>                        </template>                        <div class="flexbox flex-alignc flex-col" style="padding: 15px 0; min-width: 120px;">                            <Icon name="ve-icon-yuyin" size="40" color="#0fa27e" />                            <p class="fs-12 mb-15 c-999">网络不给力</p>                            <van-button size="mini"><i></i>开始讲话</van-button>                        </div>                    </van-popover>                </template>            </Input>            <Button type="primary" icon="ve-icon-arrowup" circle :disabled="!editorText" @click="handleSubmit"></Button>        </div>    </div></template>
<script setup>    import { ref, watch } from 'vue'    import { guid } from '@/utils'    import { chatStore } from '@/store/modules/chat'    const props = defineProps({        value: { type: [String, Number] }    })    const emit = defineEmits(['clear'])    const chatState = chatStore()        const showPopover = ref(false)    const uploadImgRef = ref()    const editorRef = ref()    const editorText = ref(props.value)    watch(() => props.value, () => {        editorText.value = props.value    })    // 发送会话    const handleSubmit = () => {        // editorRef.value.focus()        if(!editorText.value) return        let data = {            type: 'text',            role: 'ME',            key: guid(),            content: editorText.value        }        chatState.addSession(data)        // 清空        editorText.value = ''    }        // 抉择图片    const handleUploadImage = () => {        let file = uploadImgRef.value.files[0]        if(!file) return        let size = Math.floor(file.size / 1024)        if(size > 2*1024) {            Message.danger('图片大小不能超过2M')            uploadImgRef.value.value = ''            return false        }        let reader = new FileReader()        reader.readAsDataURL(file)        reader.onload = function() {            let img = this.result            let data = {                type: 'image',                role: 'ME',                key: guid(),                content: img            }            chatState.addSession(data)        }    }        // ...</script>

store/chat.js进行聊天状态本地存储管理。

/** * 聊天状态存储管理 * @author YXY * @contact Q:282310962 */import { defineStore } from 'pinia'import { guid, isEmpty } from '@/utils'export const chatStore = defineStore('chat', {    state: () => ({        sessionId: '',        session: []    }),    actions: {        // 创立新会话        createSession(ssid) {            this.sessionId = ssid            this.session.push({                sessionId: ssid,                title: '',                data: []            })        },        // 新增会话        addSession(message) {            // 判断以后会话uuid是否存在,不存在创立新会话            if(!this.sessionId) {                const ssid = guid()                this.createSession(ssid)            }            this.session.map(item => {                if(item.sessionId == this.sessionId) {                    if(!item.title) {                        item.title = message.content                    }                    item.data.push(message)                }            })        },        // 获取会话        getSession() {            return this.session.find(item => item.sessionId == this.sessionId)        },        // 移除会话        removeSession(ssid) {            const index = this.session.findIndex(item => item?.sessionId === ssid)            if(index > -1) {                this.session.splice(index, 1)            }            this.sessionId = ''        },        // 删除某一条会话        deleteSession(ssid) {            this.session.map(item => {                if(item.sessionId == this.sessionId) {                    if(item.data && !isEmpty(item.data)) {                        item.data.map((it, key) => {                            if(it.key == ssid) {                                item.data.splice(key, 1)                            }                        })                    }                }            })        },        // 清空会话        clearSession() {            this.session = []            this.sessionId = ''        }    },    // 本地长久化存储(默认存储localStorage)    persist: true})

Oker,以上就是vue3模拟chatgpt挪动端的一些简略分享,心愿对大家有所帮忙。

https://segmentfault.com/a/1190000040711708
https://segmentfault.com/a/1190000043667464