布局先提交一波

This commit is contained in:
aixianling
2023-05-12 15:53:39 +08:00
parent 81f76164d8
commit 8d362c7468
18 changed files with 2800 additions and 188 deletions

View File

@@ -1,13 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
<head>
<meta charset="UTF-8"/>
<link rel="icon" type="image/svg+xml" href="/vite.svg"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>语言模型聊天</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

2305
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,10 @@
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.4.0",
"element-plus": "^2.3.4",
"sass": "^1.62.1",
"sass-loader": "^13.2.2",
"vue": "^3.2.47"
},
"devDependencies": {

View File

@@ -1,33 +1,44 @@
<template>
<div id="app" @mousedown="windowMove(true)" @mouseup="windowMove(false)">
<Home/>
<div id="app">
<el-row class="home">
<chat class="fill" :config="setting"/>
<settings class="mar-l16" v-show="showSettings" v-model="setting"/>
</el-row>
</div>
</template>
<script>
import {isPc} from '@/util/util.js'
import Home from './view/home.vue'
import Chat from "./components/chat";
import Settings from "./components/settings";
import {ChatGPT} from "./utils/models";
export default {
name: 'App',
components: {
Home
components: {Settings, Chat},
data() {
return {
showSettings: true,
setting: {
model: new ChatGPT()
},
}
},
methods: {
windowMove(canMove) {
if (isPc()) {
import("electron").then(({ipcRenderer}) => {
ipcRenderer.send('window-move-open', canMove ?? false);
});
}
},
handleResize() {
console.log("App handleResize")
this.showSettings = window.innerWidth > 1150;
}
},
created() {
window.addEventListener('resize', this.handleResize)
},
unmounted() {
window.removeEventListener('resize', this.handleResize)
}
}
</script>
<style lang="scss">
@import url(./assets/font/iconfont.css);
@each $v in (333, 666, 888, 999, '26f', 'f46') {
.color-#{$v} {
color: \##{$v};
@@ -79,17 +90,6 @@ export default {
width: 100%;
}
.iconfont {
font-family: "iconfont" !important;
font-style: normal;
font-size: 25px;
vertical-align: middle;
color: rgb(117, 120, 137);
transition: .3s;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
padding: 0;
margin: 0;
@@ -102,7 +102,20 @@ export default {
position: absolute;
}
::-webkit-scrollbar {
display: none; /* Chrome Safari */
.home {
width: 100vw;
height: 100vh;
padding: 16px;
background-color: #272A37;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.fill {
flex: 1;
min-height: 0;
min-width: 0;
}
</style>

4
src/assets/clear.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-ea893728="">
<path fill="#fff"
d="M352 192V95.936a32 32 0 0 1 32-32h256a32 32 0 0 1 32 32V192h256a32 32 0 1 1 0 64H96a32 32 0 0 1 0-64h256zm64 0h192v-64H416v64zM192 960a32 32 0 0 1-32-32V256h704v672a32 32 0 0 1-32 32H192zm224-192a32 32 0 0 0 32-32V416a32 32 0 0 0-64 0v320a32 32 0 0 0 32 32zm192 0a32 32 0 0 0 32-32V416a32 32 0 0 0-64 0v320a32 32 0 0 0 32 32z"></path>
</svg>

After

Width:  |  Height:  |  Size: 461 B

View File

@@ -1,40 +0,0 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Install
<a href="https://github.com/vuejs/language-tools" target="_blank">Volar</a>
in your IDE for a better DX
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

123
src/components/chat.vue Normal file
View File

@@ -0,0 +1,123 @@
<template>
<section class="chat">
<el-row align="middle">
<div class="modelInfo">
<img :src="config.model.avatar" alt=""/>
<div class="fill mar-l8">
<b v-text="config.model.name"/>
<div class="desc" v-text="config.model.desc"/>
</div>
</div>
<el-row justify="end" class="fill mar-l8">
<div class="clear-icon"/>
<!-- todo 聊天操作栏-->
</el-row>
</el-row>
<thinking-bar v-show="loading"/>
<chat-content class="fill"/>
<chat-input/>
</section>
</template>
<script>
import ChatContent from "../components/chatContent";
import ChatInput from "../components/chatInput";
import ThinkingBar from "../components/thinkingBar";
export default {
name: "chat",
components: {ChatContent, ChatInput, ThinkingBar},
props: {
config: {default: () => ({})}
},
data() {
return {
loading: true
}
},
methods: {
handleResize() {
if (window.innerWidth <= 700) {
this.$nextTick(() => {
document.querySelectorAll('.chat-content')[0].style.height = '93%';
this.buttonStatus = false
const textareaMsg = document.getElementById("textareaMsg");
textareaMsg.style.marginLeft = "0px";
this.personInfoSpan = [14, 0, 10];
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)
if (isMobile) {
document.querySelectorAll('.chatInputs')[0].style.margin = '0%';
} else {
document.querySelectorAll('.chatInputs')[0].style.margin = '3%';
}
});
} else {
this.$nextTick(() => {
document.querySelectorAll('.chat-content')[0].style.height = '88%';
this.buttonStatus = true
this.personInfoSpan = [1, 17, 6];
});
}
},
},
created() {
window.addEventListener('resize', this.handleResize)
},
unmounted() {
window.removeEventListener('resize', this.handleResize)
}
}
</script>
<style lang="scss" scoped>
.chat {
color: #fff;
display: flex;
flex-direction: column;
.modelInfo {
display: flex;
align-items: center;
line-height: 1.4;
& > img {
border: 2px solid #fff;
border-radius: 50%;
padding: 4px;
width: 45px;
height: 45px;
}
b {
font-size: 20px;
}
.desc {
color: #999;
font-size: 14px;
}
}
.clear-icon {
box-sizing: border-box;
height: 40px;
background-size: 24px 24px;
background-image: url("../assets/clear.svg");
background-repeat: no-repeat;
background-position: top center;
cursor: pointer;
opacity: .6;
padding-top: 24px;
&:after {
font-size: 12px;
content: "清空历史";
display: block;
}
&:hover {
opacity: 1;
}
}
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<section class="chatContent" @scroll="onScroll">
<div class="chat-wrapper" v-for="item in list" :key="item.id">
<div class="chat-friend" v-if="item.uid !== 'jcm'">
<div class="chat-text" v-if="item.chatType == 0">
<el-row :gutter="20">
<el-col :span="2">
<svg t="1679666016648" @click="$copy(item.msg, '已复制')" class="icon" viewBox="0 0 1024 1024"
version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6241" width="22" height="22">
<path
d="M661.333333 234.666667A64 64 0 0 1 725.333333 298.666667v597.333333a64 64 0 0 1-64 64h-469.333333A64 64 0 0 1 128 896V298.666667a64 64 0 0 1 64-64z m-21.333333 85.333333H213.333333v554.666667h426.666667v-554.666667z m191.829333-256a64 64 0 0 1 63.744 57.856l0.256 6.144v575.701333a42.666667 42.666667 0 0 1-85.034666 4.992l-0.298667-4.992V149.333333H384a42.666667 42.666667 0 0 1-42.368-37.674666L341.333333 106.666667a42.666667 42.666667 0 0 1 37.674667-42.368L384 64h447.829333z"
fill="#909399" p-id="6242"></path>
</svg>
</el-col>
<el-col :span="21">
</el-col>
</el-row>
<markdown-it-vue :content="item.msg.trim()"/>
</div>
<div class="chat-img" v-if="item.chatType == 1">
<img :src="item.msg" alt="表情" v-if="item.extend.imgType == 1" style="width: 100px; height: 100px"/>
<el-image style="border-radius: 10px" :src="item.msg" :preview-src-list="srcImgList" v-else>
</el-image>
</div>
<div class="chat-img" v-if="item.chatType == 2">
<div class="word-file">
<FileCard :fileType="item.extend.fileType" :file="item.msg"></FileCard>
</div>
</div>
<div class="info-time">
<img :src="item.headImg" alt=""/>
<span>{{ item.name }}</span>
<span>{{ item.time }}</span>
</div>
</div>
<div class="chat-me" v-else>
<div class="chat-text" v-if="item.chatType == 0">
<span style="font-size:16px">{{ item.msg }}</span>
</div>
<div class="chat-img" v-if="item.chatType == 1">
<img :src="item.msg" alt="表情" v-if="item.extend.imgType == 1" style="width: 100px; height: 100px"/>
<el-image style="max-width: 300px; border-radius: 10px" :src="item.msg" :preview-src-list="srcImgList"
v-else>
</el-image>
</div>
<div class="chat-img" v-if="item.chatType == 2">
<div class="word-file">
<FileCard :fileType="item.extend.fileType" :file="item.msg"></FileCard>
</div>
</div>
<div class="info-time">
<span>{{ item.name }}</span>
<span>{{ item.time }}</span>
<img :src="item.headImg" alt=""/>
</div>
</div>
</div>
</section>
</template>
<script>
export default {
name: "chatContent",
data() {
return {
list:[]
}
},
methods: {
onScroll() {
const scrollDom = this.$el;
const scrollTop = scrollDom.scrollTop;
const offsetHeight = scrollDom.offsetHeight;
const scrollHeight = scrollDom.scrollHeight;
// 当滚动到底部,设置 isAutoScroll 为 true
this.isAutoScroll = scrollTop + offsetHeight === scrollHeight;
},
},
created() {
}
}
</script>
<style lang="scss" scoped>
.chatContent {
background: #323644;
border-radius: 4px;
padding: 16px;
box-sizing: border-box;
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1,22 @@
<template>
<section class="chatInput">
</section>
</template>
<script>
export default {
name: "chatInput",
data() {
return {}
},
methods: {},
created() {
}
}
</script>
<style lang="scss" scoped>
.chatInput {
}
</style>

View File

@@ -0,0 +1,30 @@
<template>
<section class="settings">
</section>
</template>
<script>
export default {
name: "settings",
model: {
prop: "settings",
event: "input"
},
props: {
settings: {default: () => ({})}
},
data() {
return {}
},
methods: {},
created() {
}
}
</script>
<style lang="scss" scoped>
.settings {
min-width: 400px;
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<section class="thinkingBar"/>
</template>
<script>
export default {
name: "thinkingBar",
}
</script>
<style lang="scss" scoped>
.thinkingBar {
position: relative;
margin-top: 16px;
height: 2px;
background-image: linear-gradient(to right, #eea2a2 0%, #bbc1bf 19%, #57c6e1 42%, #b49fda 79%, #7ac5d8 100%);
background-size: 100%;
animation: shrink-and-expand 2s ease-in-out infinite;
&::before,
&::after {
content: "";
position: absolute;
top: 0;
width: 50%;
height: 100%;
background: inherit;
}
&::before {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
left: 0;
transform-origin: left;
animation: shrink-left 2s ease-in-out infinite;
}
&::after {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
right: 0;
transform-origin: right;
animation: shrink-right 2s ease-in-out infinite;
}
@keyframes shrink-and-expand {
0%,
100% {
transform: scaleX(1);
}
50% {
transform: scaleX(0);
}
}
@keyframes shrink-left {
0%,
50% {
transform: scaleX(1);
}
50.1%,
100% {
transform: scaleX(0);
}
}
@keyframes shrink-right {
0%,
50% {
transform: scaleX(1);
}
50.1%,
100% {
transform: scaleX(0);
}
}
}
</style>

View File

@@ -1,20 +1,10 @@
import { createApp } from 'vue'
import './style.css'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import {createApp} from 'vue'
import App from './App.vue'
import ElementUI from 'element-ui';
import VueRouter from 'vue-router'
import 'element-ui/lib/theme-chalk/index.css';
import router from './router/index'
import { copyToClipboard } from '@/util/util'
import i18n from '@/config/i18n'
Vue.use(VueRouter)
Vue.config.productionTip = false
Vue.use(ElementUI);
import axios from "./utils/axios";
/**
* 复制
*/
const ins = createApp(App)
ins.use(VueRouter)
ins.use(ElementUI)
ins.mount('#app')
const app = createApp(App)
app.config.globalProperties.$http = axios
app.use(ElementPlus)
app.mount('#app')

View File

@@ -1,89 +0,0 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

6
src/utils/axios.js Normal file
View File

@@ -0,0 +1,6 @@
import axios from "axios";
const ins = axios.create({
})
export default ins

7
src/utils/env.js Normal file
View File

@@ -0,0 +1,7 @@
export const OPEN_AI_KEY = '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'
//ai头像
export const AI_AVATAR = "https://th.bing.com/th?id=ODL.3e2fbff4543f0d3632d34be6d02adc93&w=100&h=100&c=12&pcl=faf9f7&o=6&dpr=1.5&pid=13.1"
//用户头像
export const USER_AVATAR = "https://avatars.githubusercontent.com/u/20533272?v=4"

31
src/utils/models.js Normal file
View File

@@ -0,0 +1,31 @@
import {AI_AVATAR} from "./env";
class BaseModel {
constructor(props) {
for (const k in props) {
this[k] = props[k];
}
}
}
export class ChatGPT extends BaseModel {
constructor() {
super({
avatar: AI_AVATAR,
name: 'ChatGPT',
id: "gpt-3.5-turbo",
desc: "ChatGPT-3.5所基于的模型"
});
}
}
export class ChatGLM extends BaseModel {
constructor() {
super({
avatar: AI_AVATAR,
name: 'ChatGLM',
id: "chatglm-6b",
desc: "ChatGLM-6B所基于的模型"
});
}
}

25
src/utils/tools.js Normal file
View File

@@ -0,0 +1,25 @@
/**
* 复制到剪切板
*/
export function copyToClipboard(content) {
const clipboardData = window.clipboardData
if (clipboardData) {
clipboardData.clearData()
clipboardData.setData('Text', content)
return true
} else if (document.execCommand) {
const el = document.createElement('textarea')
el.value = content
el.setAttribute('readonly', '')
el.style.position = 'absolute'
el.style.left = '-9999px'
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
return true
}
return false
}

View File

@@ -1,7 +1,15 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import {defineConfig} from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
extensions: ['.mjs', '.js', '.mts', '.json', '.vue']
},
server: {
open: true,
host: '0.0.0.0',
port: 10109
}
})