yangyin
2024-06-27 bb08a46aed02e5e739a5ecafb5b9303a0dbabe67
Merge branch 'master' of http://47.103.154.90:83/r/WI/Web.V1.0
已修改5个文件
已添加6个文件
19671 ■■■■ 文件已修改
package-lock.json 19101 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 95 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/Chat.vue 207 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/components/Copy.vue 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/components/Loding.vue 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/libs/gpt.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/libs/markdown.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/types.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/project/ch/home/Home.vue 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/project/ch/home/component/waterRight/top.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tailwind.config.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
package-lock.json
ÎļþÌ«´ó
package.json
@@ -1,23 +1,24 @@
{
  "name": "phm",
  "version": "2.4.31",
  "description": "vue3 vite next admin template",
  "author": "lyt_20201208",
  "license": "MIT",
  "scripts": {
    "use": "node ./scripts/use.js",
    "dev": "node ./scripts/dev.js",
    "deploy": "node ./scripts/deploy.js",
    "publish": "node ./scripts/publish.js",
    "preview": "vite preview",
    "dev:yw": "vite --mode yw",
    "serve": "vite",
    "build": "node ./scripts/build.js",
    "build:yw": "vite build --mode yw",
    "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/",
    "fix-memory-limit": "cross-env LIMIT=8048 increase-memory-limit"
  },
  "dependencies": {
    "name": "phm",
    "version": "2.4.31",
    "description": "vue3 vite next admin template",
    "author": "lyt_20201208",
    "license": "MIT",
    "scripts": {
        "use": "node ./scripts/use.js",
        "dev": "node ./scripts/dev.js",
        "deploy": "node ./scripts/deploy.js",
        "publish": "node ./scripts/publish.js",
        "preview": "vite preview",
        "dev:yw": "vite --mode yw",
        "serve": "vite",
        "build": "node ./scripts/build.js",
        "build:yw": "vite build --mode yw",
        "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/",
        "fix-memory-limit": "cross-env LIMIT=8048 increase-memory-limit"
    },
    "dependencies": {
        "@icon-park/vue-next": "^1.4.2",
        "@amap/amap-jsapi-loader": "^1.0.1",
        "@element-plus/icons-vue": "^2.1.0",
        "@types/three": "^0.164.1",
@@ -58,7 +59,11 @@
        "vue-grid-layout": "^3.0.0-beta1",
        "vue-i18n": "^9.2.2",
        "vue-router": "^4.1.6",
        "xlsx": "^0.18.5"
        "xlsx": "^0.18.5",
        "@tailwindcss/typography": "^0.5.9",
        "crypto-js": "^4.1.1",
        "highlight.js": "^11.7.0",
        "markdown-it": "^13.0.1"
    },
    "devDependencies": {
        "@types/node": "^18.15.0",
@@ -82,29 +87,29 @@
        "vite-plugin-vue-setup-extend-plus": "^0.1.0",
        "vue-eslint-parser": "^9.1.0"
    },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ],
  "bugs": {
    "url": "https://gitee.com/lyt-top/vue-next-admin/issues"
  },
  "engines": {
    "node": ">=16.0.0",
    "npm": ">= 7.0.0"
  },
  "keywords": [
    "vue",
    "vue3",
    "vuejs/vue-next",
    "element-ui",
    "element-plus",
    "vue-next-admin",
    "next-admin"
  ],
  "repository": {
    "type": "git",
    "url": "https://gitee.com/lyt-top/vue-next-admin.git"
  }
    "browserslist": [
        "> 1%",
        "last 2 versions",
        "not dead"
    ],
    "bugs": {
        "url": "https://gitee.com/lyt-top/vue-next-admin/issues"
    },
    "engines": {
        "node": ">=16.0.0",
        "npm": ">= 7.0.0"
    },
    "keywords": [
        "vue",
        "vue3",
        "vuejs/vue-next",
        "element-ui",
        "element-plus",
        "vue-next-admin",
        "next-admin"
    ],
    "repository": {
        "type": "git",
        "url": "https://gitee.com/lyt-top/vue-next-admin.git"
    }
}
src/components/chat/Chat.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,207 @@
<template>
    <div class="flex flex-col h-screen">
        <div class="flex flex-nowrap fixed w-full items-baseline top-0 px-6 py-4 bg-gray-100">
            <div class="text-2xl font-bold">ChatGPT</div>
            <div class="ml-4 text-sm text-gray-500">基于 OpenAI çš„ ChatGPT è‡ªç„¶è¯­è¨€æ¨¡åž‹äººå·¥æ™ºèƒ½å¯¹è¯</div>
            <div class="ml-auto px-3 py-2 text-sm cursor-pointer hover:bg-white rounded-md" @click="clickConfig()">设置</div>
        </div>
        <div class="flex-1 mx-2 mt-20 mb-2" ref="chatListDom">
            <div
                class="group flex flex-col px-4 py-3 hover:bg-slate-100 rounded-lg"
                v-for="(item, index) of messageList.filter((v) => v.role !== 'system')"
                :key="index"
            >
                <div class="flex justify-between items-center mb-2">
                    <div class="font-bold">{{ roleAlias[item.role] }}:</div>
                    <Copy class="invisible group-hover:visible" :content="item.content" />
                </div>
                <div>
                    <div class="prose text-sm text-slate-600 leading-relaxed" v-if="item.content" v-html="md.render(item.content)"></div>
                    <Loding v-else />
                </div>
            </div>
        </div>
        <div class="sticky bottom-0 w-full p-6 pb-8 bg-gray-100">
            <div class="-mt-2 mb-2 text-sm text-gray-500" v-if="isConfig">请输入 API Key:</div>
            <div class="flex">
                <input
                    class="input"
                    :type="isConfig ? 'password' : 'text'"
                    :placeholder="isConfig ? 'sk-xxxxxxxxxx' : '请输入'"
                    v-model="messageContent"
                    @keydown.enter="isTalking || sendOrSave()"
                />
                <button class="btn" :disabled="isTalking" @click="sendOrSave()">
                    {{ isConfig ? '保存' : '发送' }}
                </button>
            </div>
        </div>
    </div>
