feat: vue重构

This commit is contained in:
Cassianvale
2025-03-05 17:45:09 +08:00
parent 5dacc9f528
commit 4393bf68cd
27 changed files with 4164 additions and 15 deletions

5
frontend/README.md Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

28
frontend/package.json Normal file
View 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
View 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
View 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>

View 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

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View File

@@ -0,0 +1,6 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')

View 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
View 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;
}
}

View 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
View 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
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View 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
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View 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
View 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
View 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"