Bootstrap

基于vue3+pinia2仿ChatGPT聊天实例|vite4.x仿chatgpt界面

原文:基于vue3+pinia2仿ChatGPT聊天实例|vite4.x仿chatgpt界面

基于vue3+pinia2仿ChatGPT聊天实例|vite4.x仿chatgpt界面

使用vue3+pinia2开发仿制chatgpt界面聊天实例Vue3-Chatgpt

基于Vue3.x+Pinia2+VueRouter+Vue3-Markdown等技术构建仿ChatGPT网页端聊天程序。支持经典+分栏界面布局、light/dark模式、全屏+半屏显示、Markdown语法解析、侧边栏隐藏等功能。

在这里插入图片描述

在这里插入图片描述

技术框架

  • 编辑工具Cursor
  • 框架技术Vue3+Vite4.x+Pinia2
  • 组件库VEPlus (基于vue3桌面端组件库)
  • 国际化多语言vue-i18n^9.2.2
  • 代码高亮highlight.js^11.7.0
  • 本地存储pinia-plugin-persistedstate^3.1.0
  • markdown解析vue3-markdown-it

在这里插入图片描述

在这里插入图片描述

项目目录结构

在这里插入图片描述


 

在这里插入图片描述


 

在这里插入图片描述


 

在这里插入图片描述


 

在这里插入图片描述


 

在这里插入图片描述


 

在这里插入图片描述


 

在这里插入图片描述


 

在这里插入图片描述


 

在这里插入图片描述


 

在这里插入图片描述


 

在这里插入图片描述


 

在这里插入图片描述

在这里插入图片描述

vite.config.js配置

TypeScript

import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { parseEnv } from './src/utils/env'

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
	const viteEnv = loadEnv(mode, process.cwd())
	const env = parseEnv(viteEnv)

	return {
		plugins: [vue()],

		// base: '/',
		// mode: 'development', // development|production

		/*构建选项*/
		build: {
			// minify: 'esbuild', // 打包方式 esbuild(打包快)|terser
			// chunkSizeWarningLimit: 2000, // 打包大小警告
			// rollupOptions: {
			// 	output: {
			// 		chunkFileNames: 'assets/js/[name]-[hash].js',
			// 		entryFileNames: 'assets/js/[name]-[hash].js',
			// 		assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
			// 	}
			// }
		},
		esbuild: {
			// 打包去除 console.log 和 debugger
			drop: env.VITE_DROP_CONSOLE ? ['console', 'debugger'] : []
		},

		/*开发服务器选项*/
		server: {
			// 端口
			port: env.VITE_PORT,
			// 是否浏览器自动打开
			open: env.VITE_OPEN,
			// 开启https
			https: env.VITE_HTTPS,
			// 代理配置
			proxy: {
				// ...
			}
		},

		resolve: {
			// 设置别名
			alias: {
				'@': resolve(__dirname, 'src'),
				'@assets': resolve(__dirname, 'src/assets'),
				'@components': resolve(__dirname, 'src/components'),
				'@views': resolve(__dirname, 'src/views'),
				// 解决vue-i18n警告提示You are running the esm-bundler build of vue-i18n.
				'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js'
			}
		}
	}
})

main.js主入口

TypeScript

import { createApp } from 'vue'
import App from './App.vue'

// 引入Router和Store
import Router from './router'
import Store from './store'

// 引入插件配置
import Plugins from './plugins'

const app = createApp(App)

app
.use(Router)
.use(Store)
.use(Plugins)
.mount('#app')

vue3.x组件库

项目中使用的组件库是基于vue3自定义UI组件库Ve-plus。一个支持40+组件的轻量级组件库。

在这里插入图片描述

在这里插入图片描述


安装组件

TypeScript

yarn add ve-plus
npm i ve-plus --save

基于vue3.js自定义pc端组件库VePlus_xiaoyan_2018的博客-CSDN博客

整体布局

项目支持2种布局模式整体分为顶栏+侧边栏+主体内容三大模块构成。

在这里插入图片描述