</template>
<script setup lang="ts">
import type { ChatMessage } from './types';
import { ref, watch, nextTick, onMounted } from 'vue';
import cryptoJS from 'crypto-js';
import Loding from './components/Loding.vue';
import Copy from './components/Copy.vue';
import { md } from './libs/markdown';
let apiKey = '';
let isConfig = ref(false);
let isTalking = ref(false);
let messageContent = ref('');
const chatListDom = ref<HTMLDivElement>();
const decoder = new TextDecoder('utf-8');
const roleAlias = { user: 'ME', assistant: 'ChatGPT', system: 'System' };
const messageList = ref<ChatMessage[]>([
    {
        role: 'system',
        content: '你是 ChatGPT,OpenAI è®­ç»ƒçš„大型语言模型,尽可能简洁地回答。',
    },
    {
        role: 'assistant',
        content: `你好,我是AI语言模型,我可以提供一些常用服务和信息,例如:
  1. ç¿»è¯‘:我可以把中文翻译成英文,英文翻译成中文,还有其他一些语言翻译,比如法语、日语、西班牙语等。
  2. å’¨è¯¢æœåŠ¡ï¼šå¦‚æžœä½ æœ‰ä»»ä½•é—®é¢˜éœ€è¦å’¨è¯¢ï¼Œä¾‹å¦‚å¥åº·ã€æ³•å¾‹ã€æŠ•èµ„ç­‰æ–¹é¢ï¼Œæˆ‘å¯ä»¥å°½å¯èƒ½ä¸ºä½ æä¾›å¸®åŠ©ã€‚
  3. é—²èŠï¼šå¦‚果你感到寂寞或无聊,我们可以聊一些有趣的话题,以减轻你的压力。
  è¯·å‘Šè¯‰æˆ‘你需要哪方面的帮助,我会根据你的需求给你提供相应的信息和建议。`,
    },
]);
onMounted(() => {
    if (getAPIKey()) {
        switchConfigStatus();
    }
});
const sendChatMessage = async (content: string = messageContent.value) => {
    try {
        isTalking.value = true;
        if (messageList.value.length === 2) {
            messageList.value.pop();
        }
        messageList.value.push({ role: 'user', content });
        clearMessageContent();
        messageList.value.push({ role: 'assistant', content: '' });
        // const { body, status } = await chat(messageList.value, getAPIKey());
        // if (body) {
        //   const reader = body.getReader();
        //   await readStream(reader, status);
        // }
        const a = new Promise<string>((resolve) => {
            setTimeout(() => {
                resolve('nihao ');
            }, 500);
        });
        const msg = await a;
        appendLastMessageContent(msg);
    } catch (error: any) {
        appendLastMessageContent(error);
    } finally {
        isTalking.value = false;
    }
};
const readStream = async (reader: ReadableStreamDefaultReader<Uint8Array>, status: number) => {
    let partialLine = '';
    while (true) {
        // eslint-disable-next-line no-await-in-loop
        const { value, done } = await reader.read();
        if (done) break;
        const decodedText = decoder.decode(value, { stream: true });
        if (status !== 200) {
            const json = JSON.parse(decodedText); // start with "data: "
            const content = json.error.message ?? decodedText;
            appendLastMessageContent(content);
            return;
        }
        const chunk = partialLine + decodedText;
        const newLines = chunk.split(/\r?\n/);
        partialLine = newLines.pop() ?? '';
        for (const line of newLines) {
            if (line.length === 0) continue; // ignore empty message
            if (line.startsWith(':')) continue; // ignore sse comment message
            if (line === 'data: [DONE]') return; //
            const json = JSON.parse(line.substring(6)); // start with "data: "
            const content = status === 200 ? json.choices[0].delta.content ?? '' : json.error.message;
            appendLastMessageContent(content);
        }
    }
};
const appendLastMessageContent = (content: string) => (messageList.value[messageList.value.length - 1].content += content);
const sendOrSave = () => {
    if (!messageContent.value.length) return;
    if (isConfig.value) {
        if (saveAPIKey(messageContent.value.trim())) {
            switchConfigStatus();
        }
        clearMessageContent();
    } else {
        sendChatMessage();
    }
};
const clickConfig = () => {
    if (!isConfig.value) {
        messageContent.value = getAPIKey();
    } else {
        clearMessageContent();
    }
    switchConfigStatus();
};
const getSecretKey = () => 'lianginx';
const saveAPIKey = (apiKey: string) => {
    if (apiKey.slice(0, 3) !== 'sk-' || apiKey.length !== 51) {
        alert('API Key é”™è¯¯ï¼Œè¯·æ£€æŸ¥åŽé‡æ–°è¾“入!');
        return false;
    }
    const aesAPIKey = cryptoJS.AES.encrypt(apiKey, getSecretKey()).toString();
    localStorage.setItem('apiKey', aesAPIKey);
    return true;
};
const getAPIKey = () => {
    if (apiKey) return apiKey;
    const aesAPIKey = localStorage.getItem('apiKey') ?? '';
    apiKey = cryptoJS.AES.decrypt(aesAPIKey, getSecretKey()).toString(cryptoJS.enc.Utf8);
    return apiKey;
};
const switchConfigStatus = () => (isConfig.value = !isConfig.value);
const clearMessageContent = () => (messageContent.value = '');
const scrollToBottom = () => {
    if (!chatListDom.value) return;
    scrollTo(0, chatListDom.value.scrollHeight);
};
watch(messageList.value, () => nextTick(() => scrollToBottom()));
</script>
<style scoped>
pre {
    font-family: -apple-system, 'Noto Sans', 'Helvetica Neue', Helvetica, 'Nimbus Sans L', Arial, 'Liberation Sans', 'PingFang SC',
        'Hiragino Sans GB', 'Noto Sans CJK SC', 'Source Han Sans SC', 'Source Han Sans CN', 'Microsoft YaHei', 'Wenquanyi Micro Hei',
        'WenQuanYi Zen Hei', 'ST Heiti', SimHei, 'WenQuanYi Zen Hei Sharp', sans-serif;
}
</style>
src/components/chat/components/Copy.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,80 @@
<script setup lang="ts">
import { Copy, Loading, CheckOne } from "@icon-park/vue-next";
import type { Theme } from "@icon-park/vue-next/lib/runtime";
import { ref } from "vue";
const porps = defineProps<{ content: string }>();
const btnConfig: {
  size: number;
  fill: string;
  theme: Theme;
} = {
  size: 14,
  fill: "#999",
  theme: "outline",
};
const btnTips = {
  copy: "复制全文",
  loading: "",
  success: "已复制到剪贴板!",
  error: "复制失败!",
};
const btnStatus = ref<"copy" | "loading" | "success" | "error">("copy");
const copyToClipboard = (content: string = porps.content) => {
  btnStatus.value = "loading";
  navigator.clipboard
    .writeText(content)
    .then(() => setTimeout(() => (btnStatus.value = "success"), 150))
    .catch(() => (btnStatus.value = "error"))
    .finally(() => setTimeout(() => (btnStatus.value = "copy"), 1500));
};
</script>
<template>
  <div class="flex items-center cursor-pointer" @click="copyToClipboard()">
    <copy
      v-show="btnStatus === 'copy'"
      :theme="btnConfig.theme"
      :size="btnConfig.size"
      :fill="btnConfig.fill"
    />
    <loading
      class="rotate"
      v-show="btnStatus === 'loading'"
      :theme="btnConfig.theme"
      :size="btnConfig.size"
      :fill="btnConfig.fill"
    />
    <check-one
      v-show="btnStatus === 'success'"
      :theme="btnConfig.theme"
      :size="btnConfig.size"
      :fill="btnConfig.fill"
    />
    <close-one
      v-show="btnStatus === 'error'"
      :theme="btnConfig.theme"
      :size="btnConfig.size"
      :fill="btnConfig.fill"
    />
    <span class="text-xs ml-0.5 text-gray-500 leading-none">{{
      btnTips[btnStatus]
    }}</span>
  </div>
</template>
<style scoped>
@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
.rotate {
  animation: spin 2s linear infinite;
}
</style>
src/components/chat/components/Loding.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,113 @@
<template>
  <div class="com__box">
    <div class="loading">
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
    </div>
  </div>
</template>
<style scoped>
.loading,
.loading > div {
  position: relative;
  box-sizing: border-box;
}
.loading {
  display: block;
  font-size: 0;
  color: #000;
}
.loading.la-dark {
  color: #333;
}
.loading > div {
  display: inline-block;
  float: none;
  background-color: currentColor;
  border: 0 solid currentColor;
}
.loading {
  width: 17px;
  height: 17px;
}
.loading > div {
  width: 3px;
  height: 3px;
  margin: 1px;
  border-radius: 100%;
  animation-name: ball-grid-beat;
  animation-iteration-count: infinite;
}
.loading > div:nth-child(1) {
  animation-duration: 0.65s;
  animation-delay: 0.03s;
}
.loading > div:nth-child(2) {
  animation-duration: 1.02s;
  animation-delay: 0.09s;
}
.loading > div:nth-child(3) {
  animation-duration: 1.06s;
  animation-delay: -0.69s;
}
.loading > div:nth-child(4) {
  animation-duration: 1.5s;
  animation-delay: -0.41s;
}
.loading > div:nth-child(5) {
  animation-duration: 1.6s;
  animation-delay: 0.04s;
}
.loading > div:nth-child(6) {
  animation-duration: 0.84s;
  animation-delay: 0.07s;
}
.loading > div:nth-child(7) {
  animation-duration: 0.68s;
  animation-delay: -0.66s;
}
.loading > div:nth-child(8) {
  animation-duration: 0.93s;
  animation-delay: -0.76s;
}
.loading > div:nth-child(9) {
  animation-duration: 1.24s;
  animation-delay: -0.76s;
}
@keyframes ball-grid-beat {
  0% {
    opacity: 1;
  }
  50% {
    opacity: 0.35;
  }
  100% {
    opacity: 1;
  }
}
</style>
src/components/chat/libs/gpt.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
import type { ChatMessage } from '../types';
export async function chat(messageList: ChatMessage[], apiKey: string) {
    const result = await fetch('https://api.openai.com/v1/chat/completions', {
        method: 'post',
        // signal: AbortSignal.timeout(8000),
        // å¼€å¯åŽåˆ°è¾¾è®¾å®šæ—¶é—´ä¼šä¸­æ–­æµå¼è¾“出
        headers: {
            'Content-Type': 'application/json',
            Authorization: `Bearer ${apiKey}`,
        },
        body: JSON.stringify({
            model: 'gpt-3.5-turbo',
            stream: true,
            messages: messageList,
        }),
    }).catch(error=>{
    throw error
  });
    return result;
}
src/components/chat/libs/markdown.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
import highlight from 'highlight.js';
import Markdown from 'markdown-it';
const mdOptions: Markdown.Options = {
    linkify: true,
    typographer: true,
    breaks: true,
    langPrefix: 'language-',
    // ä»£ç é«˜äº®
    highlight(str, lang) {
        if (lang && highlight.getLanguage(lang)) {
            try {
                return '<pre class="hljs"><code>' + highlight.highlight(lang, str, true).value + '</code></pre>';
            } catch (__) {
      }
        }
        return '';
    },
};
export const md = new Markdown(mdOptions);
src/components/chat/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,5 @@
export interface ChatMessage {
    role: "user" | "assistant" | "system";
    content: string;
  }
