feat: vue重构
This commit is contained in:
17
.gitignore
vendored
17
.gitignore
vendored
@@ -21,6 +21,23 @@ build_upload.log
|
|||||||
*.spec
|
*.spec
|
||||||
*.zip
|
*.zip
|
||||||
|
|
||||||
|
# frontend
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
dist/
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|||||||
5
frontend/README.md
Normal file
5
frontend/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + TypeScript + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||||
14
frontend/index.html
Normal file
14
frontend/index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>股票分析系统</title>
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<meta name="description" content="股票分析系统 - 基于Vue 3 + TypeScript + Naive UI">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1256
frontend/package-lock.json
generated
Normal file
1256
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
frontend/package.json
Normal file
28
frontend/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "^22.13.9",
|
||||||
|
"@vicons/ionicons5": "^0.13.0",
|
||||||
|
"@vueuse/core": "^12.7.0",
|
||||||
|
"axios": "^1.8.1",
|
||||||
|
"marked": "^15.0.7",
|
||||||
|
"naive-ui": "^2.41.0",
|
||||||
|
"vue": "^3.5.13"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/marked": "^5.0.2",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
"typescript": "~5.7.2",
|
||||||
|
"vite": "^6.2.0",
|
||||||
|
"vue-tsc": "^2.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
frontend/public/vite.svg
Normal file
1
frontend/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
41
frontend/src/App.vue
Normal file
41
frontend/src/App.vue
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<n-config-provider :theme="theme">
|
||||||
|
<n-message-provider>
|
||||||
|
<n-loading-bar-provider>
|
||||||
|
<n-dialog-provider>
|
||||||
|
<n-notification-provider>
|
||||||
|
<StockAnalysisApp />
|
||||||
|
</n-notification-provider>
|
||||||
|
</n-dialog-provider>
|
||||||
|
</n-loading-bar-provider>
|
||||||
|
</n-message-provider>
|
||||||
|
</n-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import {
|
||||||
|
NConfigProvider,
|
||||||
|
NMessageProvider,
|
||||||
|
NLoadingBarProvider,
|
||||||
|
NDialogProvider,
|
||||||
|
NNotificationProvider,
|
||||||
|
} from 'naive-ui'
|
||||||
|
import StockAnalysisApp from './components/StockAnalysisApp.vue'
|
||||||
|
|
||||||
|
// 主题设置 (默认使用亮色主题)
|
||||||
|
const theme = ref<any>(null) // 可以切换为 darkTheme 以启用暗色模式
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||||
|
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
frontend/src/assets/vue.svg
Normal file
1
frontend/src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
128
frontend/src/components/AnnouncementBanner.vue
Normal file
128
frontend/src/components/AnnouncementBanner.vue
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="showAnnouncement" class="announcement-container">
|
||||||
|
<n-card class="announcement-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="announcement-header">
|
||||||
|
<n-icon size="18" :component="InformationCircleIcon" class="info-icon" />
|
||||||
|
<span>系统公告</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="announcement-content" v-html="processedContent"></div>
|
||||||
|
<div class="announcement-timer">{{ remainingTimeText }}</div>
|
||||||
|
<template #action>
|
||||||
|
<n-button quaternary circle size="small" @click="closeAnnouncement">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="CloseIcon" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||||
|
import { NCard, NIcon, NButton } from 'naive-ui';
|
||||||
|
import { InformationCircleOutline as InformationCircleIcon } from '@vicons/ionicons5';
|
||||||
|
import { Close as CloseIcon } from '@vicons/ionicons5';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
content: string;
|
||||||
|
autoCloseTime?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const showAnnouncement = ref(true);
|
||||||
|
const remainingTime = ref(props.autoCloseTime || 5);
|
||||||
|
const timer = ref<number | null>(null);
|
||||||
|
|
||||||
|
const remainingTimeText = computed(() => {
|
||||||
|
return `${remainingTime.value}秒后自动关闭`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const processedContent = computed(() => {
|
||||||
|
// 处理文本中的URL
|
||||||
|
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||||
|
return props.content.replace(
|
||||||
|
urlRegex,
|
||||||
|
'<a href="$1" target="_blank" class="announcement-link">$1</a>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function closeAnnouncement() {
|
||||||
|
showAnnouncement.value = false;
|
||||||
|
if (timer.value !== null) {
|
||||||
|
window.clearInterval(timer.value);
|
||||||
|
timer.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTimer() {
|
||||||
|
if (remainingTime.value <= 1) {
|
||||||
|
closeAnnouncement();
|
||||||
|
} else {
|
||||||
|
remainingTime.value--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
timer.value = window.setInterval(updateTimer, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (timer.value !== null) {
|
||||||
|
window.clearInterval(timer.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.announcement-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
max-width: 24rem;
|
||||||
|
z-index: 50;
|
||||||
|
animation: fadeInDown 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-card {
|
||||||
|
border-left: 4px solid var(--n-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
color: var(--n-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-content {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-timer {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--n-text-color-disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-link {
|
||||||
|
color: var(--n-primary-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
339
frontend/src/components/ApiConfigPanel.vue
Normal file
339
frontend/src/components/ApiConfigPanel.vue
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
<template>
|
||||||
|
<div class="api-config-section">
|
||||||
|
<n-button
|
||||||
|
class="toggle-button"
|
||||||
|
size="small"
|
||||||
|
@click="toggleConfig"
|
||||||
|
:quaternary="true"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="expanded ? ChevronUpIcon : ChevronDownIcon" />
|
||||||
|
</template>
|
||||||
|
API配置 {{ expanded ? '收起' : '展开' }}
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-collapse-transition :show="expanded">
|
||||||
|
<n-card class="api-config-card" content-style="padding: 0.75rem;">
|
||||||
|
<n-alert title="OpenAI API配置" type="info" v-if="isApiInfoVisible">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="InformationCircleIcon" />
|
||||||
|
</template>
|
||||||
|
<p>您可以配置自己的API,也可以使用系统默认配置。API密钥仅在您的浏览器中使用,不会发送到服务器存储。</p>
|
||||||
|
<div class="alert-actions">
|
||||||
|
<n-button text @click="isApiInfoVisible = false">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="CloseIcon" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</n-alert>
|
||||||
|
|
||||||
|
<n-grid :cols="24" :x-gap="12">
|
||||||
|
<n-grid-item :span="14">
|
||||||
|
<n-form-item label="API URL" path="apiUrl">
|
||||||
|
<n-input
|
||||||
|
v-model:value="apiConfig.apiUrl"
|
||||||
|
placeholder="https://api.openai.com/v1/chat/completions"
|
||||||
|
@update:value="handleConfigChange"
|
||||||
|
/>
|
||||||
|
<template #feedback>
|
||||||
|
<span class="formatted-url">{{ formattedUrl }}</span>
|
||||||
|
</template>
|
||||||
|
</n-form-item>
|
||||||
|
</n-grid-item>
|
||||||
|
|
||||||
|
<n-grid-item :span="10">
|
||||||
|
<n-form-item label="API Key" path="apiKey">
|
||||||
|
<n-input
|
||||||
|
v-model:value="apiConfig.apiKey"
|
||||||
|
type="password"
|
||||||
|
placeholder="sk-..."
|
||||||
|
show-password-on="click"
|
||||||
|
@update:value="handleConfigChange"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-grid-item>
|
||||||
|
|
||||||
|
<n-grid-item :span="12">
|
||||||
|
<n-form-item label="模型" path="apiModel">
|
||||||
|
<n-input
|
||||||
|
v-model:value="apiConfig.apiModel"
|
||||||
|
placeholder="gpt-3.5-turbo"
|
||||||
|
@update:value="handleConfigChange"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-grid-item>
|
||||||
|
|
||||||
|
<n-grid-item :span="12">
|
||||||
|
<n-form-item label="超时时间(秒)" path="apiTimeout">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="apiTimeout"
|
||||||
|
placeholder="60"
|
||||||
|
:min="1"
|
||||||
|
:max="300"
|
||||||
|
@update:value="handleTimeoutChange"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
|
||||||
|
<div class="api-actions">
|
||||||
|
<div class="api-save-option">
|
||||||
|
<n-switch
|
||||||
|
v-model:value="apiConfig.saveApiConfig"
|
||||||
|
@update:value="handleConfigChange"
|
||||||
|
/>
|
||||||
|
<span class="save-label">保存配置到本地</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="api-buttons">
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
:loading="testingConnection"
|
||||||
|
:disabled="!isConfigValid"
|
||||||
|
@click="testConnection"
|
||||||
|
>
|
||||||
|
测试连接
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-button @click="resetConfig">
|
||||||
|
重置
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</n-collapse-transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import {
|
||||||
|
NButton,
|
||||||
|
NIcon,
|
||||||
|
NCard,
|
||||||
|
NCollapseTransition,
|
||||||
|
NGrid,
|
||||||
|
NGridItem,
|
||||||
|
NFormItem,
|
||||||
|
NInput,
|
||||||
|
NInputNumber,
|
||||||
|
NSwitch,
|
||||||
|
NAlert,
|
||||||
|
useMessage
|
||||||
|
} from 'naive-ui';
|
||||||
|
import {
|
||||||
|
ChevronDown as ChevronDownIcon,
|
||||||
|
ChevronUp as ChevronUpIcon,
|
||||||
|
InformationCircleOutline as InformationCircleIcon,
|
||||||
|
Close as CloseIcon
|
||||||
|
} from '@vicons/ionicons5';
|
||||||
|
import { apiService } from '@/services/api';
|
||||||
|
import { saveApiConfigToLocalStorage, loadApiConfig } from '@/utils';
|
||||||
|
import type { ApiConfig } from '@/types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
defaultApiUrl?: string;
|
||||||
|
defaultApiModel?: string;
|
||||||
|
defaultApiTimeout?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:apiConfig', value: ApiConfig): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const message = useMessage();
|
||||||
|
const expanded = ref(false);
|
||||||
|
const testingConnection = ref(false);
|
||||||
|
const isApiInfoVisible = ref(true);
|
||||||
|
|
||||||
|
// API配置
|
||||||
|
const apiConfig = ref<ApiConfig>({
|
||||||
|
apiUrl: props.defaultApiUrl || '',
|
||||||
|
apiKey: '',
|
||||||
|
apiModel: props.defaultApiModel || 'gpt-3.5-turbo',
|
||||||
|
apiTimeout: props.defaultApiTimeout || '60',
|
||||||
|
saveApiConfig: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const apiTimeout = computed({
|
||||||
|
get: () => parseInt(apiConfig.value.apiTimeout) || 60,
|
||||||
|
set: (val: number) => {
|
||||||
|
apiConfig.value.apiTimeout = val.toString();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isConfigValid = computed(() => {
|
||||||
|
return apiConfig.value.apiUrl && apiConfig.value.apiKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedUrl = computed(() => {
|
||||||
|
return formatApiUrl(apiConfig.value.apiUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleConfig() {
|
||||||
|
expanded.value = !expanded.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConfigChange() {
|
||||||
|
// 如果选择了保存配置,则自动保存
|
||||||
|
if (apiConfig.value.saveApiConfig) {
|
||||||
|
saveApiConfigToLocalStorage({
|
||||||
|
apiUrl: apiConfig.value.apiUrl,
|
||||||
|
apiKey: apiConfig.value.apiKey,
|
||||||
|
apiModel: apiConfig.value.apiModel,
|
||||||
|
apiTimeout: apiConfig.value.apiTimeout,
|
||||||
|
saveApiConfig: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向父组件发送更新事件
|
||||||
|
emit('update:apiConfig', { ...apiConfig.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTimeoutChange(value: number | null) {
|
||||||
|
if (value !== null) {
|
||||||
|
apiConfig.value.apiTimeout = value.toString();
|
||||||
|
handleConfigChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatApiUrl(url: string): string {
|
||||||
|
if (!url) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 尝试解析URL
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
return `${parsedUrl.origin}${parsedUrl.pathname}`;
|
||||||
|
} catch (e) {
|
||||||
|
// 如果URL格式错误,则返回原始字符串
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testConnection() {
|
||||||
|
if (!isConfigValid.value) {
|
||||||
|
message.error('请填写完整的API配置信息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
testingConnection.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiService.testApiConnection({
|
||||||
|
api_url: apiConfig.value.apiUrl,
|
||||||
|
api_key: apiConfig.value.apiKey,
|
||||||
|
api_model: apiConfig.value.apiModel,
|
||||||
|
api_timeout: apiConfig.value.apiTimeout
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
message.success('API连接测试成功');
|
||||||
|
|
||||||
|
// 如果选择了保存配置,则保存
|
||||||
|
if (apiConfig.value.saveApiConfig) {
|
||||||
|
saveApiConfigToLocalStorage({
|
||||||
|
apiUrl: apiConfig.value.apiUrl,
|
||||||
|
apiKey: apiConfig.value.apiKey,
|
||||||
|
apiModel: apiConfig.value.apiModel,
|
||||||
|
apiTimeout: apiConfig.value.apiTimeout,
|
||||||
|
saveApiConfig: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(`API连接测试失败: ${response.message}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(`测试连接出错: ${error.message || '未知错误'}`);
|
||||||
|
} finally {
|
||||||
|
testingConnection.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetConfig() {
|
||||||
|
apiConfig.value = {
|
||||||
|
apiUrl: props.defaultApiUrl || '',
|
||||||
|
apiKey: '',
|
||||||
|
apiModel: props.defaultApiModel || 'gpt-3.5-turbo',
|
||||||
|
apiTimeout: props.defaultApiTimeout || '60',
|
||||||
|
saveApiConfig: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清除本地存储
|
||||||
|
if (window.localStorage) {
|
||||||
|
localStorage.removeItem('apiConfig');
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success('已重置API配置');
|
||||||
|
emit('update:apiConfig', { ...apiConfig.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 加载保存的配置
|
||||||
|
const savedConfig = loadApiConfig();
|
||||||
|
|
||||||
|
if (savedConfig.saveApiConfig) {
|
||||||
|
apiConfig.value = {
|
||||||
|
apiUrl: savedConfig.apiUrl || props.defaultApiUrl || '',
|
||||||
|
apiKey: savedConfig.apiKey || '',
|
||||||
|
apiModel: savedConfig.apiModel || props.defaultApiModel || 'gpt-3.5-turbo',
|
||||||
|
apiTimeout: savedConfig.apiTimeout || props.defaultApiTimeout || '60',
|
||||||
|
saveApiConfig: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// 通知父组件配置已加载
|
||||||
|
emit('update:apiConfig', { ...apiConfig.value });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.api-config-section {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-config-card {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formatted-url {
|
||||||
|
color: var(--n-text-color-info);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-actions {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-save-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-label {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
133
frontend/src/components/MarketTimeDisplay.vue
Normal file
133
frontend/src/components/MarketTimeDisplay.vue
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<template>
|
||||||
|
<n-card class="market-time-card">
|
||||||
|
<n-grid :x-gap="16" :y-gap="16" :cols="gridCols">
|
||||||
|
<!-- 当前时间 -->
|
||||||
|
<n-grid-item>
|
||||||
|
<div class="time-block">
|
||||||
|
<p class="time-label">当前时间</p>
|
||||||
|
<p class="current-time">{{ marketInfo.currentTime }}</p>
|
||||||
|
</div>
|
||||||
|
</n-grid-item>
|
||||||
|
|
||||||
|
<!-- A股状态 -->
|
||||||
|
<n-grid-item>
|
||||||
|
<div class="time-block">
|
||||||
|
<p class="time-label">A股市场</p>
|
||||||
|
<p class="market-status" :class="marketInfo.cnMarket.isOpen ? 'status-open' : 'status-closed'">
|
||||||
|
{{ marketInfo.cnMarket.isOpen ? '交易中' : '已休市' }}
|
||||||
|
</p>
|
||||||
|
<p class="time-counter">{{ marketInfo.cnMarket.nextTime }}</p>
|
||||||
|
</div>
|
||||||
|
</n-grid-item>
|
||||||
|
|
||||||
|
<!-- 港股状态 -->
|
||||||
|
<n-grid-item>
|
||||||
|
<div class="time-block">
|
||||||
|
<p class="time-label">港股市场</p>
|
||||||
|
<p class="market-status" :class="marketInfo.hkMarket.isOpen ? 'status-open' : 'status-closed'">
|
||||||
|
{{ marketInfo.hkMarket.isOpen ? '交易中' : '已休市' }}
|
||||||
|
</p>
|
||||||
|
<p class="time-counter">{{ marketInfo.hkMarket.nextTime }}</p>
|
||||||
|
</div>
|
||||||
|
</n-grid-item>
|
||||||
|
|
||||||
|
<!-- 美股状态 -->
|
||||||
|
<n-grid-item>
|
||||||
|
<div class="time-block">
|
||||||
|
<p class="time-label">美股市场</p>
|
||||||
|
<p class="market-status" :class="marketInfo.usMarket.isOpen ? 'status-open' : 'status-closed'">
|
||||||
|
{{ marketInfo.usMarket.isOpen ? '交易中' : '已休市' }}
|
||||||
|
</p>
|
||||||
|
<p class="time-counter">{{ marketInfo.usMarket.nextTime }}</p>
|
||||||
|
</div>
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||||
|
import { NCard, NGrid, NGridItem } from 'naive-ui';
|
||||||
|
import { updateMarketTimeInfo } from '@/utils';
|
||||||
|
import type { MarketTimeInfo } from '@/types';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isMobile: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const marketInfo = ref<MarketTimeInfo>({
|
||||||
|
currentTime: '',
|
||||||
|
cnMarket: { isOpen: false, nextTime: '' },
|
||||||
|
hkMarket: { isOpen: false, nextTime: '' },
|
||||||
|
usMarket: { isOpen: false, nextTime: '' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const gridCols = computed(() => {
|
||||||
|
return props.isMobile ? 1 : 4;
|
||||||
|
});
|
||||||
|
|
||||||
|
let intervalId: number | null = null;
|
||||||
|
|
||||||
|
function updateMarketTime() {
|
||||||
|
marketInfo.value = updateMarketTimeInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateMarketTime(); // 立即更新一次
|
||||||
|
intervalId = window.setInterval(updateMarketTime, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (intervalId !== null) {
|
||||||
|
window.clearInterval(intervalId);
|
||||||
|
intervalId = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.market-time-card {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--n-text-color-3);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-time {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--n-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-status {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-open {
|
||||||
|
color: var(--n-success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-closed {
|
||||||
|
color: var(--n-text-color-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-counter {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--n-text-color-3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
489
frontend/src/components/StockAnalysisApp.vue
Normal file
489
frontend/src/components/StockAnalysisApp.vue
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- 公告栏 -->
|
||||||
|
<AnnouncementBanner v-if="announcement" :content="announcement" :auto-close-time="5" />
|
||||||
|
|
||||||
|
<n-layout class="main-layout">
|
||||||
|
<n-layout-content class="main-content">
|
||||||
|
<n-page-header title="股票分析系统">
|
||||||
|
<template #avatar>
|
||||||
|
<n-icon :component="BarChartIcon" color="#2080f0" size="28" />
|
||||||
|
</template>
|
||||||
|
</n-page-header>
|
||||||
|
|
||||||
|
<!-- 市场时间显示 -->
|
||||||
|
<MarketTimeDisplay />
|
||||||
|
|
||||||
|
<!-- API配置面板 -->
|
||||||
|
<ApiConfigPanel
|
||||||
|
:default-api-url="defaultApiUrl"
|
||||||
|
:default-api-model="defaultApiModel"
|
||||||
|
:default-api-timeout="defaultApiTimeout"
|
||||||
|
@update:api-config="updateApiConfig"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 主要内容 -->
|
||||||
|
<n-card class="analysis-container">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-title">股票批量分析</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<n-grid :cols="24" :x-gap="16" :y-gap="16">
|
||||||
|
<!-- 左侧配置区域 -->
|
||||||
|
<n-grid-item :span="24" :lg-span="8">
|
||||||
|
<div class="config-section">
|
||||||
|
<n-form-item label="选择市场类型">
|
||||||
|
<n-select
|
||||||
|
v-model:value="marketType"
|
||||||
|
:options="marketOptions"
|
||||||
|
@update:value="handleMarketTypeChange"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="股票搜索" v-if="marketType === 'US'">
|
||||||
|
<StockSearch :market-type="marketType" @select="addSelectedStock" />
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="输入代码">
|
||||||
|
<n-input
|
||||||
|
v-model:value="stockCodes"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="输入股票代码,多个代码用逗号、空格或换行分隔"
|
||||||
|
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<div class="action-buttons">
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
:loading="isAnalyzing"
|
||||||
|
:disabled="!stockCodes.trim()"
|
||||||
|
@click="analyzeStocks"
|
||||||
|
>
|
||||||
|
{{ isAnalyzing ? '分析中...' : '开始分析' }}
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-button
|
||||||
|
:disabled="analyzedStocks.length === 0"
|
||||||
|
@click="copyAnalysisResults"
|
||||||
|
>
|
||||||
|
复制结果
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-grid-item>
|
||||||
|
|
||||||
|
<!-- 右侧结果区域 -->
|
||||||
|
<n-grid-item :span="24" :lg-span="16">
|
||||||
|
<div class="results-section">
|
||||||
|
<template v-if="analyzedStocks.length === 0 && !isAnalyzing">
|
||||||
|
<n-empty description="尚未分析股票" size="large">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="DocumentTextIcon" />
|
||||||
|
</template>
|
||||||
|
</n-empty>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<n-grid :cols="1" :x-gap="16" :y-gap="16" :lg-cols="2">
|
||||||
|
<n-grid-item v-for="stock in analyzedStocks" :key="stock.code">
|
||||||
|
<StockCard :stock="stock" />
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
</n-card>
|
||||||
|
</n-layout-content>
|
||||||
|
</n-layout>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import {
|
||||||
|
NLayout,
|
||||||
|
NLayoutContent,
|
||||||
|
NCard,
|
||||||
|
NPageHeader,
|
||||||
|
NIcon,
|
||||||
|
NGrid,
|
||||||
|
NGridItem,
|
||||||
|
NFormItem,
|
||||||
|
NSelect,
|
||||||
|
NInput,
|
||||||
|
NButton,
|
||||||
|
NEmpty,
|
||||||
|
useMessage
|
||||||
|
} from 'naive-ui';
|
||||||
|
import { useClipboard } from '@vueuse/core'
|
||||||
|
import {
|
||||||
|
BarChart as BarChartIcon,
|
||||||
|
DocumentText as DocumentTextIcon
|
||||||
|
} from '@vicons/ionicons5';
|
||||||
|
|
||||||
|
import AnnouncementBanner from './AnnouncementBanner.vue';
|
||||||
|
import MarketTimeDisplay from './MarketTimeDisplay.vue';
|
||||||
|
import ApiConfigPanel from './ApiConfigPanel.vue';
|
||||||
|
import StockSearch from './StockSearch.vue';
|
||||||
|
import StockCard from './StockCard.vue';
|
||||||
|
|
||||||
|
import { apiService } from '@/services/api';
|
||||||
|
import type { StockInfo, ApiConfig, StreamInitMessage, StreamAnalysisUpdate } from '@/types';
|
||||||
|
import { loadApiConfig } from '@/utils';
|
||||||
|
|
||||||
|
// 使用Naive UI的消息组件
|
||||||
|
const message = useMessage();
|
||||||
|
const { copy } = useClipboard();
|
||||||
|
|
||||||
|
// 从环境变量获取的默认配置
|
||||||
|
const defaultApiUrl = ref('');
|
||||||
|
const defaultApiModel = ref('gpt-3.5-turbo');
|
||||||
|
const defaultApiTimeout = ref('60');
|
||||||
|
const announcement = ref('');
|
||||||
|
|
||||||
|
// 股票分析配置
|
||||||
|
const marketType = ref('A');
|
||||||
|
const stockCodes = ref('');
|
||||||
|
const isAnalyzing = ref(false);
|
||||||
|
const analyzedStocks = ref<StockInfo[]>([]);
|
||||||
|
|
||||||
|
// API配置
|
||||||
|
const apiConfig = ref<ApiConfig>({
|
||||||
|
apiUrl: '',
|
||||||
|
apiKey: '',
|
||||||
|
apiModel: 'gpt-3.5-turbo',
|
||||||
|
apiTimeout: '60',
|
||||||
|
saveApiConfig: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 市场选项
|
||||||
|
const marketOptions = [
|
||||||
|
{ label: 'A股', value: 'A' },
|
||||||
|
{ label: '港股', value: 'HK' },
|
||||||
|
{ label: '美股', value: 'US' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 更新API配置
|
||||||
|
function updateApiConfig(config: ApiConfig) {
|
||||||
|
apiConfig.value = { ...config };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理市场类型变更
|
||||||
|
function handleMarketTypeChange() {
|
||||||
|
stockCodes.value = '';
|
||||||
|
analyzedStocks.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加选择的股票
|
||||||
|
function addSelectedStock(symbol: string) {
|
||||||
|
if (stockCodes.value) {
|
||||||
|
stockCodes.value += ', ' + symbol;
|
||||||
|
} else {
|
||||||
|
stockCodes.value = symbol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理流式响应的数据
|
||||||
|
function processStreamData(text: string) {
|
||||||
|
try {
|
||||||
|
// 尝试解析为JSON
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
|
||||||
|
// 判断是初始消息还是更新消息
|
||||||
|
if (data.stream_type === 'single' || data.stream_type === 'batch') {
|
||||||
|
// 初始消息
|
||||||
|
handleStreamInit(data as StreamInitMessage);
|
||||||
|
} else if (data.stock_code) {
|
||||||
|
// 更新消息
|
||||||
|
handleStreamUpdate(data as StreamAnalysisUpdate);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析流数据出错:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理流式初始化消息
|
||||||
|
function handleStreamInit(data: StreamInitMessage) {
|
||||||
|
if (data.stream_type === 'single' && data.stock_code) {
|
||||||
|
// 单个股票分析
|
||||||
|
analyzedStocks.value = [{
|
||||||
|
code: data.stock_code,
|
||||||
|
name: '',
|
||||||
|
marketType: marketType.value,
|
||||||
|
analysisStatus: 'waiting'
|
||||||
|
}];
|
||||||
|
} else if (data.stream_type === 'batch' && data.stock_codes) {
|
||||||
|
// 批量分析
|
||||||
|
analyzedStocks.value = data.stock_codes.map(code => ({
|
||||||
|
code,
|
||||||
|
name: '',
|
||||||
|
marketType: marketType.value,
|
||||||
|
analysisStatus: 'waiting'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理流式更新消息
|
||||||
|
function handleStreamUpdate(data: StreamAnalysisUpdate) {
|
||||||
|
const stockIndex = analyzedStocks.value.findIndex(s => s.code === data.stock_code);
|
||||||
|
|
||||||
|
if (stockIndex >= 0) {
|
||||||
|
const stock = { ...analyzedStocks.value[stockIndex] };
|
||||||
|
|
||||||
|
// 更新分析状态
|
||||||
|
stock.analysisStatus = data.status;
|
||||||
|
|
||||||
|
// 如果有分析结果,则更新
|
||||||
|
if (data.analysis !== undefined) {
|
||||||
|
stock.analysis = data.analysis;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有错误,则更新
|
||||||
|
if (data.error !== undefined) {
|
||||||
|
stock.error = data.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新股票名称、价格等信息
|
||||||
|
if (data.name !== undefined) {
|
||||||
|
stock.name = data.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.price !== undefined) {
|
||||||
|
stock.price = data.price;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.change_percent !== undefined) {
|
||||||
|
stock.changePercent = data.change_percent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.market_value !== undefined) {
|
||||||
|
stock.marketValue = data.market_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新数组中的股票信息
|
||||||
|
analyzedStocks.value[stockIndex] = stock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析股票
|
||||||
|
async function analyzeStocks() {
|
||||||
|
if (!stockCodes.value.trim()) {
|
||||||
|
message.warning('请输入股票代码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAnalyzing.value = true;
|
||||||
|
analyzedStocks.value = [];
|
||||||
|
|
||||||
|
// 解析股票代码
|
||||||
|
const codes = stockCodes.value
|
||||||
|
.split(/[,\s\n]+/)
|
||||||
|
.map(code => code.trim())
|
||||||
|
.filter(code => code);
|
||||||
|
|
||||||
|
if (codes.length === 0) {
|
||||||
|
message.warning('未找到有效的股票代码');
|
||||||
|
isAnalyzing.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建请求参数
|
||||||
|
const requestData = {
|
||||||
|
stock_codes: codes,
|
||||||
|
market_type: marketType.value
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
// 添加自定义API配置
|
||||||
|
if (apiConfig.value.apiUrl) {
|
||||||
|
requestData.api_url = apiConfig.value.apiUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiConfig.value.apiKey) {
|
||||||
|
requestData.api_key = apiConfig.value.apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiConfig.value.apiModel) {
|
||||||
|
requestData.api_model = apiConfig.value.apiModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiConfig.value.apiTimeout) {
|
||||||
|
requestData.api_timeout = apiConfig.value.apiTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送分析请求
|
||||||
|
const response = await fetch('/analyze', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理流式响应
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (!reader) {
|
||||||
|
throw new Error('无法读取响应流');
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码并处理数据
|
||||||
|
const text = decoder.decode(value, { stream: true });
|
||||||
|
buffer += text;
|
||||||
|
|
||||||
|
// 按行处理数据
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop() || ''; // 最后一行可能不完整,保留到下一次
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim()) {
|
||||||
|
processStreamData(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理最后可能剩余的数据
|
||||||
|
if (buffer.trim()) {
|
||||||
|
processStreamData(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success('分析完成');
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(`分析出错: ${error.message || '未知错误'}`);
|
||||||
|
console.error('分析股票时出错:', error);
|
||||||
|
} finally {
|
||||||
|
isAnalyzing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制分析结果
|
||||||
|
async function copyAnalysisResults() {
|
||||||
|
if (analyzedStocks.value.length === 0) {
|
||||||
|
message.warning('没有可复制的分析结果');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 格式化分析结果
|
||||||
|
const formattedResults = analyzedStocks.value
|
||||||
|
.filter(stock => stock.analysisStatus === 'completed')
|
||||||
|
.map(stock => {
|
||||||
|
return `【${stock.code} ${stock.name || ''}】\n${stock.analysis || '无分析结果'}\n`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
if (!formattedResults) {
|
||||||
|
message.warning('没有已完成的分析结果可复制');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制到剪贴板
|
||||||
|
await copy(formattedResults);
|
||||||
|
message.success('已复制分析结果到剪贴板');
|
||||||
|
} catch (error) {
|
||||||
|
message.error('复制失败,请手动复制');
|
||||||
|
console.error('复制分析结果时出错:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从本地存储恢复API配置
|
||||||
|
function restoreLocalApiConfig() {
|
||||||
|
const savedConfig = loadApiConfig();
|
||||||
|
if (savedConfig && savedConfig.saveApiConfig) {
|
||||||
|
apiConfig.value = {
|
||||||
|
apiUrl: savedConfig.apiUrl || '',
|
||||||
|
apiKey: savedConfig.apiKey || '',
|
||||||
|
apiModel: savedConfig.apiModel || defaultApiModel.value,
|
||||||
|
apiTimeout: savedConfig.apiTimeout || defaultApiTimeout.value,
|
||||||
|
saveApiConfig: savedConfig.saveApiConfig
|
||||||
|
};
|
||||||
|
|
||||||
|
// 通知父组件配置已更新
|
||||||
|
updateApiConfig(apiConfig.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时获取默认配置和公告
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
// 从API获取配置信息
|
||||||
|
const config = await apiService.getConfig();
|
||||||
|
|
||||||
|
if (config.default_api_url) {
|
||||||
|
defaultApiUrl.value = config.default_api_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.default_api_model) {
|
||||||
|
defaultApiModel.value = config.default_api_model;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.default_api_timeout) {
|
||||||
|
defaultApiTimeout.value = config.default_api_timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.announcement) {
|
||||||
|
announcement.value = config.announcement;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化后恢复本地保存的配置
|
||||||
|
restoreLocalApiConfig();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取默认配置时出错:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-layout {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-container {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-section {
|
||||||
|
padding: 0.5rem;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
207
frontend/src/components/StockCard.vue
Normal file
207
frontend/src/components/StockCard.vue
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
<template>
|
||||||
|
<n-card class="stock-card" :bordered="false" :class="{ 'is-analyzing': isAnalyzing }">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="stock-info">
|
||||||
|
<div class="stock-code">{{ stock.code }}</div>
|
||||||
|
<div class="stock-name">{{ stock.name || '加载中...' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stock-price-info" v-if="stock.price !== undefined">
|
||||||
|
<div class="stock-price">{{ stock.price.toFixed(2) }}</div>
|
||||||
|
<div class="stock-change" :class="{
|
||||||
|
'up': stock.changePercent && stock.changePercent > 0,
|
||||||
|
'down': stock.changePercent && stock.changePercent < 0
|
||||||
|
}">
|
||||||
|
{{ formatChangePercent(stock.changePercent) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-divider />
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<template v-if="stock.analysisStatus === 'waiting'">
|
||||||
|
<div class="waiting-status">
|
||||||
|
<n-spin size="small" />
|
||||||
|
<span>等待分析...</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="stock.analysisStatus === 'analyzing'">
|
||||||
|
<div class="analyzing-status">
|
||||||
|
<n-spin size="small" />
|
||||||
|
<span>正在分析...</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="stock.analysisStatus === 'error'">
|
||||||
|
<div class="error-status">
|
||||||
|
<n-icon :component="AlertCircleIcon" class="error-icon" />
|
||||||
|
<span>分析出错: {{ stock.error || '未知错误' }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="stock.analysisStatus === 'completed'">
|
||||||
|
<div class="analysis-result" v-html="parsedAnalysis"></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="market-value" v-if="stock.marketValue">
|
||||||
|
市值: {{ formatMarketValue(stock.marketValue) }}
|
||||||
|
</div>
|
||||||
|
<div class="market-type">
|
||||||
|
{{ getMarketName(stock.marketType) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { NCard, NDivider, NSpin, NIcon } from 'naive-ui';
|
||||||
|
import { AlertCircleOutline as AlertCircleIcon } from '@vicons/ionicons5';
|
||||||
|
import { parseMarkdown, formatMarketValue as formatMarketValueFn } from '@/utils';
|
||||||
|
import type { StockInfo } from '@/types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
stock: StockInfo;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isAnalyzing = computed(() => {
|
||||||
|
return props.stock.analysisStatus === 'analyzing';
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsedAnalysis = computed(() => {
|
||||||
|
if (props.stock.analysis) {
|
||||||
|
return parseMarkdown(props.stock.analysis);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatChangePercent(percent: number | undefined): string {
|
||||||
|
if (percent === undefined) return '--';
|
||||||
|
|
||||||
|
const sign = percent > 0 ? '+' : '';
|
||||||
|
return `${sign}${percent.toFixed(2)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMarketValue(value: number): string {
|
||||||
|
return formatMarketValueFn(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMarketName(marketType: string): string {
|
||||||
|
const marketMap: Record<string, string> = {
|
||||||
|
'A': 'A股',
|
||||||
|
'HK': '港股',
|
||||||
|
'US': '美股'
|
||||||
|
};
|
||||||
|
|
||||||
|
return marketMap[marketType] || marketType;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stock-card {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-card.is-analyzing {
|
||||||
|
border-left: 3px solid var(--n-info-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-code {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--n-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-name {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--n-text-color-3);
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-price-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-price {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--n-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-change {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.up {
|
||||||
|
color: var(--n-error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.down {
|
||||||
|
color: var(--n-success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 100px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting-status,
|
||||||
|
.analyzing-status,
|
||||||
|
.error-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: var(--n-text-color-3);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
color: var(--n-error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-result {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-result :deep(p) {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-result :deep(ul) {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--n-text-color-3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
220
frontend/src/components/StockSearch.vue
Normal file
220
frontend/src/components/StockSearch.vue
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
<template>
|
||||||
|
<div class="stock-search-container">
|
||||||
|
<n-input
|
||||||
|
v-model:value="searchKeyword"
|
||||||
|
placeholder="输入股票代码或名称搜索"
|
||||||
|
@input="handleSearchInput"
|
||||||
|
@blur="handleBlur"
|
||||||
|
@focus="handleFocus"
|
||||||
|
ref="searchInputRef"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<n-icon :component="SearchIcon" />
|
||||||
|
</template>
|
||||||
|
</n-input>
|
||||||
|
|
||||||
|
<div class="search-results" v-show="showResults">
|
||||||
|
<div v-if="loading" class="loading-results">
|
||||||
|
<n-spin size="small" />
|
||||||
|
<span>搜索中...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="results.length === 0 && searchKeyword" class="no-results">
|
||||||
|
未找到相关股票
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<n-scrollbar style="max-height: 300px;">
|
||||||
|
<div
|
||||||
|
v-for="item in results"
|
||||||
|
:key="item.symbol"
|
||||||
|
class="search-result-item"
|
||||||
|
@click="selectStock(item)"
|
||||||
|
>
|
||||||
|
<div class="result-symbol-name">
|
||||||
|
<span class="result-symbol">{{ item.symbol }}</span>
|
||||||
|
<span class="result-name">{{ item.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="result-meta">
|
||||||
|
<span class="result-market">{{ item.market }}</span>
|
||||||
|
<span v-if="item.marketValue" class="result-market-value">
|
||||||
|
市值: {{ formatMarketValue(item.marketValue) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-scrollbar>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||||
|
import { NInput, NIcon, NSpin, NScrollbar } from 'naive-ui';
|
||||||
|
import { Search as SearchIcon } from '@vicons/ionicons5';
|
||||||
|
import { apiService } from '@/services/api';
|
||||||
|
import { debounce, formatMarketValue as formatMarketValueFn } from '@/utils';
|
||||||
|
import type { SearchResult } from '@/types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
marketType: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'select', symbol: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const searchKeyword = ref('');
|
||||||
|
const results = ref<SearchResult[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const showResults = ref(false);
|
||||||
|
const searchInputRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
// 创建防抖搜索函数
|
||||||
|
const debouncedSearch = debounce(async (keyword: string) => {
|
||||||
|
if (!keyword) {
|
||||||
|
results.value = [];
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (props.marketType === 'US') {
|
||||||
|
// 美股搜索
|
||||||
|
results.value = await apiService.searchUsStocks(keyword);
|
||||||
|
} else {
|
||||||
|
// 其他市场搜索 (后端需要实现对应的接口)
|
||||||
|
results.value = [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('搜索股票时出错:', error);
|
||||||
|
results.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
function handleSearchInput() {
|
||||||
|
showResults.value = true;
|
||||||
|
debouncedSearch(searchKeyword.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectStock(item: SearchResult) {
|
||||||
|
emit('select', item.symbol);
|
||||||
|
searchKeyword.value = '';
|
||||||
|
showResults.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBlur() {
|
||||||
|
// 延迟隐藏,以便可以点击结果项
|
||||||
|
setTimeout(() => {
|
||||||
|
showResults.value = false;
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFocus() {
|
||||||
|
if (searchKeyword.value) {
|
||||||
|
showResults.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMarketValue(value: number): string {
|
||||||
|
return formatMarketValueFn(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击外部时隐藏搜索结果
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (
|
||||||
|
searchInputRef.value &&
|
||||||
|
!searchInputRef.value.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
showResults.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', handleClickOutside);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('click', handleClickOutside);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stock-search-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 10;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
background-color: var(--n-color);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
border: 1px solid var(--n-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-results,
|
||||||
|
.no-results {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
color: var(--n-text-color-3);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-item {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-item:hover {
|
||||||
|
background-color: var(--n-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-symbol-name {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-symbol {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--n-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-name {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--n-text-color-3);
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-market,
|
||||||
|
.result-market-value {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--n-text-color-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-market-value {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
6
frontend/src/main.ts
Normal file
6
frontend/src/main.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import './style.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.mount('#app')
|
||||||
59
frontend/src/services/api.ts
Normal file
59
frontend/src/services/api.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import type { AnalyzeRequest, TestApiRequest, TestApiResponse, SearchResult } from '@/types';
|
||||||
|
|
||||||
|
// 在开发环境中前缀为空,因为已经在vite.config.ts中配置了代理
|
||||||
|
const API_PREFIX = '';
|
||||||
|
|
||||||
|
export const apiService = {
|
||||||
|
// 分析股票
|
||||||
|
analyzeStocks: async (request: AnalyzeRequest) => {
|
||||||
|
return axios.post(`${API_PREFIX}/analyze`, request, {
|
||||||
|
responseType: 'stream'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 测试API连接
|
||||||
|
testApiConnection: async (request: TestApiRequest): Promise<TestApiResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${API_PREFIX}/test_api_connection`, request);
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response) {
|
||||||
|
return error.response.data;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error.message || '连接失败'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 搜索美股
|
||||||
|
searchUsStocks: async (keyword: string): Promise<SearchResult[]> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_PREFIX}/search_us_stocks`, {
|
||||||
|
params: { keyword }
|
||||||
|
});
|
||||||
|
return response.data.results || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('搜索美股时出错:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取配置
|
||||||
|
getConfig: async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_PREFIX}/config`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取配置时出错:', error);
|
||||||
|
return {
|
||||||
|
announcement: '',
|
||||||
|
default_api_url: '',
|
||||||
|
default_api_model: 'gpt-3.5-turbo',
|
||||||
|
default_api_timeout: '60'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
79
frontend/src/style.css
Normal file
79
frontend/src/style.css
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
:root {
|
||||||
|
font-family: 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
80
frontend/src/types/index.ts
Normal file
80
frontend/src/types/index.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// API接口相关类型
|
||||||
|
export interface ApiConfig {
|
||||||
|
apiUrl: string;
|
||||||
|
apiKey: string;
|
||||||
|
apiModel: string;
|
||||||
|
apiTimeout: string;
|
||||||
|
saveApiConfig: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StockInfo {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
marketType: string;
|
||||||
|
price?: number;
|
||||||
|
changePercent?: number;
|
||||||
|
marketValue?: number;
|
||||||
|
analysis?: string;
|
||||||
|
analysisStatus: 'waiting' | 'analyzing' | 'completed' | 'error';
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchResult {
|
||||||
|
symbol: string;
|
||||||
|
name: string;
|
||||||
|
market: string;
|
||||||
|
marketValue?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketStatus {
|
||||||
|
isOpen: boolean;
|
||||||
|
nextTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketTimeInfo {
|
||||||
|
currentTime: string;
|
||||||
|
cnMarket: MarketStatus;
|
||||||
|
hkMarket: MarketStatus;
|
||||||
|
usMarket: MarketStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析请求和响应
|
||||||
|
export interface AnalyzeRequest {
|
||||||
|
stock_codes: string[];
|
||||||
|
market_type: string;
|
||||||
|
api_url?: string;
|
||||||
|
api_key?: string;
|
||||||
|
api_model?: string;
|
||||||
|
api_timeout?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestApiRequest {
|
||||||
|
api_url: string;
|
||||||
|
api_key: string;
|
||||||
|
api_model: string;
|
||||||
|
api_timeout: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestApiResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
status_code?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 流式响应类型
|
||||||
|
export interface StreamInitMessage {
|
||||||
|
stream_type: 'single' | 'batch';
|
||||||
|
stock_code?: string;
|
||||||
|
stock_codes?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StreamAnalysisUpdate {
|
||||||
|
stock_code: string;
|
||||||
|
analysis?: string;
|
||||||
|
status: 'analyzing' | 'completed' | 'error';
|
||||||
|
error?: string;
|
||||||
|
name?: string;
|
||||||
|
price?: number;
|
||||||
|
change_percent?: number;
|
||||||
|
market_value?: number;
|
||||||
|
}
|
||||||
201
frontend/src/utils/index.ts
Normal file
201
frontend/src/utils/index.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import type { MarketTimeInfo } from '@/types';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
// 防抖函数
|
||||||
|
export function debounce<T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
wait: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
let timeout: number | null = null;
|
||||||
|
|
||||||
|
return function(...args: Parameters<T>): void {
|
||||||
|
const later = () => {
|
||||||
|
timeout = null;
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (timeout !== null) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
timeout = window.setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化市值
|
||||||
|
export function formatMarketValue(value: number): string {
|
||||||
|
if (!value) return '未知';
|
||||||
|
|
||||||
|
if (value >= 1000000000000) {
|
||||||
|
return (value / 1000000000000).toFixed(2) + '万亿';
|
||||||
|
} else if (value >= 100000000) {
|
||||||
|
return (value / 100000000).toFixed(2) + '亿';
|
||||||
|
} else if (value >= 10000) {
|
||||||
|
return (value / 10000).toFixed(2) + '万';
|
||||||
|
} else {
|
||||||
|
return value.toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析Markdown
|
||||||
|
export function parseMarkdown(text: string): string {
|
||||||
|
try {
|
||||||
|
const result = marked(text);
|
||||||
|
if (typeof result === 'string') {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析Markdown出错:', e);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新市场时间信息
|
||||||
|
export function updateMarketTimeInfo(): MarketTimeInfo {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// 当前时间
|
||||||
|
const currentTime = now.toLocaleTimeString('zh-CN', { hour12: false });
|
||||||
|
|
||||||
|
// 中国时间
|
||||||
|
const cnOptions = { timeZone: 'Asia/Shanghai', hour12: false } as Intl.DateTimeFormatOptions;
|
||||||
|
const cnTime = now.toLocaleString('zh-CN', cnOptions);
|
||||||
|
const cnHour = new Date(cnTime).getHours();
|
||||||
|
const cnMinute = new Date(cnTime).getMinutes();
|
||||||
|
|
||||||
|
// A股市场状态
|
||||||
|
const cnMarketOpen = (cnHour === 9 && cnMinute >= 30) || (cnHour === 10) ||
|
||||||
|
(cnHour === 11 && cnMinute <= 30) ||
|
||||||
|
(cnHour >= 13 && cnHour < 15);
|
||||||
|
|
||||||
|
const cnNextTime = getNextTimeText(cnMarketOpen, cnHour, cnMinute, 9, 30, 15, 0);
|
||||||
|
|
||||||
|
// 港股市场状态(与A股相同时区)
|
||||||
|
const hkMarketOpen = (cnHour === 9 && cnMinute >= 30) ||
|
||||||
|
(cnHour === 10) || (cnHour === 11) ||
|
||||||
|
(cnHour >= 13 && cnHour < 16);
|
||||||
|
|
||||||
|
const hkNextTime = getNextTimeText(hkMarketOpen, cnHour, cnMinute, 9, 30, 16, 0);
|
||||||
|
|
||||||
|
// 获取美国东部时间
|
||||||
|
const usOptions = { timeZone: 'America/New_York', hour12: false } as Intl.DateTimeFormatOptions;
|
||||||
|
const usTime = now.toLocaleString('zh-CN', usOptions);
|
||||||
|
const usHour = new Date(usTime).getHours();
|
||||||
|
const usMinute = new Date(usTime).getMinutes();
|
||||||
|
|
||||||
|
// 美股市场状态
|
||||||
|
const usMarketOpen = (usHour >= 9 && usHour < 16) ||
|
||||||
|
(usHour === 16 && usMinute === 0);
|
||||||
|
|
||||||
|
const usNextTime = getNextTimeText(usMarketOpen, usHour, usMinute, 9, 30, 16, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentTime,
|
||||||
|
cnMarket: { isOpen: cnMarketOpen, nextTime: cnNextTime },
|
||||||
|
hkMarket: { isOpen: hkMarketOpen, nextTime: hkNextTime },
|
||||||
|
usMarket: { isOpen: usMarketOpen, nextTime: usNextTime }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:获取距离下一次开/闭市的时间文本
|
||||||
|
function getNextTimeText(
|
||||||
|
isOpen: boolean,
|
||||||
|
currentHour: number,
|
||||||
|
currentMinute: number,
|
||||||
|
openHour: number,
|
||||||
|
openMinute: number,
|
||||||
|
closeHour: number,
|
||||||
|
closeMinute: number
|
||||||
|
): string {
|
||||||
|
if (isOpen) {
|
||||||
|
// 计算距离收盘时间
|
||||||
|
let timeToCloseMinutes = (closeHour - currentHour) * 60 + (closeMinute - currentMinute);
|
||||||
|
|
||||||
|
if (timeToCloseMinutes <= 0) {
|
||||||
|
return '即将收盘';
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = Math.floor(timeToCloseMinutes / 60);
|
||||||
|
const minutes = timeToCloseMinutes % 60;
|
||||||
|
|
||||||
|
return `距离收盘还有 ${hours}小时${minutes}分钟`;
|
||||||
|
} else {
|
||||||
|
// 计算距离开盘时间
|
||||||
|
let nextOpenHour = openHour;
|
||||||
|
let nextOpenMinute = openMinute;
|
||||||
|
let isNextDay = false;
|
||||||
|
|
||||||
|
if (currentHour >= closeHour) {
|
||||||
|
// 已经过了今天的收盘时间,下一个开盘是明天
|
||||||
|
isNextDay = true;
|
||||||
|
} else if (currentHour < openHour || (currentHour === openHour && currentMinute < openMinute)) {
|
||||||
|
// 还没到今天的开盘时间
|
||||||
|
isNextDay = false;
|
||||||
|
} else {
|
||||||
|
// 当前处于盘中休息时间,下一个开盘时间是当天下午
|
||||||
|
nextOpenHour = 13;
|
||||||
|
nextOpenMinute = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeToOpenMinutes;
|
||||||
|
|
||||||
|
if (isNextDay) {
|
||||||
|
timeToOpenMinutes = (24 - currentHour + nextOpenHour) * 60 + (nextOpenMinute - currentMinute);
|
||||||
|
} else {
|
||||||
|
timeToOpenMinutes = (nextOpenHour - currentHour) * 60 + (nextOpenMinute - currentMinute);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeToOpenMinutes <= 0) {
|
||||||
|
return '即将开盘';
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = Math.floor(timeToOpenMinutes / 60);
|
||||||
|
const minutes = timeToOpenMinutes % 60;
|
||||||
|
|
||||||
|
return `距离开盘还有 ${hours}小时${minutes}分钟`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存API配置到localStorage
|
||||||
|
export function saveApiConfigToLocalStorage(config: Partial<Pick<
|
||||||
|
{ apiUrl: string, apiKey: string, apiModel: string, apiTimeout: string, saveApiConfig: boolean },
|
||||||
|
'apiUrl' | 'apiKey' | 'apiModel' | 'apiTimeout' | 'saveApiConfig'
|
||||||
|
>>): void {
|
||||||
|
if (window.localStorage) {
|
||||||
|
localStorage.setItem('apiConfig', JSON.stringify(config));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从localStorage加载API配置
|
||||||
|
export function loadApiConfig(): Partial<{
|
||||||
|
apiUrl: string,
|
||||||
|
apiKey: string,
|
||||||
|
apiModel: string,
|
||||||
|
apiTimeout: string,
|
||||||
|
saveApiConfig: boolean
|
||||||
|
}> {
|
||||||
|
if (window.localStorage) {
|
||||||
|
const saved = localStorage.getItem('apiConfig');
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(saved);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析保存的API配置出错:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
apiUrl: '',
|
||||||
|
apiKey: '',
|
||||||
|
apiModel: '',
|
||||||
|
apiTimeout: '',
|
||||||
|
saveApiConfig: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除API配置
|
||||||
|
export function clearApiConfig(): void {
|
||||||
|
if (window.localStorage) {
|
||||||
|
localStorage.removeItem('apiConfig');
|
||||||
|
}
|
||||||
|
}
|
||||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
18
frontend/tsconfig.app.json
Normal file
18
frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"incremental": true, // 启用增量编译
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
||||||
7
frontend/tsconfig.json
Normal file
7
frontend/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
25
frontend/tsconfig.node.json
Normal file
25
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"incremental": true, // 启用增量编译
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
43
frontend/vite.config.ts
Normal file
43
frontend/vite.config.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
import { dirname, resolve } from 'path'
|
||||||
|
|
||||||
|
// 获取当前文件的目录路径(在ESM中替代__dirname)
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = dirname(__filename)
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, 'src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://127.0.0.1:8888',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
|
},
|
||||||
|
'/analyze': {
|
||||||
|
target: 'http://127.0.0.1:8888',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
'/test_api_connection': {
|
||||||
|
target: 'http://127.0.0.1:8888',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
'/search_us_stocks': {
|
||||||
|
target: 'http://127.0.0.1:8888',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
'/config': {
|
||||||
|
target: 'http://127.0.0.1:8888',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
731
frontend/yarn.lock
Normal file
731
frontend/yarn.lock
Normal file
@@ -0,0 +1,731 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@babel/helper-string-parser@^7.25.9":
|
||||||
|
version "7.25.9"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz"
|
||||||
|
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier@^7.25.9":
|
||||||
|
version "7.25.9"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz"
|
||||||
|
integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
|
||||||
|
|
||||||
|
"@babel/parser@^7.25.3":
|
||||||
|
version "7.26.9"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.26.9.tgz"
|
||||||
|
integrity sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==
|
||||||
|
dependencies:
|
||||||
|
"@babel/types" "^7.26.9"
|
||||||
|
|
||||||
|
"@babel/types@^7.26.9":
|
||||||
|
version "7.26.9"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/types/-/types-7.26.9.tgz"
|
||||||
|
integrity sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-string-parser" "^7.25.9"
|
||||||
|
"@babel/helper-validator-identifier" "^7.25.9"
|
||||||
|
|
||||||
|
"@css-render/plugin-bem@^0.15.14":
|
||||||
|
version "0.15.14"
|
||||||
|
resolved "https://registry.npmmirror.com/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz"
|
||||||
|
integrity sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==
|
||||||
|
|
||||||
|
"@css-render/vue3-ssr@^0.15.10", "@css-render/vue3-ssr@^0.15.14":
|
||||||
|
version "0.15.14"
|
||||||
|
resolved "https://registry.npmmirror.com/@css-render/vue3-ssr/-/vue3-ssr-0.15.14.tgz"
|
||||||
|
integrity sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==
|
||||||
|
|
||||||
|
"@emotion/hash@~0.8.0":
|
||||||
|
version "0.8.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz"
|
||||||
|
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
|
||||||
|
|
||||||
|
"@esbuild/win32-x64@0.25.0":
|
||||||
|
version "0.25.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz"
|
||||||
|
integrity sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec@^1.5.0":
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz"
|
||||||
|
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
|
||||||
|
|
||||||
|
"@juggle/resize-observer@^3.3.1":
|
||||||
|
version "3.4.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz"
|
||||||
|
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc@4.34.9":
|
||||||
|
version "4.34.9"
|
||||||
|
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz"
|
||||||
|
integrity sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==
|
||||||
|
|
||||||
|
"@types/estree@1.0.6":
|
||||||
|
version "1.0.6"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.6.tgz"
|
||||||
|
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
|
||||||
|
|
||||||
|
"@types/katex@^0.16.2":
|
||||||
|
version "0.16.7"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/katex/-/katex-0.16.7.tgz"
|
||||||
|
integrity sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==
|
||||||
|
|
||||||
|
"@types/lodash-es@^4.17.9":
|
||||||
|
version "4.17.12"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz"
|
||||||
|
integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/lodash" "*"
|
||||||
|
|
||||||
|
"@types/lodash@*", "@types/lodash@^4.14.198":
|
||||||
|
version "4.17.16"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.16.tgz"
|
||||||
|
integrity sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==
|
||||||
|
|
||||||
|
"@types/marked@^5.0.2":
|
||||||
|
version "5.0.2"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/marked/-/marked-5.0.2.tgz"
|
||||||
|
integrity sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==
|
||||||
|
|
||||||
|
"@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^22.13.9":
|
||||||
|
version "22.13.9"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/node/-/node-22.13.9.tgz"
|
||||||
|
integrity sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==
|
||||||
|
dependencies:
|
||||||
|
undici-types "~6.20.0"
|
||||||
|
|
||||||
|
"@types/web-bluetooth@^0.0.20":
|
||||||
|
version "0.0.20"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz"
|
||||||
|
integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==
|
||||||
|
|
||||||
|
"@vicons/ionicons5@^0.13.0":
|
||||||
|
version "0.13.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@vicons/ionicons5/-/ionicons5-0.13.0.tgz"
|
||||||
|
integrity sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ==
|
||||||
|
|
||||||
|
"@vitejs/plugin-vue@^5.2.1":
|
||||||
|
version "5.2.1"
|
||||||
|
resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz"
|
||||||
|
integrity sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==
|
||||||
|
|
||||||
|
"@volar/language-core@~2.4.11", "@volar/language-core@2.4.11":
|
||||||
|
version "2.4.11"
|
||||||
|
resolved "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.11.tgz"
|
||||||
|
integrity sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg==
|
||||||
|
dependencies:
|
||||||
|
"@volar/source-map" "2.4.11"
|
||||||
|
|
||||||
|
"@volar/source-map@2.4.11":
|
||||||
|
version "2.4.11"
|
||||||
|
resolved "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.11.tgz"
|
||||||
|
integrity sha512-ZQpmafIGvaZMn/8iuvCFGrW3smeqkq/IIh9F1SdSx9aUl0J4Iurzd6/FhmjNO5g2ejF3rT45dKskgXWiofqlZQ==
|
||||||
|
|
||||||
|
"@volar/typescript@~2.4.11":
|
||||||
|
version "2.4.11"
|
||||||
|
resolved "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.11.tgz"
|
||||||
|
integrity sha512-2DT+Tdh88Spp5PyPbqhyoYavYCPDsqbHLFwcUI9K1NlY1YgUJvujGdrqUp0zWxnW7KWNTr3xSpMuv2WnaTKDAw==
|
||||||
|
dependencies:
|
||||||
|
"@volar/language-core" "2.4.11"
|
||||||
|
path-browserify "^1.0.1"
|
||||||
|
vscode-uri "^3.0.8"
|
||||||
|
|
||||||
|
"@vue/compiler-core@3.5.13":
|
||||||
|
version "3.5.13"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.13.tgz"
|
||||||
|
integrity sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==
|
||||||
|
dependencies:
|
||||||
|
"@babel/parser" "^7.25.3"
|
||||||
|
"@vue/shared" "3.5.13"
|
||||||
|
entities "^4.5.0"
|
||||||
|
estree-walker "^2.0.2"
|
||||||
|
source-map-js "^1.2.0"
|
||||||
|
|
||||||
|
"@vue/compiler-dom@^3.5.0", "@vue/compiler-dom@3.5.13":
|
||||||
|
version "3.5.13"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz"
|
||||||
|
integrity sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==
|
||||||
|
dependencies:
|
||||||
|
"@vue/compiler-core" "3.5.13"
|
||||||
|
"@vue/shared" "3.5.13"
|
||||||
|
|
||||||
|
"@vue/compiler-sfc@3.5.13":
|
||||||
|
version "3.5.13"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz"
|
||||||
|
integrity sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/parser" "^7.25.3"
|
||||||
|
"@vue/compiler-core" "3.5.13"
|
||||||
|
"@vue/compiler-dom" "3.5.13"
|
||||||
|
"@vue/compiler-ssr" "3.5.13"
|
||||||
|
"@vue/shared" "3.5.13"
|
||||||
|
estree-walker "^2.0.2"
|
||||||
|
magic-string "^0.30.11"
|
||||||
|
postcss "^8.4.48"
|
||||||
|
source-map-js "^1.2.0"
|
||||||
|
|
||||||
|
"@vue/compiler-ssr@3.5.13":
|
||||||
|
version "3.5.13"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz"
|
||||||
|
integrity sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==
|
||||||
|
dependencies:
|
||||||
|
"@vue/compiler-dom" "3.5.13"
|
||||||
|
"@vue/shared" "3.5.13"
|
||||||
|
|
||||||
|
"@vue/compiler-vue2@^2.7.16":
|
||||||
|
version "2.7.16"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz"
|
||||||
|
integrity sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==
|
||||||
|
dependencies:
|
||||||
|
de-indent "^1.0.2"
|
||||||
|
he "^1.2.0"
|
||||||
|
|
||||||
|
"@vue/language-core@2.2.8":
|
||||||
|
version "2.2.8"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.8.tgz"
|
||||||
|
integrity sha512-rrzB0wPGBvcwaSNRriVWdNAbHQWSf0NlGqgKHK5mEkXpefjUlVRP62u03KvwZpvKVjRnBIQ/Lwre+Mx9N6juUQ==
|
||||||
|
dependencies:
|
||||||
|
"@volar/language-core" "~2.4.11"
|
||||||
|
"@vue/compiler-dom" "^3.5.0"
|
||||||
|
"@vue/compiler-vue2" "^2.7.16"
|
||||||
|
"@vue/shared" "^3.5.0"
|
||||||
|
alien-signals "^1.0.3"
|
||||||
|
minimatch "^9.0.3"
|
||||||
|
muggle-string "^0.4.1"
|
||||||
|
path-browserify "^1.0.1"
|
||||||
|
|
||||||
|
"@vue/reactivity@3.5.13":
|
||||||
|
version "3.5.13"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.13.tgz"
|
||||||
|
integrity sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==
|
||||||
|
dependencies:
|
||||||
|
"@vue/shared" "3.5.13"
|
||||||
|
|
||||||
|
"@vue/runtime-core@3.5.13":
|
||||||
|
version "3.5.13"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.13.tgz"
|
||||||
|
integrity sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==
|
||||||
|
dependencies:
|
||||||
|
"@vue/reactivity" "3.5.13"
|
||||||
|
"@vue/shared" "3.5.13"
|
||||||
|
|
||||||
|
"@vue/runtime-dom@3.5.13":
|
||||||
|
version "3.5.13"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz"
|
||||||
|
integrity sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==
|
||||||
|
dependencies:
|
||||||
|
"@vue/reactivity" "3.5.13"
|
||||||
|
"@vue/runtime-core" "3.5.13"
|
||||||
|
"@vue/shared" "3.5.13"
|
||||||
|
csstype "^3.1.3"
|
||||||
|
|
||||||
|
"@vue/server-renderer@3.5.13":
|
||||||
|
version "3.5.13"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.13.tgz"
|
||||||
|
integrity sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==
|
||||||
|
dependencies:
|
||||||
|
"@vue/compiler-ssr" "3.5.13"
|
||||||
|
"@vue/shared" "3.5.13"
|
||||||
|
|
||||||
|
"@vue/shared@^3.5.0", "@vue/shared@3.5.13":
|
||||||
|
version "3.5.13"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.13.tgz"
|
||||||
|
integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==
|
||||||
|
|
||||||
|
"@vue/tsconfig@^0.7.0":
|
||||||
|
version "0.7.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.7.0.tgz"
|
||||||
|
integrity sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==
|
||||||
|
|
||||||
|
"@vueuse/core@^12.7.0":
|
||||||
|
version "12.7.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@vueuse/core/-/core-12.7.0.tgz"
|
||||||
|
integrity sha512-jtK5B7YjZXmkGNHjviyGO4s3ZtEhbzSgrbX+s5o+Lr8i2nYqNyHuPVOeTdM1/hZ5Tkxg/KktAuAVDDiHMraMVA==
|
||||||
|
dependencies:
|
||||||
|
"@types/web-bluetooth" "^0.0.20"
|
||||||
|
"@vueuse/metadata" "12.7.0"
|
||||||
|
"@vueuse/shared" "12.7.0"
|
||||||
|
vue "^3.5.13"
|
||||||
|
|
||||||
|
"@vueuse/metadata@12.7.0":
|
||||||
|
version "12.7.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-12.7.0.tgz"
|
||||||
|
integrity sha512-4VvTH9mrjXqFN5LYa5YfqHVRI6j7R00Vy4995Rw7PQxyCL3z0Lli86iN4UemWqixxEvYfRjG+hF9wL8oLOn+3g==
|
||||||
|
|
||||||
|
"@vueuse/shared@12.7.0":
|
||||||
|
version "12.7.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-12.7.0.tgz"
|
||||||
|
integrity sha512-coLlUw2HHKsm7rPN6WqHJQr18WymN4wkA/3ThFaJ4v4gWGWAQQGK+MJxLuJTBs4mojQiazlVWAKNJNpUWGRkNw==
|
||||||
|
dependencies:
|
||||||
|
vue "^3.5.13"
|
||||||
|
|
||||||
|
alien-signals@^1.0.3:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.4.tgz"
|
||||||
|
integrity sha512-DJqqQD3XcsaQcQ1s+iE2jDUZmmQpXwHiR6fCAim/w87luaW+vmLY8fMlrdkmRwzaFXhkxf3rqPCR59tKVv1MDw==
|
||||||
|
|
||||||
|
async-validator@^4.2.5:
|
||||||
|
version "4.2.5"
|
||||||
|
resolved "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz"
|
||||||
|
integrity sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==
|
||||||
|
|
||||||
|
asynckit@^0.4.0:
|
||||||
|
version "0.4.0"
|
||||||
|
resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz"
|
||||||
|
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||||
|
|
||||||
|
axios@^1.8.1:
|
||||||
|
version "1.8.1"
|
||||||
|
resolved "https://registry.npmmirror.com/axios/-/axios-1.8.1.tgz"
|
||||||
|
integrity sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==
|
||||||
|
dependencies:
|
||||||
|
follow-redirects "^1.15.6"
|
||||||
|
form-data "^4.0.0"
|
||||||
|
proxy-from-env "^1.1.0"
|
||||||
|
|
||||||
|
balanced-match@^1.0.0:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz"
|
||||||
|
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||||
|
|
||||||
|
brace-expansion@^2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz"
|
||||||
|
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
|
||||||
|
dependencies:
|
||||||
|
balanced-match "^1.0.0"
|
||||||
|
|
||||||
|
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz"
|
||||||
|
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
|
||||||
|
dependencies:
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
function-bind "^1.1.2"
|
||||||
|
|
||||||
|
combined-stream@^1.0.8:
|
||||||
|
version "1.0.8"
|
||||||
|
resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz"
|
||||||
|
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||||
|
dependencies:
|
||||||
|
delayed-stream "~1.0.0"
|
||||||
|
|
||||||
|
css-render@^0.15.10, css-render@^0.15.14, css-render@~0.15.14:
|
||||||
|
version "0.15.14"
|
||||||
|
resolved "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz"
|
||||||
|
integrity sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==
|
||||||
|
dependencies:
|
||||||
|
"@emotion/hash" "~0.8.0"
|
||||||
|
csstype "~3.0.5"
|
||||||
|
|
||||||
|
csstype@^3.1.3:
|
||||||
|
version "3.1.3"
|
||||||
|
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz"
|
||||||
|
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
||||||
|
|
||||||
|
csstype@~3.0.5:
|
||||||
|
version "3.0.11"
|
||||||
|
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.0.11.tgz"
|
||||||
|
integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==
|
||||||
|
|
||||||
|
date-fns-tz@^3.1.3:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-3.2.0.tgz"
|
||||||
|
integrity sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==
|
||||||
|
|
||||||
|
"date-fns@^3.0.0 || ^4.0.0", date-fns@^3.6.0:
|
||||||
|
version "3.6.0"
|
||||||
|
resolved "https://registry.npmmirror.com/date-fns/-/date-fns-3.6.0.tgz"
|
||||||
|
integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==
|
||||||
|
|
||||||
|
de-indent@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz"
|
||||||
|
integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
|
||||||
|
|
||||||
|
delayed-stream@~1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz"
|
||||||
|
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||||
|
|
||||||
|
dunder-proto@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz"
|
||||||
|
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers "^1.0.1"
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
gopd "^1.2.0"
|
||||||
|
|
||||||
|
entities@^4.5.0:
|
||||||
|
version "4.5.0"
|
||||||
|
resolved "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz"
|
||||||
|
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||||
|
|
||||||
|
es-define-property@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz"
|
||||||
|
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
|
||||||
|
|
||||||
|
es-errors@^1.3.0:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz"
|
||||||
|
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||||
|
|
||||||
|
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz"
|
||||||
|
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
|
||||||
|
dependencies:
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
|
||||||
|
es-set-tostringtag@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz"
|
||||||
|
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
|
||||||
|
dependencies:
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
get-intrinsic "^1.2.6"
|
||||||
|
has-tostringtag "^1.0.2"
|
||||||
|
hasown "^2.0.2"
|
||||||
|
|
||||||
|
esbuild@^0.25.0:
|
||||||
|
version "0.25.0"
|
||||||
|
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.0.tgz"
|
||||||
|
integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==
|
||||||
|
optionalDependencies:
|
||||||
|
"@esbuild/aix-ppc64" "0.25.0"
|
||||||
|
"@esbuild/android-arm" "0.25.0"
|
||||||
|
"@esbuild/android-arm64" "0.25.0"
|
||||||
|
"@esbuild/android-x64" "0.25.0"
|
||||||
|
"@esbuild/darwin-arm64" "0.25.0"
|
||||||
|
"@esbuild/darwin-x64" "0.25.0"
|
||||||
|
"@esbuild/freebsd-arm64" "0.25.0"
|
||||||
|
"@esbuild/freebsd-x64" "0.25.0"
|
||||||
|
"@esbuild/linux-arm" "0.25.0"
|
||||||
|
"@esbuild/linux-arm64" "0.25.0"
|
||||||
|
"@esbuild/linux-ia32" "0.25.0"
|
||||||
|
"@esbuild/linux-loong64" "0.25.0"
|
||||||
|
"@esbuild/linux-mips64el" "0.25.0"
|
||||||
|
"@esbuild/linux-ppc64" "0.25.0"
|
||||||
|
"@esbuild/linux-riscv64" "0.25.0"
|
||||||
|
"@esbuild/linux-s390x" "0.25.0"
|
||||||
|
"@esbuild/linux-x64" "0.25.0"
|
||||||
|
"@esbuild/netbsd-arm64" "0.25.0"
|
||||||
|
"@esbuild/netbsd-x64" "0.25.0"
|
||||||
|
"@esbuild/openbsd-arm64" "0.25.0"
|
||||||
|
"@esbuild/openbsd-x64" "0.25.0"
|
||||||
|
"@esbuild/sunos-x64" "0.25.0"
|
||||||
|
"@esbuild/win32-arm64" "0.25.0"
|
||||||
|
"@esbuild/win32-ia32" "0.25.0"
|
||||||
|
"@esbuild/win32-x64" "0.25.0"
|
||||||
|
|
||||||
|
estree-walker@^2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz"
|
||||||
|
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
|
||||||
|
|
||||||
|
evtd@^0.2.2, evtd@^0.2.4:
|
||||||
|
version "0.2.4"
|
||||||
|
resolved "https://registry.npmmirror.com/evtd/-/evtd-0.2.4.tgz"
|
||||||
|
integrity sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==
|
||||||
|
|
||||||
|
follow-redirects@^1.15.6:
|
||||||
|
version "1.15.9"
|
||||||
|
resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz"
|
||||||
|
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
|
||||||
|
|
||||||
|
form-data@^4.0.0:
|
||||||
|
version "4.0.2"
|
||||||
|
resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.2.tgz"
|
||||||
|
integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==
|
||||||
|
dependencies:
|
||||||
|
asynckit "^0.4.0"
|
||||||
|
combined-stream "^1.0.8"
|
||||||
|
es-set-tostringtag "^2.1.0"
|
||||||
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
|
function-bind@^1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz"
|
||||||
|
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||||
|
|
||||||
|
get-intrinsic@^1.2.6:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
|
||||||
|
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers "^1.0.2"
|
||||||
|
es-define-property "^1.0.1"
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
es-object-atoms "^1.1.1"
|
||||||
|
function-bind "^1.1.2"
|
||||||
|
get-proto "^1.0.1"
|
||||||
|
gopd "^1.2.0"
|
||||||
|
has-symbols "^1.1.0"
|
||||||
|
hasown "^2.0.2"
|
||||||
|
math-intrinsics "^1.1.0"
|
||||||
|
|
||||||
|
get-proto@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz"
|
||||||
|
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
|
||||||
|
dependencies:
|
||||||
|
dunder-proto "^1.0.1"
|
||||||
|
es-object-atoms "^1.0.0"
|
||||||
|
|
||||||
|
gopd@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz"
|
||||||
|
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
|
||||||
|
|
||||||
|
has-symbols@^1.0.3, has-symbols@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz"
|
||||||
|
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
|
||||||
|
|
||||||
|
has-tostringtag@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz"
|
||||||
|
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
|
||||||
|
dependencies:
|
||||||
|
has-symbols "^1.0.3"
|
||||||
|
|
||||||
|
hasown@^2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz"
|
||||||
|
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
|
||||||
|
dependencies:
|
||||||
|
function-bind "^1.1.2"
|
||||||
|
|
||||||
|
he@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.npmmirror.com/he/-/he-1.2.0.tgz"
|
||||||
|
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||||
|
|
||||||
|
highlight.js@^11.8.0:
|
||||||
|
version "11.11.1"
|
||||||
|
resolved "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz"
|
||||||
|
integrity sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==
|
||||||
|
|
||||||
|
lodash-es@^4.17.21:
|
||||||
|
version "4.17.21"
|
||||||
|
resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz"
|
||||||
|
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||||
|
|
||||||
|
lodash@^4.17.21:
|
||||||
|
version "4.17.21"
|
||||||
|
resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz"
|
||||||
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
|
||||||
|
magic-string@^0.30.11:
|
||||||
|
version "0.30.17"
|
||||||
|
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz"
|
||||||
|
integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
|
||||||
|
dependencies:
|
||||||
|
"@jridgewell/sourcemap-codec" "^1.5.0"
|
||||||
|
|
||||||
|
marked@^15.0.7:
|
||||||
|
version "15.0.7"
|
||||||
|
resolved "https://registry.npmmirror.com/marked/-/marked-15.0.7.tgz"
|
||||||
|
integrity sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==
|
||||||
|
|
||||||
|
math-intrinsics@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz"
|
||||||
|
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
|
||||||
|
|
||||||
|
mime-db@1.52.0:
|
||||||
|
version "1.52.0"
|
||||||
|
resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz"
|
||||||
|
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||||
|
|
||||||
|
mime-types@^2.1.12:
|
||||||
|
version "2.1.35"
|
||||||
|
resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz"
|
||||||
|
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||||
|
dependencies:
|
||||||
|
mime-db "1.52.0"
|
||||||
|
|
||||||
|
minimatch@^9.0.3:
|
||||||
|
version "9.0.5"
|
||||||
|
resolved "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz"
|
||||||
|
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
|
||||||
|
dependencies:
|
||||||
|
brace-expansion "^2.0.1"
|
||||||
|
|
||||||
|
muggle-string@^0.4.1:
|
||||||
|
version "0.4.1"
|
||||||
|
resolved "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz"
|
||||||
|
integrity sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==
|
||||||
|
|
||||||
|
naive-ui@^2.41.0:
|
||||||
|
version "2.41.0"
|
||||||
|
resolved "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.41.0.tgz"
|
||||||
|
integrity sha512-KnmLg+xPLwXV8QVR7ZZ69eCjvel7R5vru8+eFe4VoAJHEgqAJgVph6Zno9K2IVQRpSF3GBGea3tjavslOR4FAA==
|
||||||
|
dependencies:
|
||||||
|
"@css-render/plugin-bem" "^0.15.14"
|
||||||
|
"@css-render/vue3-ssr" "^0.15.14"
|
||||||
|
"@types/katex" "^0.16.2"
|
||||||
|
"@types/lodash" "^4.14.198"
|
||||||
|
"@types/lodash-es" "^4.17.9"
|
||||||
|
async-validator "^4.2.5"
|
||||||
|
css-render "^0.15.14"
|
||||||
|
csstype "^3.1.3"
|
||||||
|
date-fns "^3.6.0"
|
||||||
|
date-fns-tz "^3.1.3"
|
||||||
|
evtd "^0.2.4"
|
||||||
|
highlight.js "^11.8.0"
|
||||||
|
lodash "^4.17.21"
|
||||||
|
lodash-es "^4.17.21"
|
||||||
|
seemly "^0.3.8"
|
||||||
|
treemate "^0.3.11"
|
||||||
|
vdirs "^0.1.8"
|
||||||
|
vooks "^0.2.12"
|
||||||
|
vueuc "^0.4.63"
|
||||||
|
|
||||||
|
nanoid@^3.3.8:
|
||||||
|
version "3.3.8"
|
||||||
|
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.8.tgz"
|
||||||
|
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
|
||||||
|
|
||||||
|
path-browserify@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz"
|
||||||
|
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
|
||||||
|
|
||||||
|
picocolors@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz"
|
||||||
|
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||||
|
|
||||||
|
postcss@^8.4.48, postcss@^8.5.3:
|
||||||
|
version "8.5.3"
|
||||||
|
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.3.tgz"
|
||||||
|
integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==
|
||||||
|
dependencies:
|
||||||
|
nanoid "^3.3.8"
|
||||||
|
picocolors "^1.1.1"
|
||||||
|
source-map-js "^1.2.1"
|
||||||
|
|
||||||
|
proxy-from-env@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
|
||||||
|
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||||
|
|
||||||
|
rollup@^4.30.1:
|
||||||
|
version "4.34.9"
|
||||||
|
resolved "https://registry.npmmirror.com/rollup/-/rollup-4.34.9.tgz"
|
||||||
|
integrity sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/estree" "1.0.6"
|
||||||
|
optionalDependencies:
|
||||||
|
"@rollup/rollup-android-arm-eabi" "4.34.9"
|
||||||
|
"@rollup/rollup-android-arm64" "4.34.9"
|
||||||
|
"@rollup/rollup-darwin-arm64" "4.34.9"
|
||||||
|
"@rollup/rollup-darwin-x64" "4.34.9"
|
||||||
|
"@rollup/rollup-freebsd-arm64" "4.34.9"
|
||||||
|
"@rollup/rollup-freebsd-x64" "4.34.9"
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf" "4.34.9"
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf" "4.34.9"
|
||||||
|
"@rollup/rollup-linux-arm64-gnu" "4.34.9"
|
||||||
|
"@rollup/rollup-linux-arm64-musl" "4.34.9"
|
||||||
|
"@rollup/rollup-linux-loongarch64-gnu" "4.34.9"
|
||||||
|
"@rollup/rollup-linux-powerpc64le-gnu" "4.34.9"
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu" "4.34.9"
|
||||||
|
"@rollup/rollup-linux-s390x-gnu" "4.34.9"
|
||||||
|
"@rollup/rollup-linux-x64-gnu" "4.34.9"
|
||||||
|
"@rollup/rollup-linux-x64-musl" "4.34.9"
|
||||||
|
"@rollup/rollup-win32-arm64-msvc" "4.34.9"
|
||||||
|
"@rollup/rollup-win32-ia32-msvc" "4.34.9"
|
||||||
|
"@rollup/rollup-win32-x64-msvc" "4.34.9"
|
||||||
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
|
seemly@^0.3.6, seemly@^0.3.8:
|
||||||
|
version "0.3.10"
|
||||||
|
resolved "https://registry.npmmirror.com/seemly/-/seemly-0.3.10.tgz"
|
||||||
|
integrity sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==
|
||||||
|
|
||||||
|
source-map-js@^1.2.0, source-map-js@^1.2.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz"
|
||||||
|
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||||
|
|
||||||
|
treemate@^0.3.11:
|
||||||
|
version "0.3.11"
|
||||||
|
resolved "https://registry.npmmirror.com/treemate/-/treemate-0.3.11.tgz"
|
||||||
|
integrity sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==
|
||||||
|
|
||||||
|
typescript@*, typescript@>=5.0.0, typescript@~5.7.2, typescript@5.x:
|
||||||
|
version "5.7.3"
|
||||||
|
resolved "https://registry.npmmirror.com/typescript/-/typescript-5.7.3.tgz"
|
||||||
|
integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==
|
||||||
|
|
||||||
|
undici-types@~6.20.0:
|
||||||
|
version "6.20.0"
|
||||||
|
resolved "https://registry.npmmirror.com/undici-types/-/undici-types-6.20.0.tgz"
|
||||||
|
integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==
|
||||||
|
|
||||||
|
vdirs@^0.1.4, vdirs@^0.1.8:
|
||||||
|
version "0.1.8"
|
||||||
|
resolved "https://registry.npmmirror.com/vdirs/-/vdirs-0.1.8.tgz"
|
||||||
|
integrity sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==
|
||||||
|
dependencies:
|
||||||
|
evtd "^0.2.2"
|
||||||
|
|
||||||
|
"vite@^5.0.0 || ^6.0.0", vite@^6.2.0:
|
||||||
|
version "6.2.0"
|
||||||
|
resolved "https://registry.npmmirror.com/vite/-/vite-6.2.0.tgz"
|
||||||
|
integrity sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==
|
||||||
|
dependencies:
|
||||||
|
esbuild "^0.25.0"
|
||||||
|
postcss "^8.5.3"
|
||||||
|
rollup "^4.30.1"
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents "~2.3.3"
|
||||||
|
|
||||||
|
vooks@^0.2.12, vooks@^0.2.4:
|
||||||
|
version "0.2.12"
|
||||||
|
resolved "https://registry.npmmirror.com/vooks/-/vooks-0.2.12.tgz"
|
||||||
|
integrity sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==
|
||||||
|
dependencies:
|
||||||
|
evtd "^0.2.2"
|
||||||
|
|
||||||
|
vscode-uri@^3.0.8:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz"
|
||||||
|
integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==
|
||||||
|
|
||||||
|
vue-tsc@^2.2.4:
|
||||||
|
version "2.2.8"
|
||||||
|
resolved "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.8.tgz"
|
||||||
|
integrity sha512-jBYKBNFADTN+L+MdesNX/TB3XuDSyaWynKMDgR+yCSln0GQ9Tfb7JS2lr46s2LiFUT1WsmfWsSvIElyxzOPqcQ==
|
||||||
|
dependencies:
|
||||||
|
"@volar/typescript" "~2.4.11"
|
||||||
|
"@vue/language-core" "2.2.8"
|
||||||
|
|
||||||
|
vue@^3.0.0, vue@^3.0.11, vue@^3.2.25, vue@^3.4.0, vue@^3.5.13, vue@3.5.13:
|
||||||
|
version "3.5.13"
|
||||||
|
resolved "https://registry.npmmirror.com/vue/-/vue-3.5.13.tgz"
|
||||||
|
integrity sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==
|
||||||
|
dependencies:
|
||||||
|
"@vue/compiler-dom" "3.5.13"
|
||||||
|
"@vue/compiler-sfc" "3.5.13"
|
||||||
|
"@vue/runtime-dom" "3.5.13"
|
||||||
|
"@vue/server-renderer" "3.5.13"
|
||||||
|
"@vue/shared" "3.5.13"
|
||||||
|
|
||||||
|
vueuc@^0.4.63:
|
||||||
|
version "0.4.64"
|
||||||
|
resolved "https://registry.npmmirror.com/vueuc/-/vueuc-0.4.64.tgz"
|
||||||
|
integrity sha512-wlJQj7fIwKK2pOEoOq4Aro8JdPOGpX8aWQhV8YkTW9OgWD2uj2O8ANzvSsIGjx7LTOc7QbS7sXdxHi6XvRnHPA==
|
||||||
|
dependencies:
|
||||||
|
"@css-render/vue3-ssr" "^0.15.10"
|
||||||
|
"@juggle/resize-observer" "^3.3.1"
|
||||||
|
css-render "^0.15.10"
|
||||||
|
evtd "^0.2.4"
|
||||||
|
seemly "^0.3.6"
|
||||||
|
vdirs "^0.1.4"
|
||||||
|
vooks "^0.2.4"
|
||||||
@@ -476,7 +476,7 @@ class StockAnalyzer:
|
|||||||
|
|
||||||
def _process_ai_stream(self, response, stock_code) -> Generator[str, None, None]:
|
def _process_ai_stream(self, response, stock_code) -> Generator[str, None, None]:
|
||||||
"""处理AI流式响应"""
|
"""处理AI流式响应"""
|
||||||
logger.info(f"开始处理 {stock_code} 的AI流式响应")
|
logger.info(f"开始处理股票 {stock_code} 的AI流式响应\n")
|
||||||
buffer = ""
|
buffer = ""
|
||||||
chunk_count = 0
|
chunk_count = 0
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from flask import Flask, render_template, request, jsonify, Response, stream_with_context
|
from flask import Flask, render_template, request, jsonify, Response, stream_with_context, send_from_directory
|
||||||
from stock_analyzer import StockAnalyzer
|
from stock_analyzer import StockAnalyzer
|
||||||
from us_stock_service import USStockService
|
from us_stock_service import USStockService
|
||||||
from fund_service import FundService # 新增导入
|
from fund_service import FundService # 新增导入
|
||||||
@@ -16,24 +16,44 @@ load_dotenv()
|
|||||||
# 获取日志器
|
# 获取日志器
|
||||||
logger = get_logger()
|
logger = get_logger()
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__,
|
||||||
|
static_folder='frontend/dist',
|
||||||
|
static_url_path='/')
|
||||||
|
|
||||||
analyzer = StockAnalyzer()
|
analyzer = StockAnalyzer()
|
||||||
us_stock_service = USStockService()
|
us_stock_service = USStockService()
|
||||||
fund_service = FundService() # 新增服务实例
|
fund_service = FundService() # 新增服务实例
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
announcement = os.getenv('ANNOUNCEMENT_TEXT') or None
|
# 检查是否使用前端构建版本
|
||||||
# 获取默认API配置信息
|
frontend_dist = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'frontend', 'dist')
|
||||||
default_api_url = os.getenv('API_URL', '')
|
if os.path.exists(frontend_dist):
|
||||||
default_api_model = os.getenv('API_MODEL', 'gpt-3.5-turbo')
|
return send_from_directory(frontend_dist, 'index.html')
|
||||||
default_api_timeout = os.getenv('API_TIMEOUT', '60')
|
else:
|
||||||
# 不传递API_KEY到前端,出于安全考虑
|
# 传统模板渲染,用于兼容旧版本
|
||||||
return render_template('index.html',
|
announcement = os.getenv('ANNOUNCEMENT_TEXT') or None
|
||||||
announcement=announcement,
|
# 获取默认API配置信息
|
||||||
default_api_url=default_api_url,
|
default_api_url = os.getenv('API_URL', '')
|
||||||
default_api_model=default_api_model,
|
default_api_model = os.getenv('API_MODEL', 'gpt-3.5-turbo')
|
||||||
default_api_timeout=default_api_timeout)
|
default_api_timeout = os.getenv('API_TIMEOUT', '60')
|
||||||
|
# 不传递API_KEY到前端,出于安全考虑
|
||||||
|
return render_template('index.html',
|
||||||
|
announcement=announcement,
|
||||||
|
default_api_url=default_api_url,
|
||||||
|
default_api_model=default_api_model,
|
||||||
|
default_api_timeout=default_api_timeout)
|
||||||
|
|
||||||
|
@app.route('/config')
|
||||||
|
def get_config():
|
||||||
|
"""返回系统配置信息"""
|
||||||
|
config = {
|
||||||
|
'announcement': os.getenv('ANNOUNCEMENT_TEXT') or '',
|
||||||
|
'default_api_url': os.getenv('API_URL', ''),
|
||||||
|
'default_api_model': os.getenv('API_MODEL', 'gpt-3.5-turbo'),
|
||||||
|
'default_api_timeout': os.getenv('API_TIMEOUT', '60')
|
||||||
|
}
|
||||||
|
return jsonify(config)
|
||||||
|
|
||||||
@app.route('/analyze', methods=['POST'])
|
@app.route('/analyze', methods=['POST'])
|
||||||
def analyze():
|
def analyze():
|
||||||
@@ -200,4 +220,4 @@ def test_api_connection():
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logger.info("股票分析系统启动")
|
logger.info("股票分析系统启动")
|
||||||
app.run(host='0.0.0.0', port=8888, debug=True)
|
app.run(host='127.0.0.1', port=8888, debug=True)
|
||||||
Reference in New Issue
Block a user