在这里插入图片描述

TypeScript

<div class="ve__layout-body flex1 flexbox">
	<!-- //中间栏 -->
	<div class="ve__layout-menus flexbox" :class="{'hidden': store.config.collapse}">
		<aside class="ve__layout-aside flexbox flex-col">
			<ChatNew />
			<Scrollbar class="flex1" autohide size="4" gap="1">
				<ChatList />
			</Scrollbar>
			<ExtraLink />
			<Collapse />
		</aside>
	</div>

	<!-- //右边栏 -->
	<div class="ve__layout-main flex1 flexbox flex-col">
		<!-- 主内容区 -->
		<Main />
	</div>
</div>

TypeScript

<template>
	<div class="vegpt__editor">
		<div class="vegpt__editor-inner">
			<Flex :gap="0">
				<Popover placement="top" trigger="click" width="150">
					<Button class="btn" type="link" icon="ve-icon-yuyin1" v-tooltip="{content: '发送语音', theme: 'light', arrow: false}"></Button>
					<template #content>
						<div class="flexbox flex-alignc flex-col" style="padding: 15px 0;">
							<Icon name="ve-icon-yuyin" size="40" color="#0fa27e" />
							<p class="fs-12 mb-15 c-999">网络不给力</p>
							<Button size="small"><i style="background:#f00;border-radius:50%;box-shadow:0 1px 2px #999;margin-right:5px;height:8px;width:8px;"></i>开始讲话</Button>
						</div>
					</template>
				</Popover>
				<Button class="btn" type="link" v-tooltip="{content: '发送图片', theme: 'light', arrow: false}">
					<Icon name="ve-icon-photo" size="16" cursor />
					<input ref="uploadImgRef" type="file" title="" accept="image/*" @change="handleUploadImage" />
				</Button>
				<Input
					class="flex1"
					ref="editorRef"
					v-model="editorText"
					type="textarea"
					:autosize="{maxRows: 4}"
					clearable
					placeholder="Prompt..."
					@keydown="handleKeydown"
					@clear="handleClear"
					style="margin: 0 5px;"
				/>
				<Button class="btn" type="link" icon="ve-icon-submit" @click="handleSubmit"></Button>
			</Flex>
		</div>
	</div>
</template>

TypeScript

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 uploadImgRef = ref()
const editorRef = ref()
const editorText = ref(props.value)

// ...

// 发送会话
const handleSubmit = () => {
	editorRef.value.focus()
	if(!editorText.value) return

	let data = {
		type: 'text',
		role: 'User',
		key: guid(),
		content: editorText.value
	}
	chatState.addSession(data)
	// 清空
	editorText.value = ''
}
const handleKeydown = (e) => {
	// ctrl+enter
	if(e.ctrlKey && e.keyCode == 13) {
		handleSubmit()
	}
}
const handleClear = () => {
	emit('clear')
}
// 选择图片
const handleUploadImage = () => {
	let file = uploadImgRef.value.files[0]
	if(!file) return
	let size = Math.floor(file.size / 1024)
	console.log(size)
	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: 'User',
			key: guid(),
			content: img
		}
		chatState.addSession(data)
	}
}

在这里插入图片描述

TypeScript

/**
 * 聊天状态管理
 * @author YXY  Q282310962
 */

import { defineStore } from 'pinia'
import { guid, isEmpty } from '@/utils'

export const chatStore = defineStore('chat', {
	state: () => ({
		// 聊天会话记录
		sessionId: '',
		session: []
	}),
	getters: {},
	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) {
			// ...
		},

		// 清空会话
		clearSession() {
			this.session = []
			this.sessionId = ''
		}
	},
	// 本地持久化存储(默认存储localStorage)
	persist: true
	/* persist: {
		// key: 'chatStore', // 不设置则是默认app
		storage: localStorage,
		paths: ['aa', 'bb'] // 设置缓存键
	} */
})

好了基于vue3+vite4+pinia2开发模仿chatgpt聊天就分享到这里。

Tauri-Vue3聊天实例|Tauri跨端聊天

;