This commit is contained in:
fuzhongyun 2025-11-21 15:16:45 +08:00
parent ee481a8b05
commit 582c29d50c
14 changed files with 317 additions and 172 deletions

View File

@ -11,6 +11,7 @@
"ant-design-vue": "^4.0.8",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.511.0",
"marked": "^17.0.1",
"pinia": "^2.1.7",
"tailwind-merge": "^3.3.0",
"vue": "^3.4.15",
@ -3827,6 +3828,18 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/marked": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz",
"integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",

View File

@ -15,6 +15,7 @@
"ant-design-vue": "^4.0.8",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.511.0",
"marked": "^17.0.1",
"pinia": "^2.1.7",
"tailwind-merge": "^3.3.0",
"vue": "^3.4.15",

View File

@ -35,7 +35,7 @@ html, body {
#app {
height: 100vh;
overflow: hidden;
overflow: auto;
}
/* 字体大小类 */
@ -162,4 +162,4 @@ html, body {
font-size: 17px;
}
}
</style>
</style>

View File

@ -123,7 +123,7 @@ export class MockAPIService {
}
// 模拟聊天流式响应
async *chatStream(message: string, conversationId?: string): AsyncGenerator<SSEResponse> {
async *chatStream(message: string, conversationId?: string, tag?: 'order' | 'product'): AsyncGenerator<SSEResponse> {
const sessionId = generateSessionId();
try {
@ -136,7 +136,8 @@ export class MockAPIService {
await new Promise(resolve => setTimeout(resolve, randomDelay(300, 800)));
// 2. 判断是否需要显示订单信息
const shouldShowOrder = message.includes('订单') || message.includes('查询') || message.includes('购买');
const shouldShowOrder = tag === 'order' || message.includes('订单') || message.includes('查询') || message.includes('购买');
const isProductQuery = tag === 'product' || message.includes('商品') || message.includes('价格') || message.includes('库存');
if (shouldShowOrder) {
// 显示订单相关回复
@ -158,6 +159,9 @@ export class MockAPIService {
},
randomOrder
);
} else if (isProductQuery) {
const response = '我来为您查询商品信息与库存情况...';
yield* this.generateStreamResponse(sessionId, response, 'general_chat_stream');
} else {
// 普通回复
const response = mockResponses[Math.floor(Math.random() * mockResponses.length)];
@ -219,4 +223,4 @@ export class MockAPIService {
}
// 导出单例实例
export const mockAPI = MockAPIService.getInstance();
export const mockAPI = MockAPIService.getInstance();

View File

@ -14,14 +14,14 @@ export class DevServer {
// 处理聊天请求
async handleChatRequest(request: ChatRequest): Promise<Response> {
const { message, conversation_id } = request;
const { message, conversation_id, tag } = request;
// 创建可读流
const stream = new ReadableStream({
async start(controller) {
try {
// 使用 Mock API 生成流式响应
for await (const response of mockAPI.chatStream(message, conversation_id)) {
for await (const response of mockAPI.chatStream(message, conversation_id, tag)) {
// 格式化为 SSE 格式
const sseData = `data: ${JSON.stringify(response)}\n\n`;
controller.enqueue(new TextEncoder().encode(sseData));
@ -90,4 +90,4 @@ export const setupMockAPI = () => {
export const restoreFetch = () => {
// 这里可以实现恢复逻辑,但在开发环境中通常不需要
console.log('Mock API已禁用');
};
};

View File

@ -43,7 +43,7 @@
v-for="action in quickActions"
:key="action.text"
class="quick-action-btn"
@click="selectQuickAction(action.text)"
@click="selectQuickAction(action)"
>
{{ action.text }}
</button>
@ -63,6 +63,7 @@ import { ref, computed, nextTick, onMounted } from 'vue';
interface QuickAction {
text: string;
value?: string;
tag?: 'order' | 'product';
}
interface Props {
@ -75,7 +76,7 @@ interface Props {
}
interface Emits {
(e: 'send', message: string): void;
(e: 'send', message: string, options?: { tag?: 'order' | 'product' }): void;
(e: 'input', value: string): void;
}
@ -85,10 +86,8 @@ const props = withDefaults(defineProps<Props>(), {
isLoading: false,
showQuickActions: true,
quickActions: () => [
{ text: '查询订单' },
{ text: '退换货' },
{ text: '物流信息' },
{ text: '联系人工客服' }
{ text: '订单诊断', tag: 'order' },
{ text: '商品查询', tag: 'product' }
],
maxLength: 1000
});
@ -160,20 +159,18 @@ const handleSend = () => {
const message = inputText.value.trim();
if (message) {
emit('send', message);
emit('send', message, currentTag.value ? { tag: currentTag.value } : undefined);
inputText.value = '';
currentTag.value = undefined;
adjustHeight();
}
};
//
const selectQuickAction = (text: string) => {
inputText.value = text;
adjustHeight();
//
nextTick(() => {
handleSend();
});
const currentTag = ref<'order' | 'product' | undefined>(undefined);
const selectQuickAction = (action: QuickAction) => {
currentTag.value = action.tag;
};
//
@ -314,4 +311,4 @@ onMounted(() => {
.dark .input-wrapper:focus-within {
@apply border-blue-400 ring-blue-400;
}
</style>
</style>

View File

@ -18,11 +18,11 @@
</div>
<div class="ai-content">
<div class="message-bubble ai-bubble">
<div class="typing-text" v-if="isTyping">
{{ displayContent }}
<div v-if="isTyping" class="typing-text">
<div class="markdown" v-html="rendered"></div>
<span class="cursor">|</span>
</div>
<div v-else>{{ message.content }}</div>
<div v-else class="markdown" v-html="rendered"></div>
</div>
<div class="message-time">
{{ formatTime(message.timestamp) }}
@ -58,6 +58,10 @@
v-if="message.component?.type === 'order_card'"
:order-data="message.component.data"
/>
<SimpleOrderCard
v-else-if="(message as any).component?.order"
:order="(message as any).component.order"
/>
</div>
<div class="message-time">
{{ formatTime(message.timestamp) }}
@ -70,8 +74,10 @@
<script setup lang="ts">
import { computed, ref, watch, onMounted } from 'vue';
import { marked } from 'marked';
import type { Message } from '@/types';
import OrderCard from './OrderCard.vue';
import SimpleOrderCard from './SimpleOrderCard.vue';
interface Props {
message: Message;
@ -84,6 +90,9 @@ const props = withDefaults(defineProps<Props>(), {
//
const displayContent = ref('');
const rendered = ref('');
const sanitizeMd = (s: string) => (s || '').replace(/</g, '&lt;').replace(/>/g, '&gt;');
//
const messageClass = computed(() => ({
@ -126,13 +135,13 @@ const formatTime = (timestamp: number): string => {
//
watch(() => props.message.content, (newContent) => {
if (props.isTyping && props.message.type === 'ai') {
displayContent.value = newContent;
}
displayContent.value = newContent;
rendered.value = marked.parse(sanitizeMd(newContent || ''));
}, { immediate: true });
onMounted(() => {
displayContent.value = props.message.content;
rendered.value = marked.parse(sanitizeMd(displayContent.value || ''));
});
</script>
@ -179,6 +188,16 @@ onMounted(() => {
@apply bg-gray-100 text-gray-800 px-4 py-2 rounded-2xl rounded-bl-md max-w-xs md:max-w-md break-words;
}
.markdown :is(h1,h2,h3,h4,h5,h6) { @apply font-semibold text-gray-900 }
.markdown p { @apply text-gray-800 leading-6 }
.markdown code { @apply bg-gray-200 rounded px-1 py-0.5 text-sm }
.markdown pre { @apply bg-gray-200 rounded p-3 overflow-auto text-sm }
.markdown ul { @apply list-disc pl-5 }
.markdown ol { @apply list-decimal pl-5 }
.dark .markdown :is(h1,h2,h3,h4,h5,h6) { @apply text-gray-100 }
.dark .markdown p { @apply text-gray-100 }
.dark .markdown code, .dark .markdown pre { @apply bg-gray-700 text-gray-100 }
/* 思考过程样式 */
.thinking-message {
@apply flex items-start space-x-3 opacity-70;
@ -275,4 +294,4 @@ onMounted(() => {
.dark .message-time {
@apply text-gray-400;
}
</style>
</style>

View File

@ -0,0 +1,65 @@
<template>
<div class="simple-order-card">
<div class="row">
<span class="label">订单号</span>
<span class="value">{{ order.id }}</span>
</div>
<div class="row">
<span class="label">商品</span>
<span class="value">{{ order.product }}</span>
</div>
<div class="row">
<span class="label">数量</span>
<span class="value">{{ order.amount }}</span>
</div>
<div class="row">
<span class="label">状态</span>
<span class="value" :class="statusClass">{{ order.status }}</span>
</div>
<div class="row">
<span class="label">创建时间</span>
<span class="value">{{ order.create_time }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { WSOrder } from '@/types'
interface Props {
order: WSOrder
}
const props = defineProps<Props>()
const statusClass = computed(() => {
const s = props.order.status
return {
'status-failed': s === 'failed',
'status-success': s === 'success' || s === 'completed',
'status-pending': s === 'pending'
}
})
</script>
<style scoped>
.simple-order-card {
display: grid;
grid-template-columns: 100px 1fr;
gap: 6px 12px;
padding: 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fff;
}
.row { display: contents }
.label { color: #6b7280; font-size: 12px }
.value { color: #111827; font-size: 14px }
.status-failed { color: #b91c1c }
.status-success { color: #16a34a }
.status-pending { color: #ca8a04 }
.dark .simple-order-card { background: #1f2937; border-color: #374151 }
.dark .label { color: #9ca3af }
.dark .value { color: #e5e7eb }
</style>

View File

@ -9,37 +9,7 @@
{{ connectionStatusText }}
</span>
</div>
<div class="header-actions">
<button
class="action-btn"
@click="clearChat"
:disabled="!hasMessages"
title="清空对话"
>
<svg class="action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1-1H8a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
<button
class="action-btn"
@click="goToHistory"
title="历史对话"
>
<svg class="action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</button>
<button
class="action-btn"
@click="goToSettings"
title="设置"
>
<svg class="action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
</button>
</div>
<div class="header-actions"></div>
</div>
</header>
@ -89,16 +59,13 @@
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useChatStore } from '@/stores/chat';
import { useHistoryStore } from '@/stores/history';
import { useSettingsStore } from '@/stores/settings';
import { setupMockAPI } from '@/api/server';
import MessageList from '@/components/MessageList.vue';
import ChatInput from '@/components/ChatInput.vue';
//
const router = useRouter();
const chatStore = useChatStore();
const historyStore = useHistoryStore();
const settingsStore = useSettingsStore();
//
@ -128,7 +95,7 @@ const connectionStatusText = computed(() => {
});
//
const handleSendMessage = async (message: string) => {
const handleSendMessage = async (message: string, options?: { tag?: 'order' | 'product' }) => {
try {
clearError();
@ -138,12 +105,9 @@ const handleSendMessage = async (message: string) => {
}
//
await chatStore.sendMessage(message);
await chatStore.sendMessage(message, options);
//
if (chatStore.currentConversation) {
historyStore.saveConversation(chatStore.currentConversation);
}
} catch (error) {
console.error('发送消息失败:', error);
@ -162,14 +126,10 @@ const clearChat = () => {
};
//
const goToHistory = () => {
router.push('/history');
};
const goToHistory = () => {};
//
const goToSettings = () => {
router.push('/settings');
};
const goToSettings = () => {};
//
const showError = (message: string) => {
@ -224,21 +184,19 @@ const handleKeydown = (event: KeyboardEvent) => {
};
onMounted(() => {
// Mock API
setupMockAPI();
//
settingsStore.loadSettings();
settingsStore.initializeTheme();
//
historyStore.loadConversations();
//
if (!chatStore.currentConversation) {
chatStore.createNewConversation();
}
// WebSocket
chatStore.connectWS();
//
chatInputRef.value?.focus();
@ -304,7 +262,7 @@ onUnmounted(() => {
/* 主体区域 */
.chat-main {
@apply flex-1 overflow-hidden;
@apply flex-1 overflow-y-auto;
}
/* 底部输入区域 */
@ -406,4 +364,4 @@ onUnmounted(() => {
@apply bottom-16 left-2 right-2;
}
}
</style>
</style>

View File

@ -1,7 +1,5 @@
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
import ChatPage from '@/pages/ChatPage.vue';
import HistoryPage from '@/pages/HistoryPage.vue';
import SettingsPage from '@/pages/SettingsPage.vue';
const routes: RouteRecordRaw[] = [
{
@ -12,22 +10,7 @@ const routes: RouteRecordRaw[] = [
title: 'AI客服对话'
}
},
{
path: '/history',
name: 'History',
component: HistoryPage,
meta: {
title: '历史对话'
}
},
{
path: '/settings',
name: 'Settings',
component: SettingsPage,
meta: {
title: '设置'
}
},
{
path: '/:pathMatch(.*)*',
redirect: '/'
@ -47,4 +30,4 @@ router.beforeEach((to, from, next) => {
next();
});
export default router;
export default router;

View File

@ -9,6 +9,7 @@ export const useChatStore = defineStore('chat', () => {
const isLoading = ref(false);
const isConnected = ref(false);
const currentSessionId = ref<string>('');
const wsRef = ref<WebSocket | null>(null);
// 计算属性
const hasMessages = computed(() => messages.value.length > 0);
@ -71,6 +72,17 @@ export const useChatStore = defineStore('chat', () => {
}
};
const appendToLastMessage = (chunk: string, isComplete = false) => {
if (messages.value.length > 0) {
const lastMsg = messages.value[messages.value.length - 1];
lastMsg.content = (lastMsg.content || '') + (chunk || '');
if (currentConversation.value) {
currentConversation.value.messages = [...messages.value];
currentConversation.value.updated_at = Date.now();
}
}
};
// 处理SSE响应
const handleSSEResponse = (response: SSEResponse) => {
const { type, payload } = response;
@ -86,30 +98,29 @@ export const useChatStore = defineStore('chat', () => {
// 根据数据类型处理不同的消息
switch (data_type) {
case 'general_chat_stream':
if (content.chunk) {
// 如果是新的AI回复创建新消息
if (content.chunk || content.full_message) {
if (messages.value.length === 0 || messages.value[messages.value.length - 1].type !== 'ai') {
addMessage({
type: 'ai',
content: content.chunk
});
addMessage({ type: 'ai', content: content.full_message || content.chunk });
} else {
// 更新现有消息
updateLastMessage(content.full_message || content.chunk, content.is_final);
if (content.full_message) {
updateLastMessage(content.full_message, content.is_final);
} else if (content.chunk) {
appendToLastMessage(content.chunk, content.is_final);
}
}
}
break;
case 'thinking_process':
if (content.chunk) {
// 思考过程消息
if (content.chunk || content.full_message) {
if (messages.value.length === 0 || messages.value[messages.value.length - 1].type !== 'thinking') {
addMessage({
type: 'thinking',
content: content.chunk
});
addMessage({ type: 'thinking', content: content.full_message || content.chunk });
} else {
updateLastMessage(content.full_message || content.chunk, content.is_final);
if (content.full_message) {
updateLastMessage(content.full_message, content.is_final);
} else if (content.chunk) {
appendToLastMessage(content.chunk, content.is_final);
}
}
}
break;
@ -131,7 +142,7 @@ export const useChatStore = defineStore('chat', () => {
};
// 发送消息
const sendMessage = async (content: string) => {
const sendMessage = async (content: string, options?: { tag?: 'order' | 'product' }) => {
if (!content.trim()) return;
// 如果没有当前对话,创建新对话
@ -148,63 +159,71 @@ export const useChatStore = defineStore('chat', () => {
isLoading.value = true;
try {
// 调用Mock API这里先用简单的模拟
const response = await fetch('/api/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: content,
conversation_id: currentConversation.value?.id
})
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
// 处理SSE流
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (reader) {
isConnected.value = true;
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
handleSSEResponse(data);
} catch (e) {
console.error('Failed to parse SSE data:', e);
}
}
if (wsRef.value && wsRef.value.readyState === WebSocket.OPEN) {
const payload: any = { message: content.trim(), session_id: 'default' };
if (options?.tag) payload.tag = options.tag;
wsRef.value.send(JSON.stringify(payload));
} else {
const WS_URL = 'ws://127.0.0.1:8302/api/chat/ws';
const ws = new WebSocket(WS_URL);
wsRef.value = ws;
ws.onopen = () => {
isConnected.value = true;
const payload: any = { message: content.trim(), session_id: 'default' };
if (options?.tag) payload.tag = options.tag;
ws.send(JSON.stringify(payload));
};
ws.onmessage = (event: MessageEvent) => {
try {
const raw = typeof event.data === 'string' ? event.data : '';
const data: SSEResponse = JSON.parse(raw);
handleSSEResponse(data);
} catch (e) {
console.error('WS message parse error:', e);
}
}
isConnected.value = false;
};
ws.onerror = () => {
isLoading.value = false;
isConnected.value = false;
addMessage({ type: 'ai', content: '抱歉,连接后端失败,请稍后重试。' });
};
ws.onclose = () => {
wsRef.value = null;
isConnected.value = false;
isLoading.value = false;
};
}
} catch (error) {
console.error('Failed to send message:', error);
isLoading.value = false;
isConnected.value = false;
// 添加错误消息
addMessage({
type: 'ai',
content: '抱歉,发送消息时出现错误,请稍后重试。'
});
addMessage({ type: 'ai', content: '抱歉,建立连接失败,请稍后重试。' });
}
};
const connectWS = () => {
if (wsRef.value && wsRef.value.readyState === WebSocket.OPEN) return;
const WS_URL = 'ws://127.0.0.1:8302/api/chat/ws';
const ws = new WebSocket(WS_URL);
wsRef.value = ws;
ws.onopen = () => {
isConnected.value = true;
};
ws.onmessage = (event: MessageEvent) => {
try {
const raw = typeof event.data === 'string' ? event.data : '';
const data: SSEResponse = JSON.parse(raw);
handleSSEResponse(data);
} catch {}
};
ws.onerror = () => {
isConnected.value = false;
};
ws.onclose = () => {
wsRef.value = null;
isConnected.value = false;
};
};
// 清空当前对话
const clearCurrentConversation = () => {
messages.value = [];
@ -237,7 +256,8 @@ export const useChatStore = defineStore('chat', () => {
updateLastMessage,
handleSSEResponse,
sendMessage,
connectWS,
clearCurrentConversation,
loadConversation
};
});
});

View File

@ -40,7 +40,7 @@ export interface SSEResponse {
full_message: string;
is_final: boolean;
};
component?: OrderCardData;
component?: OrderCardData | { order: WSOrder };
};
}
@ -59,6 +59,7 @@ export interface SSEErrorResponse {
export interface ChatRequest {
message: string;
conversation_id?: string;
tag?: 'order' | 'product';
}
// LocalStorage 键值结构
@ -77,4 +78,12 @@ export interface Settings {
theme: 'light' | 'dark' | 'auto';
fontSize: 'small' | 'medium' | 'large';
autoSave: boolean;
}
}
export interface WSOrder {
id: string;
product: string;
amount: number;
status: string;
create_time: string;
}

View File

@ -0,0 +1,38 @@
// vite.config.ts
import { defineConfig } from "file:///mnt/e/code/courseware/vue3-frontend/node_modules/vite/dist/node/index.js";
import vue from "file:///mnt/e/code/courseware/vue3-frontend/node_modules/@vitejs/plugin-vue/dist/index.mjs";
import { resolve } from "path";
import Inspector from "file:///mnt/e/code/courseware/vue3-frontend/node_modules/unplugin-vue-dev-locator/dist/vite.mjs";
import traeBadgePlugin from "file:///mnt/e/code/courseware/vue3-frontend/node_modules/vite-plugin-trae-solo-badge/dist/vite-plugin.esm.js";
var __vite_injected_original_dirname = "/mnt/e/code/courseware/vue3-frontend";
var vite_config_default = defineConfig({
build: {
sourcemap: "hidden"
},
plugins: [
vue(),
Inspector(),
traeBadgePlugin({
variant: "dark",
position: "bottom-right",
prodOnly: true,
clickable: true,
clickUrl: "https://www.trae.ai/solo?showJoin=1",
autoTheme: true,
autoThemeTarget: "#app"
})
],
resolve: {
alias: {
"@": resolve(__vite_injected_original_dirname, "src")
}
},
server: {
port: 3e3,
open: true
}
});
export {
vite_config_default as default
};
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvbW50L2UvY29kZS9jb3Vyc2V3YXJlL3Z1ZTMtZnJvbnRlbmRcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9tbnQvZS9jb2RlL2NvdXJzZXdhcmUvdnVlMy1mcm9udGVuZC92aXRlLmNvbmZpZy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vbW50L2UvY29kZS9jb3Vyc2V3YXJlL3Z1ZTMtZnJvbnRlbmQvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHZ1ZSBmcm9tICdAdml0ZWpzL3BsdWdpbi12dWUnXG5pbXBvcnQgeyByZXNvbHZlIH0gZnJvbSAncGF0aCdcbmltcG9ydCBJbnNwZWN0b3IgZnJvbSAndW5wbHVnaW4tdnVlLWRldi1sb2NhdG9yL3ZpdGUnXG5pbXBvcnQgdHJhZUJhZGdlUGx1Z2luIGZyb20gJ3ZpdGUtcGx1Z2luLXRyYWUtc29sby1iYWRnZSdcblxuLy8gaHR0cHM6Ly92aXRlLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBidWlsZDoge1xuICAgIHNvdXJjZW1hcDogJ2hpZGRlbicsXG4gIH0sXG4gIHBsdWdpbnM6IFtcbiAgICB2dWUoKSxcbiAgICBJbnNwZWN0b3IoKSxcbiAgICB0cmFlQmFkZ2VQbHVnaW4oe1xuICAgICAgdmFyaWFudDogJ2RhcmsnLFxuICAgICAgcG9zaXRpb246ICdib3R0b20tcmlnaHQnLFxuICAgICAgcHJvZE9ubHk6IHRydWUsXG4gICAgICBjbGlja2FibGU6IHRydWUsXG4gICAgICBjbGlja1VybDogJ2h0dHBzOi8vd3d3LnRyYWUuYWkvc29sbz9zaG93Sm9pbj0xJyxcbiAgICAgIGF1dG9UaGVtZTogdHJ1ZSxcbiAgICAgIGF1dG9UaGVtZVRhcmdldDogJyNhcHAnLFxuICAgIH0pLFxuICBdLFxuICByZXNvbHZlOiB7XG4gICAgYWxpYXM6IHtcbiAgICAgICdAJzogcmVzb2x2ZShfX2Rpcm5hbWUsICdzcmMnKVxuICAgIH1cbiAgfSxcbiAgc2VydmVyOiB7XG4gICAgcG9ydDogMzAwMCxcbiAgICBvcGVuOiB0cnVlXG4gIH1cbn0pXG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQThSLFNBQVMsb0JBQW9CO0FBQzNULE9BQU8sU0FBUztBQUNoQixTQUFTLGVBQWU7QUFDeEIsT0FBTyxlQUFlO0FBQ3RCLE9BQU8scUJBQXFCO0FBSjVCLElBQU0sbUNBQW1DO0FBT3pDLElBQU8sc0JBQVEsYUFBYTtBQUFBLEVBQzFCLE9BQU87QUFBQSxJQUNMLFdBQVc7QUFBQSxFQUNiO0FBQUEsRUFDQSxTQUFTO0FBQUEsSUFDUCxJQUFJO0FBQUEsSUFDSixVQUFVO0FBQUEsSUFDVixnQkFBZ0I7QUFBQSxNQUNkLFNBQVM7QUFBQSxNQUNULFVBQVU7QUFBQSxNQUNWLFVBQVU7QUFBQSxNQUNWLFdBQVc7QUFBQSxNQUNYLFVBQVU7QUFBQSxNQUNWLFdBQVc7QUFBQSxNQUNYLGlCQUFpQjtBQUFBLElBQ25CLENBQUM7QUFBQSxFQUNIO0FBQUEsRUFDQSxTQUFTO0FBQUEsSUFDUCxPQUFPO0FBQUEsTUFDTCxLQUFLLFFBQVEsa0NBQVcsS0FBSztBQUFBLElBQy9CO0FBQUEsRUFDRjtBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=

View File

@ -0,0 +1,38 @@
// vite.config.ts
import { defineConfig } from "file:///mnt/e/code/courseware/vue3-frontend/node_modules/vite/dist/node/index.js";
import vue from "file:///mnt/e/code/courseware/vue3-frontend/node_modules/@vitejs/plugin-vue/dist/index.mjs";
import { resolve } from "path";
import Inspector from "file:///mnt/e/code/courseware/vue3-frontend/node_modules/unplugin-vue-dev-locator/dist/vite.mjs";
import traeBadgePlugin from "file:///mnt/e/code/courseware/vue3-frontend/node_modules/vite-plugin-trae-solo-badge/dist/vite-plugin.esm.js";
var __vite_injected_original_dirname = "/mnt/e/code/courseware/vue3-frontend";
var vite_config_default = defineConfig({
build: {
sourcemap: "hidden"
},
plugins: [
vue(),
Inspector(),
traeBadgePlugin({
variant: "dark",
position: "bottom-right",
prodOnly: true,
clickable: true,
clickUrl: "https://www.trae.ai/solo?showJoin=1",
autoTheme: true,
autoThemeTarget: "#app"
})
],
resolve: {
alias: {
"@": resolve(__vite_injected_original_dirname, "src")
}
},
server: {
port: 3e3,
open: true
}
});
export {
vite_config_default as default
};
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvbW50L2UvY29kZS9jb3Vyc2V3YXJlL3Z1ZTMtZnJvbnRlbmRcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9tbnQvZS9jb2RlL2NvdXJzZXdhcmUvdnVlMy1mcm9udGVuZC92aXRlLmNvbmZpZy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vbW50L2UvY29kZS9jb3Vyc2V3YXJlL3Z1ZTMtZnJvbnRlbmQvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHZ1ZSBmcm9tICdAdml0ZWpzL3BsdWdpbi12dWUnXG5pbXBvcnQgeyByZXNvbHZlIH0gZnJvbSAncGF0aCdcbmltcG9ydCBJbnNwZWN0b3IgZnJvbSAndW5wbHVnaW4tdnVlLWRldi1sb2NhdG9yL3ZpdGUnXG5pbXBvcnQgdHJhZUJhZGdlUGx1Z2luIGZyb20gJ3ZpdGUtcGx1Z2luLXRyYWUtc29sby1iYWRnZSdcblxuLy8gaHR0cHM6Ly92aXRlLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBidWlsZDoge1xuICAgIHNvdXJjZW1hcDogJ2hpZGRlbicsXG4gIH0sXG4gIHBsdWdpbnM6IFtcbiAgICB2dWUoKSxcbiAgICBJbnNwZWN0b3IoKSxcbiAgICB0cmFlQmFkZ2VQbHVnaW4oe1xuICAgICAgdmFyaWFudDogJ2RhcmsnLFxuICAgICAgcG9zaXRpb246ICdib3R0b20tcmlnaHQnLFxuICAgICAgcHJvZE9ubHk6IHRydWUsXG4gICAgICBjbGlja2FibGU6IHRydWUsXG4gICAgICBjbGlja1VybDogJ2h0dHBzOi8vd3d3LnRyYWUuYWkvc29sbz9zaG93Sm9pbj0xJyxcbiAgICAgIGF1dG9UaGVtZTogdHJ1ZSxcbiAgICAgIGF1dG9UaGVtZVRhcmdldDogJyNhcHAnLFxuICAgIH0pLFxuICBdLFxuICByZXNvbHZlOiB7XG4gICAgYWxpYXM6IHtcbiAgICAgICdAJzogcmVzb2x2ZShfX2Rpcm5hbWUsICdzcmMnKVxuICAgIH1cbiAgfSxcbiAgc2VydmVyOiB7XG4gICAgcG9ydDogMzAwMCxcbiAgICBvcGVuOiB0cnVlXG4gIH1cbn0pXG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQThSLFNBQVMsb0JBQW9CO0FBQzNULE9BQU8sU0FBUztBQUNoQixTQUFTLGVBQWU7QUFDeEIsT0FBTyxlQUFlO0FBQ3RCLE9BQU8scUJBQXFCO0FBSjVCLElBQU0sbUNBQW1DO0FBT3pDLElBQU8sc0JBQVEsYUFBYTtBQUFBLEVBQzFCLE9BQU87QUFBQSxJQUNMLFdBQVc7QUFBQSxFQUNiO0FBQUEsRUFDQSxTQUFTO0FBQUEsSUFDUCxJQUFJO0FBQUEsSUFDSixVQUFVO0FBQUEsSUFDVixnQkFBZ0I7QUFBQSxNQUNkLFNBQVM7QUFBQSxNQUNULFVBQVU7QUFBQSxNQUNWLFVBQVU7QUFBQSxNQUNWLFdBQVc7QUFBQSxNQUNYLFVBQVU7QUFBQSxNQUNWLFdBQVc7QUFBQSxNQUNYLGlCQUFpQjtBQUFBLElBQ25CLENBQUM7QUFBQSxFQUNIO0FBQUEsRUFDQSxTQUFTO0FBQUEsSUFDUCxPQUFPO0FBQUEsTUFDTCxLQUFLLFFBQVEsa0NBQVcsS0FBSztBQUFBLElBQy9CO0FBQUEsRUFDRjtBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ04sTUFBTTtBQUFBLElBQ04sTUFBTTtBQUFBLEVBQ1I7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=