src/views/project/ch/home/Home.vue
@@ -23,9 +23,14 @@
                    <div class="pc-chatRoom w100 h100">
                        <div class="homeBox w100 h100">
                            <div class="flex items-center flex-column mt-20">
                                <waterTop />
                                <waterCenter />
                                <waterBottom />
                                <div class="flex items-center flex-column" v-if="!isShowChat">
                                    <waterTop @sendClick="sendClick"/>
                                    <waterCenter />
                                    <waterBottom />
                                </div>
                                <div v-if="isShowChat">
                                    <Chat />
                                </div>
                            </div>
                        </div>
                        <p class="declare">
@@ -41,12 +46,20 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import asideNew from './component/waterLeftAside/asideNew.vue';
import asideTitle from './component/waterLeftAside/asideTitle.vue';
import waterBottom from './component/waterRight/bottom.vue';
import waterCenter from './component/waterRight/center.vue';
import waterHeader from './component/waterRight/header.vue';
import waterTop from './component/waterRight/top.vue';
import Chat from '/@/components/chat/Chat.vue';
const isShowChat = ref(true);
const sendClick = () => {
    isShowChat.value =true;
};
</script>
<style scoped lang="scss">
.pc-chat_room {
src/views/project/ch/home/component/waterRight/top.vue
@@ -38,7 +38,7 @@
                        <img src="/static/images/wave/HeadImg.png" class="set-img-icon box-border" />
                    </el-button>
                    <el-button title="AI看图" class="cursor-pointer" link>
                    <el-button title="发送" class="cursor-pointer" link @click="sendClick">
                        <div class="send">
                            <img src="/static/images/wave/QueryImg.png" />
                        </div>
@@ -51,6 +51,11 @@
<script setup lang="ts">
import { reactive } from 'vue';
const emits = defineEmits(['sendClick']);
const sendClick = () => {
    emits('sendClick');
};
let state = reactive({
    roleList: [
        {
tailwind.config.js
@@ -8,5 +8,5 @@
        preflight: false,
    },
    theme: {},
    plugins: [],
    plugins: [require("@tailwindcss/typography")],
};