Commit adba6c80 authored by jiangwei's avatar jiangwei

first commit

parents
Pipeline #286 failed with stages
> 1%
last 2 versions
not dead
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
# DeepSeek API Configuration
VITE_DEEPSEEK_API_KEY=sk-ceb23bc709b641b79bea590bc3f7e965
VITE_API_BASE_URL=/api
VITE_MODEL_NAME=deepseek-chat
VITE_API_HOST=https://api.deepseek.com
\ No newline at end of file
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/essential',
'@vue/standard'
],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}
node_modules
.DS_Store
.github
dist
.npmrc
.cache
tests/server/static
tests/server/static/upload
.local
# local env files
.env.local
.env.*.local
.eslintcache
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.svn
# .vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
/os_del.cmd
os_del.cmd
/.vscode/
/.history/
/svn clear.bat
# DeepSeek Web Chat
一个基于 Vue 3 开发的 Web 聊天界面,用于调用 DeepSeek API 实现 AI 对话功能。
![image](https://github.com/user-attachments/assets/66041177-c7ce-433c-a3f5-fb45f6454f65)
## 背景说明
由于 DeepSeek 官网经常面临访问压力大的问题,但模型已开源,许多大厂都部署了该模型的服务。本项目采用腾讯云部署的 DeepSeek 模型实现。
> 🔗 API 文档:[腾讯云 DeepSeek API 文档](https://cloud.tencent.com/document/product/1772/115969)
> 🎯 主要优势:服务稳定,响应快速,支持流式输出
>
> PS: 腾讯提供的DeepSeek接口为671b模型,API免费到2025年2月25日23:59:59
## 功能特点
- 💬 实时对话功能
- 🤔 显示 AI 思考过程
- 🔄 支持新对话
- 📱 响应式设计
- 🌈 优雅的动画效果
## 快速开始
##### 1.克隆项目
```bash
git clone https://github.com/momoxiaoshuai/deepseek-chat.git
cd deepseek-web-chat
```
##### 2.安装依赖
```bash
npm install
```
##### 3.配置环境变量
1. 复制环境变量示例文件:
```bash
cp .env.example .env
```
2. 编辑 `.env` 文件,设置你的 API 密钥:
```bash
VITE_DEEPSEEK_API_KEY=your-api-key-here
VITE_API_BASE_URL=/api
```
> ⚠️ 注意:不要将包含实际 API 密钥的 `.env` 文件提交到版本控制系统中
##### 4.本地开发
```bash
npm run dev
```
##### 5.构建部署
```bash
npm run build
```
## 部署说明
### Nginx 配置示例
```
nginx
server {
listen 80;
server_name your-domain.com; # 替换成你的域名或 IP
root /path/to/your/dist; # 替换成你的 dist 目录路径
index index.html;
# 处理前端路由
location / {
try_files $uri $uri/ /index.html last;
}
# API 代理配置
location /api/ {
proxy_pass https://api.lkeap.cloud.tencent.com/v1/;
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2;
proxy_set_header Host api.lkeap.cloud.tencent.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
# 关闭缓冲,确保流式响应正常工作
proxy_buffering off;
}
error_log /var/log/nginx/deepseek_error.log;
access_log /var/log/nginx/deepseek_access.log;
}
```
### 部署步骤
1. 构建项目
```bash
npm run build
```
2. 将 dist 目录上传到服务器
```bash
scp -r dist/ user@your-server:/path/to/your/dist
```
3. 配置 Nginx
- 将上述 Nginx 配置保存到 `/etc/nginx/conf.d/deepseek.conf`
- 检查配置是否正确:`sudo nginx -t`
- 重启 Nginx:`sudo systemctl restart nginx`
## 环境要求
- Node.js >= 16
- npm >= 7
- 现代浏览器(支持 ES6+)
## 技术栈
- Vue 3
- Vite
- Nginx
## 注意事项
- API 密钥存储在 `.env` 文件中,确保该文件不会被提交到代码仓库
- 在生产环境中,建议使用环境变量或密钥管理服务来存储 API 密钥
- 生产环境建议使用 HTTPS
- 定期检查并更新依赖包
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>DeepSeek</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Fira+Code&display=swap" rel="stylesheet">
<style>
html, body {
margin: 0;
padding: 0;
overflow: hidden;
width: 100vw;
height: 100vh;
max-width: 100vw;
max-height: 100vh;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
\ No newline at end of file
// 简单的依赖安装脚本
import fs from 'fs';
import path from 'path';
// 读取package.json
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
console.log('检查依赖...');
// 检查关键依赖是否存在
const requiredDeps = [
'@vitejs/plugin-vue',
'vue',
'vue-router',
'vuex'
];
let missingDeps = [];
requiredDeps.forEach(dep => {
const depPath = path.join('node_modules', dep);
if (!fs.existsSync(depPath)) {
missingDeps.push(dep);
}
});
if (missingDeps.length > 0) {
console.log('缺失的依赖:', missingDeps);
console.log('请手动安装这些依赖');
} else {
console.log('所有关键依赖都已安装');
}
// 创建简单的vite配置备用
const viteConfigBackup = `
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 5174
}
})
`;
fs.writeFileSync('vite.config.backup.js', viteConfigBackup);
console.log('创建了备用vite配置文件');
\ No newline at end of file
This diff is collapsed.
{
"name": "deepseek-chat",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"marked": "^12.0.0",
"openai": "^4.28.0",
"uuid": "^11.1.0",
"vue": "^3.3.0",
"vue-router": "^4.0.0",
"vuex": "^4.0.0",
"3d-force-graph": "^1.67.6",
"three-spritetext": "^1.5.3",
"core-js": "^3.6.5",
"d3": "^6.2.0",
"d3-context-menu": "^1.1.2"
},
"devDependencies": {
"vite": "^5.0.0",
"@vitejs/plugin-vue": "^4.0.0"
}
}
<template>
<router-view />
</template>
<script setup>
// App.vue 现在只作为路由的入口点
// 所有的布局和导航逻辑都移到 Layout.vue 中
</script>
<style>
/* 全局样式可以保留在这里 */
</style>
\ No newline at end of file
<template>
<div class="input-container">
<div class="input-wrapper">
<textarea
class="chat-input"
v-model="message"
@keydown.enter.prevent="sendMessage"
placeholder="输入消息,按回车发送..."
rows="1"
ref="textarea"
:disabled="isLoading"
></textarea>
<button
class="send-btn"
@click="sendMessage"
:disabled="isLoading || !message.trim()"
>
发送
</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
isLoading: {
type: Boolean,
default: false
}
});
const message = ref('');
const textarea = ref(null);
const emit = defineEmits(['send']);
const sendMessage = () => {
if (message.value.trim()) {
emit('send', message.value);
message.value = '';
}
};
</script>
<style scoped>
.input-container {
position: absolute;
bottom: 60px;
left: 0;
right: 0;
padding: 20px 40px;
background: linear-gradient(to bottom, transparent, var(--bg-color) 20%);
}
:global(.dark-mode) .input-container {
--bg-color: #1a1b1e;
}
.input-wrapper {
position: relative;
max-width: 800px;
margin: 0 auto;
display: flex;
gap: 12px;
align-items: flex-start;
background: white;
border-radius: 12px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
padding: 12px;
border: 1px solid #eee;
transition: all 0.3s ease;
}
:global(.dark-mode) .input-wrapper {
background: #2d3748;
border-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
}
.chat-input {
flex: 1;
padding: 8px 12px;
border-radius: 8px;
border: 1px solid transparent;
background-color: transparent;
color: #333;
resize: none;
font-size: 14px;
line-height: 1.5;
transition: all 0.3s ease;
height: 24px;
min-height: 24px;
outline: none;
}
:global(.dark-mode) .chat-input {
color: #e2e8f0;
}
:global(.dark-mode) .chat-input::placeholder {
color: #a0aec0;
}
.chat-input:focus {
border-color: #e6e6e6;
}
:global(.dark-mode) .chat-input:focus {
border-color: rgba(255, 255, 255, 0.2);
}
.send-btn {
padding: 8px 20px;
border: none;
border-radius: 6px;
background-color: #4a5568;
color: white;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
height: 36px;
}
.send-btn:hover {
background-color: #2d3748;
}
:global(.dark-mode) .send-btn {
background-color: #63b3ed;
}
:global(.dark-mode) .send-btn:hover {
background-color: #4299e1;
}
.send-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
:global(.dark-mode) .send-btn:disabled {
background-color: #718096;
}
.chat-input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
:global(.dark-mode) .chat-input:disabled {
background-color: rgba(45, 55, 72, 0.5);
}
</style>
\ No newline at end of file
<template>
<div :class="['message', `${role}-message`]">
<div class="message-content">
<template v-if="role === 'assistant'">
<div v-if="reasoning" class="thinking-section">
<div class="thinking-header" @click="toggleThinking">
<span class="toggle-icon">{{ isThinkingExpanded ? '▼' : '▶' }}</span>
思考过程
</div>
<div class="thinking-content" v-show="isThinkingExpanded" v-html="formattedReasoning"></div>
</div>
<div v-if="content && !isThinking" class="answer-content" v-html="formattedContent"></div>
<div v-if="isThinking" class="thinking-indicator" v-html="formattedContent"></div>
</template>
<div v-else v-html="formattedContent"></div>
</div>
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue';
import { marked } from 'marked';
const props = defineProps({
content: {
type: String,
required: true
},
role: {
type: String,
required: true
},
isThinking: {
type: Boolean,
default: false
},
reasoning: {
type: String,
default: ''
}
});
// 默认关闭,只有在生成新内容时才打开
const isThinkingExpanded = ref(false);
// 用于跟踪是否是新生成的消息
const isNewMessage = ref(false);
// 监听 reasoning 的变化
watch(() => props.reasoning, (newVal, oldVal) => {
// 如果是从空变为有内容,说明是新生成的思考过程,则展开
if (!oldVal && newVal) {
isThinkingExpanded.value = true;
isNewMessage.value = true;
}
// 如果直接设置了完整内容(加载历史消息时),则保持折叠
});
// 监听 isThinking 的变化
watch(() => props.isThinking, (newVal) => {
// 当开始思考时展开
if (newVal) {
isThinkingExpanded.value = true;
isNewMessage.value = true;
}
// 当思考完成时,如果是新消息则保持展开
else if (isNewMessage.value) {
isThinkingExpanded.value = true;
}
});
const formattedContent = computed(() => {
return marked(props.content);
});
const formattedReasoning = computed(() => {
return marked(props.reasoning);
});
const toggleThinking = () => {
isThinkingExpanded.value = !isThinkingExpanded.value;
};
</script>
<style scoped>
.message {
display: flex;
padding: 16px 20px;
position: relative;
}
:global(.dark-mode) .user-message {
background-color: rgba(45, 55, 72, 0.3);
}
:global(.dark-mode) .user-message .message-content {
color: #e2e8f0;
}
:global(.dark-mode) .assistant-message {
background-color: rgba(45, 55, 72, 0.2);
}
:global(.dark-mode) .assistant-message .message-content {
color: #e2e8f0;
}
:global(.dark-mode) .thinking-section {
border-color: rgba(255, 255, 255, 0.1);
background: rgba(45, 55, 72, 0.3);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
:global(.dark-mode) .thinking-header {
background-color: rgba(45, 55, 72, 0.4);
color: #e2e8f0;
border-bottom-color: rgba(255, 255, 255, 0.1);
}
:global(.dark-mode) .thinking-header:hover {
background-color: rgba(45, 55, 72, 0.5);
}
:global(.dark-mode) .toggle-icon {
color: #a0aec0;
}
:global(.dark-mode) .thinking-content {
background-color: rgba(45, 55, 72, 0.2);
color: #e2e8f0;
}
:global(.dark-mode) .message-content :deep(pre) {
background-color: rgba(26, 32, 44, 0.95);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
:global(.dark-mode) .message-content :deep(code) {
background-color: rgba(45, 55, 72, 0.3);
color: #e2e8f0;
}
:global(.dark-mode) .thinking-indicator {
color: #a0aec0;
}
.user-message {
background-color: rgba(241, 243, 245, 0.7);
}
.user-message .message-content {
color: #2d3748;
font-weight: 500;
}
.assistant-message {
background-color: rgba(255, 255, 255, 0.7);
}
.assistant-message .message-content {
color: #2d3748;
}
.message-content {
flex: 1;
max-width: 100%;
margin: 0 auto;
line-height: 1.6;
}
.message + .message {
border-top: 1px solid rgba(0, 0, 0, 0.03);
}
.thinking-section {
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 8px;
margin: 8px 0;
overflow: hidden;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
}
.thinking-header {
padding: 10px 16px;
background-color: rgba(241, 243, 245, 0.7);
cursor: pointer;
display: flex;
align-items: center;
font-size: 14px;
user-select: none;
transition: all 0.2s ease;
color: #4a5568;
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
}
.thinking-header:hover {
background-color: rgba(237, 239, 241, 0.9);
color: #2d3748;
}
.toggle-icon {
margin-right: 8px;
font-size: 12px;
transition: transform 0.3s ease;
color: #718096;
}
.thinking-content {
padding: 16px;
font-size: 14px;
line-height: 1.6;
background-color: rgba(255, 255, 255, 0.7);
color: #4a5568;
}
.message-content :deep(pre) {
background-color: rgba(45, 55, 72, 0.97);
padding: 16px;
border-radius: 8px;
overflow-x: auto;
margin: 12px 0;
font-family: 'Fira Code', monospace;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.message-content :deep(code) {
background-color: rgba(45, 55, 72, 0.06);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Fira Code', monospace;
font-size: 0.9em;
color: #2d3748;
}
.message-content :deep(p) {
margin: 8px 0;
line-height: 1.6;
}
.message-content :deep(ul), .message-content :deep(ol) {
margin: 8px 0;
padding-left: 24px;
}
.message-content :deep(li) {
margin: 4px 0;
}
.answer-content {
margin-top: 8px;
color: inherit;
}
.thinking-indicator {
color: #718096;
font-style: italic;
animation: thinking 1.5s infinite;
padding: 8px 0;
}
@keyframes thinking {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
</style>
\ No newline at end of file
<template>
<Transition name="modal">
<div v-if="modelValue" class="modal-overlay" @click="$emit('update:modelValue', false)">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>{{ title }}</h3>
</div>
<div class="modal-body">
{{ message }}
</div>
<div class="modal-footer">
<button class="btn btn-cancel" @click="$emit('update:modelValue', false)">取消</button>
<button class="btn btn-confirm" @click="confirm">确认</button>
</div>
</div>
</div>
</Transition>
</template>
<script setup>
defineProps({
modelValue: Boolean,
title: {
type: String,
default: '确认'
},
message: {
type: String,
required: true
}
});
const emit = defineEmits(['update:modelValue', 'confirm']);
const confirm = () => {
emit('update:modelValue', false);
emit('confirm');
};
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: var(--chat-background);
border-radius: 8px;
width: 90%;
max-width: 400px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.modal-header {
padding: 16px 20px;
border-bottom: 1px solid #565869;
}
.modal-header h3 {
margin: 0;
font-size: 1.2em;
color: var(--text-color);
}
.modal-body {
padding: 20px;
color: var(--text-color);
line-height: 1.5;
}
.modal-footer {
padding: 16px 20px;
border-top: 1px solid #565869;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.btn {
padding: 8px 16px;
border-radius: 4px;
border: 1px solid #565869;
background-color: transparent;
color: var(--text-color);
cursor: pointer;
transition: all 0.2s;
}
.btn-cancel:hover {
background-color: #40414f;
}
.btn-confirm {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-confirm:hover {
opacity: 0.9;
}
/* 过渡动画 */
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s ease;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
.modal-enter-active .modal-content {
animation: modal-in 0.3s ease-out;
}
.modal-leave-active .modal-content {
animation: modal-in 0.3s ease-out reverse;
}
@keyframes modal-in {
from {
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
</style>
\ No newline at end of file
This diff is collapsed.
<template>
<div class="lineContainer">
<svg width="500" height="270">
<g style="transform: translate(0, 10px)">
<path :d="line" />
</g>
</svg>
</div>
</template>
<script>
export default {
name: 'd3line',
data() {
return {
data: [99, 71, 78, 25, 36, 92],
line: '',
};
},
mounted() {
this.calculatePath();
},
methods: {
getScales() {
const x = d3.scaleTime().range([0, 430]);
const y = d3.scaleLinear().range([210, 0]);
d3.axisLeft().scale(x);
d3.axisBottom().scale(y);
x.domain(d3.extent(this.data, (d, i) => i));
y.domain([0, d3.max(this.data, d => d)]);
return { x, y };
},
calculatePath() {
const scale = this.getScales();
const path = d3.line()
.x((d, i) => scale.x(i))
.y(d => scale.y(d));
this.line = path(this.data);
},
},
}
</script>
<style lang="scss" scoped>
.lineContainer {
position: relative;
border: 2px #000 solid;
background-color: #9dadc1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
svg {
margin: 25px;
path {
fill: none;
stroke: #76BF8A;
stroke-width: 3px;
}
}
}
</style>
\ No newline at end of file
<template>
<div style="margin-top: 20px;width: 500px;">
<!-- <el-button style="margin-top: 15px;" @click="query">图数据切换,动态更新</el-button> -->
<el-autocomplete
style="width: 500px"
class="inline-input"
v-model="input"
:fetch-suggestions="querySearch"
placeholder="请输入内容"
:trigger-on-focus="false"
@select="handleSelect"
clearable
>
<!-- <el-select
v-model="mode"
slot="prepend"
placeholder="关键字查询"
>
<el-option label="关键字查询" value="1"></el-option>
<el-option label="单实体查询" value="2"></el-option>
<el-option label="关联查询" value="3"></el-option>
</el-select> -->
<el-button
slot="append"
type="success"
icon="el-icon-search"
@click="query"
>搜索</el-button>
</el-autocomplete>
</div>
</template>
<script>
// 导入JSON数据
import recordsData from '../data/records.json'
import top5Data from '../data/top5.json'
export default {
name: 'gSearch',
// props: {
// isShowPrepend: {
// type: Boolean,
// default: true
// }
// },
data () {
return {
input: '',
mode: '1',
// 后台请求到的json数据
data: recordsData,
results: []
}
},
mounted () {
this.$emit('getData', this.data)
this.results = this.loadAll()
},
methods: {
query () {
// console.log(typeof this.mode)
if (this.data.length <= 20) {
this.data = top5Data
} else {
this.data = recordsData
}
this.$emit('getData', this.data)
},
querySearch (queryString, cb) {
var res = this.results
var results = queryString ? res.filter(this.createFilter(queryString)) : res
// 调用 callback 返回建议列表的数据
cb(results)
},
createFilter (queryString) {
return (res) => {
return (res.value.toLowerCase().indexOf(queryString.toLowerCase()) !== -1)
}
},
// 模拟加载数据
loadAll () {
return [
{ value: '浙江鹏顺进出口有限公司', address: '浙江诸暨艮塔路9号银证大厦8楼' },
{ value: '玉环达丰环保设备有限公司', address: '玉环市芦浦镇漩门工业城' },
{ value: '宁波海天精工股份有限公司', address: '宁波市北仑区黄山西路235号' },
{ value: '象山东兴雕刻古董家具有限公司', address: '城西路4号' },
{ value: '绍兴千海进出口有限公司', address: '绍兴袍江启圣路以南与越英路交叉口生产车间' },
{ value: '深圳万测进出口有限公司', address: '深圳' }
]
},
handleSelect (item) {
console.log(item)
}
}
}
</script>
<style lang='scss' scoped>
.el-select {
width: 120px;
// background-color: #fff;
}
.input-with-select .el-input-group__prepend {
background-color: #6ecbf3;
}
</style>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import { createApp } from 'vue'
import App from './App.vue'
// 创建Vue应用实例
const app = createApp(App)
// 同步导入路由和状态管理
import router from './router/index.js'
import store from './store/index.js'
// 使用路由和状态管理
app.use(router)
app.use(store)
// 挂载应用
app.mount('#app')
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment