Merge from Cassianvale: feat: 适配手机端

This commit is contained in:
兰志宏
2025-03-11 18:06:53 +08:00
8 changed files with 1409 additions and 390 deletions

2
.gitignore vendored
View File

@@ -21,6 +21,7 @@ build_upload.log
*.spec
*.zip
.env
.env.*
# frontend
node_modules/
@@ -38,6 +39,7 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.vite
# Byte-compiled / optimized / DLL files
__pycache__/

View File

@@ -1,5 +1,14 @@
/* 移动端通用样式优化 */
/* 标准断点变量 -- 通过CSS变量实现统一断点管理 */
:root {
--mobile-xs-breakpoint: 480px; /* 小型手机设备 */
--mobile-sm-breakpoint: 576px; /* 普通手机设备 */
--mobile-md-breakpoint: 768px; /* 平板和大型手机 */
}
/* ===== 基础移动端组件 ===== */
/* 增大触摸目标区域 */
.mobile-touch-target {
min-height: 44px; /* 推荐的最小触摸目标尺寸 */
@@ -34,22 +43,22 @@
box-sizing: border-box;
}
/* 自适应字体大小 */
@media (max-width: 480px) {
.mobile-adaptive-text {
font-size: 14px;
}
.mobile-adaptive-heading {
font-size: 18px;
}
/* ===== 移动端布局类 ===== */
/* 全宽容器 */
.mobile-full-width {
width: 100% !important;
max-width: 100% !important;
margin-left: 0 !important;
margin-right: 0 !important;
box-sizing: border-box !important;
}
/* 移动端表格优化 */
.mobile-table-container {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
/* 移动端内容容器 */
.mobile-content-container {
padding: 0.75rem !important;
width: 100% !important;
box-sizing: border-box !important;
}
/* 底部操作区固定 */
@@ -71,6 +80,15 @@
height: 60px; /* 预留底部空间 */
}
/* 底部背景延伸 */
.mobile-bottom-extend {
position: relative;
padding-bottom: env(safe-area-inset-bottom, 0);
margin-bottom: -1px; /* 防止底部出现缝隙 */
}
/* ===== 移动端UI元素 ===== */
/* 移动端友好的卡片样式 */
.mobile-card {
border-radius: 12px !important;
@@ -78,26 +96,23 @@
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08) !important;
}
/* 移动端卡片边距优化 */
.mobile-card-spacing {
margin: 0.5rem 0 !important;
border-radius: 0.75rem !important;
}
/* 移动端阴影优化 - 更轻微的阴影效果 */
.mobile-shadow {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08) !important;
}
/* 移动端列表样式优化 */
.mobile-list-item {
padding: 12px !important;
}
/* 可滑动区域提示 */
.mobile-scrollable-hint {
position: relative;
}
.mobile-scrollable-hint::after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 24px;
background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.8));
pointer-events: none;
}
/* ===== 特殊效果类 ===== */
/* 边框优化 */
.mobile-border-fix {
@@ -122,42 +137,415 @@
pointer-events: none;
}
/* 底部背景延伸 */
.mobile-bottom-extend {
/* 可滑动区域提示 */
.mobile-scrollable-hint {
position: relative;
padding-bottom: env(safe-area-inset-bottom, 0);
margin-bottom: -1px; /* 防止底部出现缝隙 */
}
/* 全宽容器 */
.mobile-full-width {
.mobile-scrollable-hint::after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 24px;
background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.8));
pointer-events: none;
}
/* 表格横向滚动指示器 */
.mobile-table-scroll-indicator {
position: relative;
}
.mobile-table-scroll-indicator::after {
content: '←→';
position: absolute;
bottom: 10px;
right: 10px;
color: rgba(32, 128, 240, 0.6);
font-size: 14px;
pointer-events: none;
z-index: 2;
animation: fadeInOut 2s infinite;
}
@keyframes fadeInOut {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.8; }
}
/* ===== 响应式文本 ===== */
/* 自适应字体大小 */
@media (max-width: 480px) {
.mobile-adaptive-text {
font-size: 14px;
}
.mobile-adaptive-heading {
font-size: 18px;
}
}
/* ===== 网格系统优化 ===== */
/* 网格布局基础类 */
.mobile-grid {
width: 100% !important;
max-width: 100% !important;
margin-left: 0 !important;
margin-right: 0 !important;
box-sizing: border-box !important;
}
/* 移动端卡片边距优化 */
.mobile-card-spacing {
margin: 0.5rem 0 !important;
border-radius: 0.75rem !important;
.mobile-grid-item {
width: 100% !important;
max-width: 100% !important;
padding: 0 !important;
}
/* 移动端阴影优化 - 更轻微的阴影效果 */
.mobile-shadow {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08) !important;
}
/* ===== API配置面板专用类 ===== */
/* 移动端内容容器 - 确保内容不会太靠近边缘 */
.mobile-content-container {
.mobile-connection-status {
padding: 0.75rem !important;
border-radius: 0.5rem !important;
margin-top: 0.75rem !important;
}
.mobile-api-config-section {
margin-bottom: 1.5rem !important;
width: 100% !important;
}
.mobile-api-config-card {
padding: 0.5rem !important;
border-radius: 0.625rem !important;
margin-bottom: 1rem !important;
}
.mobile-api-actions {
display: flex;
width: 100%;
}
.mobile-api-buttons {
display: flex;
gap: 8px;
}
.mobile-api-info-alert {
padding: 0.75rem !important;
margin-bottom: 0.75rem !important;
border-radius: 0.5rem !important;
}
.mobile-model-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.mobile-toggle-text {
font-size: 0.875rem;
}
.mobile-api-save-option {
display: flex;
align-items: center;
width: 100%;
}
.mobile-api-button {
height: 36px;
min-width: 40%;
display: flex;
align-items: center;
justify-content: center;
}
.mobile-toggle-button {
font-weight: 500;
transition: all 0.3s ease;
border-radius: 16px;
padding: 4px 12px;
}
/* ===== StockCard专用类 ===== */
.mobile-stock-card {
width: 100% !important;
box-sizing: border-box !important;
border-radius: 0.75rem !important;
overflow: hidden !important;
margin-bottom: 0.75rem;
}
.mobile-card-header {
padding: 0.75rem !important;
border-bottom: 1px solid rgba(0, 0, 0, 0.05) !important;
}
.mobile-card-content {
padding: 0.75rem !important;
}
/* ===== StockSearch专用类 ===== */
.mobile-search-results {
max-width: 100%;
width: 100%;
border-radius: 0.75rem;
border: 1px solid var(--n-border-color, rgba(0, 0, 0, 0.1));
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.mobile-search-result-item {
padding: 0.625rem 0.875rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
min-height: 44px; /* 确保触摸区域足够大 */
}
.mobile-result-name {
max-width: 170px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 小屏幕适配 */
@media (max-width: 480px) {
.mobile-result-name {
max-width: 120px;
}
.mobile-search-result-item {
padding: 0.5rem 0.75rem;
}
.mobile-search-results {
border-radius: 0.625rem;
border: 1px solid rgba(0, 0, 0, 0.08);
}
}
/* ===== MarketTimeDisplay专用类 ===== */
.mobile-market-time-card {
padding: 0.5rem;
margin-bottom: 1rem;
min-height: 180px; /* 移动端下的最小高度 */
}
.mobile-time-block {
padding: 0.625rem;
margin-bottom: 0.75rem; /* 增加底部外边距 */
}
.mobile-current-time {
font-size: 1.5rem;
}
.mobile-time-label {
font-size: 0.9375rem;
margin-bottom: 0.5rem;
}
.mobile-status-tag {
min-width: 100px; /* 减小移动端下的最小宽度 */
height: 36px !important;
font-size: 0.875rem; /* 减小字体大小 */
}
.mobile-time-counter {
font-size: 0.75rem; /* 减小字体大小 */
margin-top: 0.375rem;
}
/* 小屏幕特殊适配 */
@media (max-width: 480px) {
.mobile-market-time-card {
padding: 0.375rem;
min-height: 160px; /* 小屏幕下的最小高度 */
}
.mobile-time-block {
padding: 0.5rem;
margin-bottom: 1rem; /* 增加小屏幕下的底部外边距 */
}
.mobile-current-time {
font-size: 1.25rem;
}
.mobile-time-label {
font-size: 0.875rem;
}
.mobile-time-counter {
font-size: 0.75rem;
}
.mobile-status-tag {
min-width: 90px; /* 进一步减小最小宽度 */
font-size: 0.8125rem;
padding: 0 12px !important; /* 减小内边距 */
}
}
/* ===== 媒体查询部分 ===== */
/* 平板和大型手机 - 768px以下 */
@media (max-width: 768px) {
.mobile-content-container {
padding: 0.75rem !important;
}
.mobile-api-actions {
flex-direction: column !important;
align-items: flex-start !important;
}
.mobile-api-buttons {
width: 100% !important;
justify-content: space-between !important;
margin-top: 0.75rem !important;
}
.mobile-api-config-card {
padding: 0.75rem !important;
width: 100% !important;
box-sizing: border-box !important;
border-radius: 0.75rem !important;
overflow: hidden !important;
border: 1px solid rgba(0, 0, 0, 0.08) !important;
}
.mobile-url-feedback {
flex-direction: column !important;
width: 100% !important;
}
/* 网格优化 */
:deep(.n-grid) {
gap: 12px !important;
margin-bottom: 12px !important;
}
:deep(.n-grid-item) {
margin-bottom: 12px !important;
}
}
/* 普通手机设备 - 480px以下 */
@media (max-width: 480px) {
.mobile-content-container {
padding: 0.5rem !important;
}
.mobile-api-config-section {
padding-bottom: 15px !important;
}
.mobile-api-config-card {
padding: 0.5rem !important;
min-height: 80px !important;
}
/* 小屏幕网格优化 */
.mobile-grid-small {
width: 100% !important;
max-width: 100% !important;
gap: 8px !important;
}
.mobile-grid-item-small {
padding: 0 !important;
margin-bottom: 8px !important;
}
:deep(.n-grid) {
gap: 6px !important;
}
:deep(.n-grid-item) {
padding: 0 !important;
margin-bottom: 6px !important;
}
:deep(.n-grid-item) > * {
margin-bottom: 6px !important;
}
.mobile-form-item {
margin-bottom: 8px !important;
}
.mobile-api-buttons-small {
flex-wrap: wrap !important;
gap: 0.5rem !important;
}
.mobile-api-button {
flex: 1 !important;
}
.mobile-toggle-button {
width: 100% !important;
}
.mobile-api-info-alert-small {
padding: 0.5rem !important;
margin-bottom: 0.5rem !important;
font-size: 0.75rem !important;
}
.mobile-model-tag {
font-size: 0.75rem !important;
padding: 0 0.5rem !important;
}
}
/* ===== AnnouncementBanner专用类 ===== */
.mobile-announcement-container {
max-width: 100%;
margin-left: 0.5rem;
margin-right: 0.5rem;
}
.mobile-announcement-header {
font-size: 0.9375rem;
}
.mobile-announcement-content {
font-size: 0.875rem;
}
.mobile-announcement-timer {
font-size: 0.75rem;
}
.mobile-login-announcement {
top: 0.75rem;
right: 0.75rem;
left: 0.75rem;
}
/* 小屏幕适配 */
@media (max-width: 480px) {
.mobile-announcement-container {
top: 0.25rem;
right: 0.25rem;
left: 0.25rem;
max-width: calc(100% - 0.5rem);
}
.mobile-announcement-header {
font-size: 0.875rem;
}
.mobile-announcement-content {
font-size: 0.8125rem;
}
.mobile-announcement-timer {
font-size: 0.6875rem;
}
}

View File

@@ -1,14 +1,14 @@
<template>
<div v-if="showAnnouncement" class="announcement-container" :class="{ 'login-page-announcement': isLoginPage }">
<div v-if="showAnnouncement" class="announcement-container mobile-announcement-container" :class="{ 'login-page-announcement mobile-login-announcement': isLoginPage }">
<n-card class="announcement-card mobile-card" :class="{ 'login-card-style': isLoginPage }">
<template #header>
<div class="announcement-header">
<div class="announcement-header mobile-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>
<div class="announcement-content mobile-announcement-content" v-html="processedContent"></div>
<div class="announcement-timer mobile-announcement-timer">{{ remainingTimeText }}</div>
<template #action>
<n-button quaternary circle size="small" @click="closeAnnouncement" class="mobile-touch-target">
<template #icon>
@@ -154,54 +154,4 @@ onBeforeUnmount(() => {
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
}
/* 移动端适配 */
@media (max-width: 768px) {
.announcement-container {
top: 0.5rem;
right: 0.5rem;
left: 0.5rem;
max-width: calc(100% - 1rem);
}
.announcement-card {
width: 100%;
}
.announcement-header {
font-size: 0.9375rem;
}
.announcement-content {
font-size: 0.875rem;
}
/* 登录页面移动端适配 */
.login-page-announcement {
top: 0.75rem;
right: 0.75rem;
left: 0.75rem;
}
}
/* 小屏幕手机适配 */
@media (max-width: 480px) {
.announcement-container {
top: 0.25rem;
right: 0.25rem;
left: 0.25rem;
max-width: calc(100% - 0.5rem);
}
.announcement-header {
font-size: 0.875rem;
}
.announcement-content {
font-size: 0.8125rem;
}
.announcement-timer {
font-size: 0.6875rem;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="api-config-section">
<div class="api-config-section mobile-api-config-section">
<n-button
class="toggle-button mobile-touch-target"
class="toggle-button mobile-touch-target mobile-toggle-button"
size="small"
@click="toggleConfig"
:quaternary="true"
@@ -10,12 +10,12 @@
<template #icon>
<n-icon :component="expanded ? ChevronUpIcon : ChevronDownIcon" />
</template>
<span class="toggle-text">API配置 {{ expanded ? '收起' : '展开' }}</span>
<span class="toggle-text mobile-toggle-text">API配置 {{ expanded ? '收起' : '展开' }}</span>
</n-button>
<n-collapse-transition :show="expanded">
<n-card class="api-config-card mobile-card mobile-shadow" :bordered="false">
<n-alert title="OpenAI API配置" type="info" v-if="isApiInfoVisible" class="api-info-alert">
<n-card class="api-config-card mobile-card mobile-shadow mobile-api-config-card" :bordered="false">
<n-alert title="OpenAI API配置" type="info" v-if="isApiInfoVisible" class="api-info-alert mobile-api-info-alert mobile-api-info-alert-small">
<template #icon>
<n-icon :component="InformationCircleIcon" />
</template>
@@ -29,9 +29,9 @@
</div>
</n-alert>
<n-grid :cols="24" :x-gap="16" :y-gap="16" responsive="screen">
<n-grid-item :span="24" :md-span="14" :lg-span="14">
<n-form-item label="API URL" path="apiUrl">
<n-grid :cols="24" :x-gap="16" :y-gap="16" responsive="screen" class="mobile-grid mobile-grid-small">
<n-grid-item :span="24" :md-span="14" :lg-span="14" class="mobile-grid-item mobile-grid-item-small">
<n-form-item label="API URL" path="apiUrl" class="mobile-form-item">
<n-input
v-model:value="apiConfig.apiUrl"
placeholder="https://api.openai.com/v1/chat/completions"
@@ -43,7 +43,7 @@
</template>
</n-input>
<template #feedback>
<div class="url-feedback">
<div class="url-feedback mobile-url-feedback">
<span class="formatted-url">实际请求地址: {{ formattedUrl }}</span>
<div class="url-tips">
<div>提示: URL以/结尾将忽略v1路径</div>
@@ -54,8 +54,8 @@
</n-form-item>
</n-grid-item>
<n-grid-item :span="24" :md-span="10" :lg-span="10">
<n-form-item label="API Key" path="apiKey">
<n-grid-item :span="24" :md-span="10" :lg-span="10" class="mobile-grid-item mobile-grid-item-small">
<n-form-item label="API Key" path="apiKey" class="mobile-form-item">
<n-input
v-model:value="apiConfig.apiKey"
type="password"
@@ -71,8 +71,8 @@
</n-form-item>
</n-grid-item>
<n-grid-item :span="12" :md-span="12" :lg-span="12">
<n-form-item label="模型" path="apiModel">
<n-grid-item :span="12" :md-span="12" :lg-span="12" class="mobile-grid-item mobile-grid-item-small">
<n-form-item label="模型" path="apiModel" class="mobile-form-item">
<n-input
v-model:value="apiConfig.apiModel"
placeholder="输入或选择模型名称"
@@ -101,7 +101,7 @@
<div class="model-suggestions">
<div class="model-tip">您可以直接输入模型名称或点击右侧按钮从下拉菜单选择</div>
<span>常用模型:</span>
<div class="model-chips">
<div class="model-chips mobile-model-chips">
<n-tag
v-for="model in commonModels"
:key="model.key"
@@ -109,6 +109,7 @@
round
clickable
@click="selectModel(model.key)"
class="mobile-model-tag"
>
{{ model.label }}
</n-tag>
@@ -118,8 +119,8 @@
</n-form-item>
</n-grid-item>
<n-grid-item :span="12" :md-span="12" :lg-span="12">
<n-form-item label="超时时间(秒)" path="apiTimeout">
<n-grid-item :span="12" :md-span="12" :lg-span="12" class="mobile-grid-item mobile-grid-item-small">
<n-form-item label="超时时间(秒)" path="apiTimeout" class="mobile-form-item">
<n-input-number
v-model:value="apiTimeout"
placeholder="60"
@@ -144,13 +145,14 @@
</n-grid-item>
</n-grid>
<div class="api-actions">
<div class="api-save-option">
<div class="api-actions mobile-api-actions">
<div class="api-save-option mobile-api-save-option">
<n-button
tertiary
size="small"
@click="saveConfig"
round
class="mobile-api-save-option-button"
>
<template #icon>
<n-icon :component="SaveIcon" />
@@ -159,13 +161,14 @@
</n-button>
</div>
<div class="api-buttons">
<div class="api-buttons mobile-api-buttons mobile-api-buttons-small">
<n-button
type="primary"
:loading="testingConnection"
:disabled="!isConfigValid"
@click="testConnection"
round
class="mobile-api-button"
>
<template #icon>
<n-icon :component="CheckmarkIcon" />
@@ -173,7 +176,7 @@
测试连接
</n-button>
<n-button @click="resetConfig" round>
<n-button @click="resetConfig" round class="mobile-api-button">
<template #icon>
<n-icon :component="RefreshIcon" />
</template>
@@ -184,7 +187,7 @@
<n-divider v-if="connectionStatus" style="margin: 16px 0 12px" />
<div v-if="connectionStatus" class="connection-status" :class="connectionStatus.type">
<div v-if="connectionStatus" class="connection-status mobile-connection-status" :class="connectionStatus.type">
<n-icon :component="connectionStatus.icon" class="status-icon" />
<span class="status-message">{{ connectionStatus.message }}</span>
</div>
@@ -611,123 +614,6 @@ onMounted(() => {
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 768px) {
.api-actions {
flex-direction: column;
align-items: flex-start;
width: 100%;
}
.api-buttons {
width: 100%;
justify-content: space-between;
margin-top: 0.75rem;
}
.api-config-card {
padding: 0.75rem;
width: 100% !important;
box-sizing: border-box !important;
border-radius: 0.75rem !important;
overflow: hidden;
border: 1px solid rgba(0, 0, 0, 0.08);
}
.url-feedback {
flex-direction: column;
width: 100%;
}
.api-info-alert {
padding: 0.75rem;
margin-bottom: 0.75rem;
border-radius: 0.5rem;
}
.model-chips {
gap: 4px;
flex-wrap: wrap;
width: 100%;
}
.toggle-text {
font-size: 0.8125rem;
}
.api-save-option {
width: 100%;
}
.api-save-option button {
width: 100%;
justify-content: center;
}
/* 确保输入框在移动端正确显示 */
:deep(.n-input) {
width: 100% !important;
}
/* 确保下拉菜单在移动端正确显示 */
:deep(.n-dropdown-menu) {
max-width: 90vw;
}
/* 确保连接状态在移动端正确显示 */
.connection-status {
padding: 0.75rem;
border-radius: 0.5rem;
margin-top: 0.75rem;
}
}
@media (max-width: 480px) {
.api-config-section {
margin-bottom: 1.5rem;
width: 100%;
padding-bottom: 15px;
}
.api-config-card {
padding: 0.5rem;
border-radius: 0.625rem !important;
margin-bottom: 1rem;
min-height: 80px;
}
.api-buttons {
flex-wrap: wrap;
gap: 0.5rem;
}
.api-buttons .n-button {
flex: 1;
min-width: 40%;
height: 36px !important;
}
.toggle-button {
width: 100%;
height: 36px !important;
}
.api-info-alert {
padding: 0.5rem;
margin-bottom: 0.5rem;
font-size: 0.75rem;
}
.model-chips :deep(.n-tag) {
font-size: 0.75rem;
padding: 0 0.5rem;
}
/* 确保边框在小屏幕上清晰可见 */
.api-config-card, .api-info-alert, .connection-status {
border: 1px solid rgba(0, 0, 0, 0.08) !important;
}
}
.model-suggestions {
margin-top: 6px;
font-size: 0.75rem;

View File

@@ -1,29 +1,29 @@
<template>
<n-card class="market-time-card mobile-card mobile-shadow">
<n-grid :x-gap="16" :y-gap="16" :cols="gridCols" responsive="screen">
<n-card class="market-time-card mobile-card mobile-shadow mobile-market-time-card">
<n-grid :x-gap="16" :y-gap="16" cols="1 s:2 m:4" responsive="screen">
<!-- 当前时间 -->
<n-grid-item :span="24" :md-span="6">
<div class="time-block current-time-block">
<p class="time-label">当前时间</p>
<p class="current-time">{{ marketInfo.currentTime }}</p>
<n-grid-item>
<div class="time-block current-time-block mobile-time-block">
<p class="time-label mobile-time-label">当前时间</p>
<p class="current-time mobile-current-time">{{ marketInfo.currentTime }}</p>
</div>
</n-grid-item>
<!-- A股状态 -->
<n-grid-item :span="24" :md-span="6">
<div class="time-block market-block" :class="{'market-open-block': marketInfo.cnMarket.isOpen, 'market-closed-block': !marketInfo.cnMarket.isOpen}">
<p class="time-label">A股市场</p>
<n-grid-item>
<div class="time-block market-block mobile-time-block" :class="{'market-open-block mobile-market-open-block': marketInfo.cnMarket.isOpen, 'market-closed-block mobile-market-closed-block': !marketInfo.cnMarket.isOpen}">
<p class="time-label mobile-time-label">A股市场</p>
<div class="market-status" :class="marketInfo.cnMarket.isOpen ? 'status-open' : 'status-closed'">
<n-tag v-if="marketInfo.cnMarket.isOpen" type="success" size="medium" round class="status-tag mobile-touch-target">
<n-tag v-if="marketInfo.cnMarket.isOpen" type="success" size="medium" round class="status-tag mobile-touch-target mobile-status-tag">
<template #icon><n-icon size="18"><pulse-icon /></n-icon></template>
交易中
</n-tag>
<n-tag v-else type="default" size="medium" round class="status-tag mobile-touch-target">
<n-tag v-else type="default" size="medium" round class="status-tag mobile-touch-target mobile-status-tag">
<template #icon><n-icon size="18"><time-icon /></n-icon></template>
已休市
</n-tag>
</div>
<p class="time-counter">{{ marketInfo.cnMarket.nextTime }}</p>
<p class="time-counter mobile-time-counter">{{ marketInfo.cnMarket.nextTime }}</p>
<div class="market-progress-container">
<div class="market-progress-bar"
:class="marketInfo.cnMarket.isOpen ? 'progress-open' : 'progress-closed'"
@@ -38,7 +38,7 @@
</n-grid-item>
<!-- 港股状态 -->
<n-grid-item :span="24" :md-span="6">
<n-grid-item>
<div class="time-block market-block" :class="{'market-open-block': marketInfo.hkMarket.isOpen, 'market-closed-block': !marketInfo.hkMarket.isOpen}">
<p class="time-label">港股市场</p>
<div class="market-status" :class="marketInfo.hkMarket.isOpen ? 'status-open' : 'status-closed'">
@@ -66,7 +66,7 @@
</n-grid-item>
<!-- 美股状态 -->
<n-grid-item :span="24" :md-span="6">
<n-grid-item>
<div class="time-block market-block" :class="{'market-open-block': marketInfo.usMarket.isOpen, 'market-closed-block': !marketInfo.usMarket.isOpen}">
<p class="time-label">美股市场</p>
<div class="market-status" :class="marketInfo.usMarket.isOpen ? 'status-open' : 'status-closed'">
@@ -97,7 +97,7 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { NCard, NGrid, NGridItem, NTag, NIcon } from 'naive-ui';
import {
PulseOutline as PulseIcon,
@@ -113,11 +113,6 @@ const marketInfo = ref<MarketTimeInfo>({
usMarket: { isOpen: false, nextTime: '' }
});
// 根据屏幕尺寸自动调整布局
const gridCols = computed(() => {
return 24;
});
let intervalId: number | null = null;
function updateMarketTime() {

View File

@@ -11,8 +11,8 @@
<n-layout class="main-layout">
<n-layout-content class="main-content mobile-content-container">
<!-- 市场时间显示 PC也显示移动端布局暂时隐藏-->
<!-- <MarketTimeDisplay :is-mobile="isMobile" /> -->
<!-- 市场时间显示 -->
<MarketTimeDisplay :is-mobile="isMobile" />
<!-- API配置面板 -->
<ApiConfigPanel
@@ -25,9 +25,9 @@
<!-- 主要内容 -->
<n-card class="analysis-container mobile-card mobile-card-spacing mobile-shadow">
<n-grid :cols="24" :x-gap="16" :y-gap="16" responsive="screen">
<n-grid cols="1 xl:24" :x-gap="16" :y-gap="16" responsive="screen">
<!-- 左侧配置区域 -->
<n-grid-item :span="24" :sm-span="24" :md-span="10" :lg-span="8">
<n-grid-item span="1 xl:8">
<div class="config-section">
<n-form-item label="选择市场类型">
<n-select
@@ -71,7 +71,7 @@
</n-grid-item>
<!-- 右侧结果区域 -->
<n-grid-item :span="24" :sm-span="24" :md-span="14" :lg-span="16">
<n-grid-item span="1 xl:16">
<div class="results-section">
<div class="results-header">
<n-space align="center" justify="space-between">
@@ -121,7 +121,7 @@
</template>
<template v-else-if="displayMode === 'card'">
<n-grid :cols="1" :x-gap="16" :y-gap="16" :lg-cols="2">
<n-grid cols="1" :x-gap="8" :y-gap="8" responsive="screen">
<n-grid-item v-for="stock in analyzedStocks" :key="stock.code">
<StockCard :stock="stock" />
</n-grid-item>
@@ -129,15 +129,18 @@
</template>
<template v-else>
<n-data-table
:columns="stockTableColumns"
:data="analyzedStocks"
:pagination="{ pageSize: 10 }"
:row-key="(row: StockInfo) => row.code"
:bordered="false"
:single-line="false"
striped
/>
<div class="table-container">
<n-data-table
:columns="stockTableColumns"
:data="analyzedStocks"
:pagination="{ pageSize: 10 }"
:row-key="(row: StockInfo) => row.code"
:bordered="false"
:single-line="false"
striped
:scroll-x="1200"
/>
</div>
</template>
</div>
</n-grid-item>
@@ -176,7 +179,7 @@ import {
DownloadOutline as DownloadIcon,
} from '@vicons/ionicons5';
// import MarketTimeDisplay from './MarketTimeDisplay.vue';
import MarketTimeDisplay from './MarketTimeDisplay.vue';
import ApiConfigPanel from './ApiConfigPanel.vue';
import StockSearch from './StockSearch.vue';
import StockCard from './StockCard.vue';
@@ -215,9 +218,9 @@ const apiConfig = ref<ApiConfig>({
});
// 移动端检测
// const isMobile = computed(() => {
// return window.innerWidth <= 768;
// });
const isMobile = computed(() => {
return window.innerWidth <= 768;
});
// 监听窗口大小变化
function handleResize() {
@@ -413,10 +416,13 @@ function handleMarketTypeChange() {
// 添加选择的股票
function addSelectedStock(symbol: string) {
// 确保symbol不包含序号或其他不需要的信息
const cleanSymbol = symbol.trim().replace(/^\d+\.\s*/, '');
if (stockCodes.value) {
stockCodes.value += ', ' + symbol;
stockCodes.value += ', ' + cleanSymbol;
} else {
stockCodes.value = symbol;
stockCodes.value = cleanSymbol;
}
}
@@ -1004,6 +1010,11 @@ function handleAnnouncementClose() {
margin-bottom: 1rem;
}
/* 修改卡片内容区域的内边距 */
.analysis-container :deep(.n-card__content) {
padding: 16px;
}
.config-section {
padding: 0.5rem;
}
@@ -1031,6 +1042,29 @@ function handleAnnouncementClose() {
word-break: break-word;
}
/* 表格容器基础样式 */
.table-container {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch; /* 支持iOS的滚动惯性 */
position: relative;
border-radius: 0.5rem;
}
/* 表格横向滚动指示器 */
.table-container::after {
content: '←→';
position: absolute;
bottom: 10px;
right: 10px;
color: rgba(32, 128, 240, 0.6);
font-size: 14px;
pointer-events: none;
z-index: 2;
animation: fadeInOut 2s infinite;
display: none; /* 默认隐藏,只在移动端显示 */
}
/* 移动端适配的媒体查询 */
@media (max-width: 768px) {
.main-content {
@@ -1039,6 +1073,26 @@ function handleAnnouncementClose() {
width: 100%;
}
/* 显示滚动指示器 */
.table-container::after {
display: block;
}
/* 减少移动端卡片内容区域的内边距 */
.analysis-container :deep(.n-card__content) {
padding: 12px 8px;
}
/* 确保卡片内部没有多余边距 */
:deep(.n-card > .n-card__content) {
padding: 12px 8px;
}
/* 减少结果区域的内边距 */
.results-section {
padding: 0.25rem 0.125rem;
}
.action-buttons {
flex-direction: column;
gap: 0.5rem;
@@ -1060,34 +1114,62 @@ function handleAnnouncementClose() {
box-sizing: border-box;
}
.config-section, .results-section {
.config-section {
padding: 0.25rem;
width: 100%;
box-sizing: border-box;
}
/* 确保表格内容在移动端可滚动 */
.n-data-table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
width: 100%;
/* 移动端表格样式优化 */
.table-container {
margin: 0 -4px; /* 抵消父容器的padding */
padding: 0 4px;
}
/* 表格组件移动端优化 */
:deep(.n-data-table-wrapper) {
border-radius: 0.5rem;
}
:deep(.n-data-table-base-table-header, .n-data-table-base-table-body) {
min-width: 100%;
}
:deep(.n-pagination) {
flex-wrap: wrap;
justify-content: center;
margin-top: 8px;
}
/* 改善表单项在移动端的间距 */
/* 保留原有移动端优化样式 */
:deep(.n-form-item) {
margin-bottom: 0.75rem;
}
/* 确保卡片视图在移动端正确显示 */
:deep(.n-grid) {
width: 100% !important;
}
:deep(.n-grid-item) {
width: 100% !important;
max-width: 100% !important;
}
:deep(.n-grid[cols="1 m\\:24"]) {
gap: 8px !important;
}
:deep(.n-grid[cols="1 l\\:2"]) {
gap: 6px !important;
}
:deep(.n-grid-item) > * {
margin-bottom: 8px;
}
:deep(.n-dropdown-menu) {
max-width: 90vw;
}
.app-container {
padding-bottom: 30px; /* 增加移动端底部内边距 */
@@ -1100,10 +1182,31 @@ function handleAnnouncementClose() {
padding: 0.25rem;
}
/* 进一步减少小屏幕卡片内容区域的内边距 */
.analysis-container :deep(.n-card__content) {
padding: 6px 4px;
}
/* 使用更精确的选择器确保覆盖 */
:deep(.n-card) > :deep(.n-card__content),
:deep(.n-card-header) {
padding: 6px 4px !important;
}
/* 减少网格间距到最小 */
:deep(.n-grid[cols="1 l\\:2"]) {
gap: 4px !important;
}
.results-section {
padding: 0.15rem 0.05rem;
}
.results-header {
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
:deep(.n-space) {
@@ -1118,15 +1221,76 @@ function handleAnnouncementClose() {
.analysis-container {
border-radius: 0.625rem;
margin-bottom: 0.5rem;
}
/* 确保下拉菜单在小屏幕上正确显示 */
:deep(.n-dropdown-menu) {
max-width: 90vw;
/* 小屏幕下进一步优化n-grid */
:deep(.n-grid) {
gap: 4px !important;
}
:deep(.n-grid-item) {
padding: 0 !important;
}
/* 确保n-grid-item内容在小屏幕下有更紧凑的间距 */
:deep(.n-grid-item) > * {
margin-bottom: 4px;
}
/* 小屏幕表格样式调整 */
.table-container {
margin: 0 -2px;
padding: 0 2px;
}
/* 小屏幕分页控件优化 */
:deep(.n-pagination .n-pagination-item) {
margin: 0 2px;
}
.app-container {
padding-bottom: 40px; /* 增加小屏幕底部内边距 */
}
}
/* 超小屏幕适配 */
@media (max-width: 375px) {
/* 超小屏幕卡片内容区域几乎无内边距 */
.analysis-container :deep(.n-card__content) {
padding: 4px 2px;
}
/* 使用更精确的选择器确保覆盖 */
:deep(.n-card) > :deep(.n-card__content),
:deep(.n-card-header) {
padding: 3px 2px !important;
}
/* 网格间距最小化 */
:deep(.n-grid[cols="1 l\\:2"]),
:deep(.n-grid[cols="1 m\\:24"]) {
gap: 3px !important;
}
/* 极简边距 */
.results-section {
padding: 0.1rem 0.025rem;
}
/* 进一步调整超小屏幕的间距和尺寸 */
.main-content {
padding: 0.15rem;
}
.config-section {
padding: 0.15rem;
}
/* 确保StockCard组件最大化利用空间 */
:deep(.stock-card) {
margin: 2px 0 !important;
border-radius: 4px !important;
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<n-card class="stock-card mobile-card mobile-shadow" :bordered="false" :class="{ 'is-analyzing': isAnalyzing }">
<div class="card-header">
<n-card class="stock-card mobile-card mobile-shadow mobile-stock-card" :bordered="false" :class="{ 'is-analyzing': isAnalyzing }">
<div class="card-header mobile-card-header">
<div class="header-main">
<div class="header-left">
<div class="stock-info">
@@ -126,7 +126,10 @@
</template>
<template v-else-if="stock.analysisStatus === 'analyzing'">
<div class="analysis-result analysis-streaming" v-if="parsedAnalysis" v-html="parsedAnalysis" :key="analysisContentKey"></div>
<div class="analysis-result analysis-streaming"
ref="analysisResultRef"
v-html="parsedAnalysis">
</div>
</template>
<template v-else-if="stock.analysisStatus === 'completed'">
@@ -138,7 +141,7 @@
</template>
<script setup lang="ts">
import { computed, watch, ref } from 'vue';
import { computed, watch, ref, nextTick, onMounted, onBeforeUnmount } from 'vue';
import { NCard, NDivider, NIcon, NTag, NButton, useMessage } from 'naive-ui';
import {
AlertCircleOutline as AlertCircleIcon,
@@ -159,22 +162,17 @@ const isAnalyzing = computed(() => {
});
const lastAnalysisLength = ref(0);
const lastAnalysisText = ref('');
// 监听分析内容变化
watch(() => props.stock.analysis, (newVal) => {
if (newVal && props.stock.analysisStatus === 'analyzing') {
lastAnalysisLength.value = newVal.length;
lastAnalysisText.value = newVal;
}
}, { immediate: true });
// 添加一个计算属性,用于监控分析内容是否更新
const analysisContentKey = ref(0);
watch(() => props.stock.analysis, (newVal, oldVal) => {
if (newVal && oldVal && newVal.length > oldVal.length && props.stock.analysisStatus === 'analyzing') {
analysisContentKey.value++;
}
}, { immediate: false });
// 分析内容的解析
const parsedAnalysis = computed(() => {
if (props.stock.analysis) {
let result = parseMarkdown(props.stock.analysis);
@@ -414,6 +412,99 @@ const getStatusText = computed(() => {
return '';
}
});
// 添加滚动控制相关变量
const analysisResultRef = ref<HTMLElement | null>(null);
const userScrolling = ref(false);
const scrollPosition = ref(0);
const scrollThreshold = 30; // 底部阈值,小于这个值认为用户已滚动到底部
// 检测用户滚动行为
function handleScroll() {
if (!analysisResultRef.value) return;
const element = analysisResultRef.value;
const atBottom = element.scrollHeight - element.scrollTop - element.clientHeight < scrollThreshold;
// 记录当前滚动位置
scrollPosition.value = element.scrollTop;
// 判断用户是否正在主动滚动
if (atBottom) {
// 用户滚动到底部,标记为非主动滚动状态
userScrolling.value = false;
} else {
// 用户未在底部,标记为主动滚动状态
userScrolling.value = true;
}
}
// 监听滚动事件
onMounted(() => {
if (analysisResultRef.value) {
// 初始滚动到底部
analysisResultRef.value.scrollTop = analysisResultRef.value.scrollHeight;
analysisResultRef.value.addEventListener('scroll', handleScroll);
}
});
// 清理事件监听
onBeforeUnmount(() => {
if (analysisResultRef.value) {
analysisResultRef.value.removeEventListener('scroll', handleScroll);
}
});
// 改进流式更新监听,更保守地控制滚动行为
let isProcessingUpdate = false; // 防止重复处理更新
watch(() => props.stock.analysis, (newVal, oldVal) => {
// 只在分析中且内容增加时处理
if (newVal && oldVal && newVal.length > oldVal.length &&
props.stock.analysisStatus === 'analyzing' && !isProcessingUpdate) {
isProcessingUpdate = true; // 标记正在处理更新
// 检查是否应该自动滚动
let shouldAutoScroll = false;
if (analysisResultRef.value) {
const element = analysisResultRef.value;
// 仅当滚动接近底部或用户尚未开始滚动时自动滚动
const atBottom = element.scrollHeight - element.scrollTop - element.clientHeight < scrollThreshold;
shouldAutoScroll = atBottom || !userScrolling.value;
}
// 使用nextTick确保DOM已更新
nextTick(() => {
if (analysisResultRef.value && shouldAutoScroll) {
// 使用smoothScroll而非直接设置scrollTop减少视觉跳动
smoothScrollToBottom(analysisResultRef.value);
}
// 重置处理标记
setTimeout(() => {
isProcessingUpdate = false;
}, 50); // 短暂延迟,防止过快连续处理
});
}
}, { immediate: false });
// 平滑滚动到底部的辅助函数
function smoothScrollToBottom(element: HTMLElement) {
const targetPosition = element.scrollHeight;
// 如果已经很接近底部,直接跳转避免不必要的动画
const currentGap = targetPosition - element.scrollTop - element.clientHeight;
if (currentGap < 100) {
element.scrollTop = targetPosition;
return;
}
// 否则使用平滑滚动
element.scrollTo({
top: targetPosition,
behavior: 'smooth'
});
}
</script>
<style scoped>
@@ -423,6 +514,8 @@ const getStatusText = computed(() => {
flex-direction: column;
transition: all 0.3s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
width: 100%; /* 确保宽度不会超过容器 */
max-width: 100%; /* 限制最大宽度 */
}
.stock-card.is-analyzing {
@@ -439,6 +532,7 @@ const getStatusText = computed(() => {
position: relative;
background: linear-gradient(to bottom, rgba(240, 240, 245, 0.3), transparent);
border-radius: 8px 8px 0 0;
width: 100%; /* 确保宽度不会超过容器 */
}
.header-main {
@@ -525,6 +619,7 @@ const getStatusText = computed(() => {
.header-right {
display: flex;
align-items: center;
max-width: 380px;
}
.copy-button {
@@ -738,6 +833,8 @@ const getStatusText = computed(() => {
text-align: left;
display: flex;
flex-direction: column;
width: 100%; /* 确保宽度不会超过容器 */
overflow-x: hidden; /* 防止内容横向溢出 */
}
.error-status {
@@ -771,10 +868,18 @@ const getStatusText = computed(() => {
width: 100%;
max-width: 100%;
overflow-x: hidden;
display: block; /* 确保显示为块级元素 */
box-sizing: border-box; /* 确保padding不增加宽度 */
/* 自定义滚动条样式 */
scrollbar-width: thin; /* Firefox */
scrollbar-color: rgba(32, 128, 240, 0.3) transparent; /* Firefox */
/* 改进滚动行为 */
scroll-behavior: smooth;
overflow-anchor: auto;
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
will-change: scroll-position;
}
/* Webkit浏览器的滚动条样式 */
@@ -807,6 +912,13 @@ const getStatusText = computed(() => {
position: relative;
border-left: 2px solid var(--n-info-color);
animation: fadePulse 2s infinite;
/* 改进滚动行为 */
overflow-y: auto;
scroll-behavior: smooth;
will-change: scroll-position;
/* 防止内容更新时的布局抖动 */
contain: content;
scroll-padding-bottom: 20px;
}
/* 改进流式输出的动画效果,消除闪烁 */
@@ -886,6 +998,8 @@ const getStatusText = computed(() => {
border-radius: 3px;
font-family: monospace;
font-size: 0.85em;
white-space: pre-wrap; /* 允许代码内容自动换行 */
word-break: break-word; /* 确保长单词可以换行 */
}
.analysis-result :deep(pre) {
@@ -896,12 +1010,16 @@ const getStatusText = computed(() => {
margin: 0.75rem 0;
border-left: 3px solid #2080f0;
max-width: 100%;
width: 100%;
box-sizing: border-box;
white-space: pre-wrap; /* 允许代码块自动换行 */
word-break: break-word; /* 允许长单词换行 */
}
.analysis-result :deep(pre code) {
background: transparent;
padding: 0;
white-space: inherit; /* 继承pre的换行行为 */
}
/* 优化引用样式 */
@@ -923,6 +1041,8 @@ const getStatusText = computed(() => {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
table-layout: fixed; /* 固定表格布局 */
max-width: 100%;
display: block; /* 使表格成为块级元素 */
overflow-x: auto; /* 允许表格滚动 */
}
.analysis-result :deep(th), .analysis-result :deep(td) {
@@ -994,6 +1114,10 @@ const getStatusText = computed(() => {
border-bottom: 1px dotted #2080f0;
transition: all 0.2s ease;
font-weight: 500;
word-break: break-word;
overflow-wrap: break-word;
display: inline-block;
max-width: 100%;
}
.analysis-result :deep(a:hover) {
@@ -1009,16 +1133,13 @@ const getStatusText = computed(() => {
margin: 0.75rem auto;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
object-fit: contain; /* 保持图片比例 */
}
/* 移动端适配样式 */
@media (max-width: 768px) {
.stock-card {
margin-bottom: 0.75rem;
width: 100% !important;
box-sizing: border-box !important;
border-radius: 0.75rem !important;
overflow: hidden;
}
.card-header {
@@ -1027,19 +1148,77 @@ const getStatusText = computed(() => {
}
.header-main {
flex-direction: column;
align-items: flex-start;
flex-direction: row;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
}
.header-left {
display: flex;
flex-wrap: wrap;
gap: 8px;
width: 100%;
margin-bottom: 0.5rem;
}
.stock-info {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
min-width: auto;
}
.stock-code {
font-size: 1.2rem;
}
.stock-name {
font-size: 0.8rem;
max-width: 100px;
}
.header-right {
margin-top: 0.5rem;
align-self: flex-end;
width: 320px;
display: flex;
justify-content: flex-end;
}
.stock-price-info {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
margin-top: 0.5rem;
gap: 0.75rem;
gap: 16px;
border-left: none;
border-top: 1px dashed rgba(0, 0, 0, 0.09);
padding-top: 8px;
padding-left: 0;
width: 100%;
}
.stock-price, .stock-change {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
padding: 0;
}
.stock-price .label,
.stock-change .label {
font-size: 0.75rem;
}
.stock-price .value {
font-size: 1rem;
}
.stock-change .value {
font-size: 0.9rem;
}
.stock-summary {
@@ -1049,10 +1228,43 @@ const getStatusText = computed(() => {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.indicators-grid {
grid-template-columns: repeat(2, 1fr);
.technical-indicators {
margin: 0.75rem 0.5rem;
background-color: rgba(240, 240, 245, 0.5);
border-radius: 0.5rem;
padding: 0.5rem;
gap: 0.5rem;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.05);
}
.indicators-grid {
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
padding: 0.25rem;
}
.indicator-item {
border-radius: 0.5rem;
padding: 0.625rem 0.5rem;
background-color: rgba(255, 255, 255, 0.7);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
}
.indicator-item:active {
transform: scale(0.98);
box-shadow: 0 0 1px rgba(0, 0, 0, 0.1);
}
.indicator-value {
font-size: 0.95rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.indicator-label {
font-size: 0.7rem;
color: var(--n-text-color-3);
margin-top: 0.125rem;
}
.actions-bar {
@@ -1066,35 +1278,242 @@ const getStatusText = computed(() => {
height: 36px !important;
}
.analysis-result {
font-size: 0.8125rem;
padding: 0.5rem 0.75rem;
max-height: 300px;
border-radius: 0.5rem;
border: 1px solid rgba(0, 0, 0, 0.05);
margin: 0.5rem;
.card-content {
padding: 0.5rem 0.3rem;
}
.analysis-result :deep(pre) {
font-size: 0.75rem;
padding: 0.5rem;
border-radius: 0.375rem;
.analysis-result {
font-size: 0.85rem;
line-height: 1.65;
padding: 0.6rem 0.5rem;
max-height: 350px;
border-radius: 0.5rem;
border: 1px solid rgba(0, 0, 0, 0.07);
margin: 0.4rem 0;
background-color: rgba(255, 255, 255, 0.7);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
-webkit-overflow-scrolling: touch; /* 加强iOS滚动平滑性 */
overscroll-behavior: contain; /* 防止滚动传播 */
touch-action: pan-y; /* 优化触摸滚动体验 */
width: 100%; /* 占据全部可用宽度 */
box-sizing: border-box;
position: relative; /* 确保滚动提示正确定位 */
overflow-x: hidden !important; /* 强制禁止横向滚动 */
}
/* 优化表格在移动端的显示 */
.analysis-result :deep(table) {
width: 100% !important;
max-width: 100% !important;
display: block;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
font-size: 0.8rem;
border: none;
border-radius: 0.4rem;
margin: 0.7rem 0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.07);
position: relative;
}
/* 优化代码块在移动端的显示 */
.analysis-result :deep(pre) {
font-size: 0.8rem;
padding: 0.75rem 0.5rem;
border-radius: 0.4rem;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
margin: 0.7rem 0;
background-color: rgba(0, 0, 0, 0.04);
border-left: 3px solid rgba(32, 128, 240, 0.5);
width: 100% !important;
box-sizing: border-box;
white-space: pre-wrap;
word-break: break-word;
position: relative;
}
/* 拖动滚动提示效果 - 恢复并优化 */
.analysis-result :deep(pre)::after,
.analysis-result :deep(table)::after {
content: '⟷';
position: absolute;
right: 5px;
bottom: 5px;
color: rgba(32, 128, 240, 0.5);
font-size: 12px;
opacity: 0.6;
pointer-events: none;
z-index: 3;
}
/* 改进链接触摸体验 */
.analysis-result :deep(a) {
padding: 0.1rem 0;
margin: 0 0.1rem;
word-break: break-word;
overflow-wrap: break-word;
max-width: 100%;
}
/* 改进按钮和交互元素触摸体验 */
.analysis-result :deep(button),
.analysis-result :deep(.interactive) {
min-height: 36px; /* 最小触摸高度 */
min-width: 36px; /* 最小触摸宽度 */
display: inline-flex;
align-items: center;
justify-content: center;
}
/* 确保所有内容在移动端都能正确换行和显示 */
.analysis-result :deep(*) {
max-width: 100% !important;
box-sizing: border-box !important;
}
.analysis-streaming {
background-color: rgba(32, 128, 240, 0.03);
}
.analysis-completed {
background-color: rgba(24, 160, 88, 0.02);
}
/* 优化标题样式 */
.analysis-result :deep(h1),
.analysis-result :deep(h2),
.analysis-result :deep(h3) {
margin: 1rem 0 0.7rem 0;
line-height: 1.3;
padding-bottom: 0.4rem;
}
.analysis-result :deep(h1) {
font-size: 1.3rem;
}
.analysis-result :deep(h2) {
font-size: 1.15rem;
}
.analysis-result :deep(h3) {
font-size: 1rem;
}
/* 优化段落间距 */
.analysis-result :deep(p) {
margin: 0.6rem 0;
}
/* 优化列表样式 */
.analysis-result :deep(ul),
.analysis-result :deep(ol) {
padding-left: 1.2rem;
margin: 0.6rem 0;
}
.analysis-result :deep(li) {
margin-bottom: 0.35rem;
padding-left: 0.3rem;
}
/* 优化引用块 */
.analysis-result :deep(blockquote) {
margin: 0.7rem 0;
padding: 0.6rem 0.75rem;
border-left: 4px solid #f0a020;
background-color: rgba(240, 160, 32, 0.07);
border-radius: 0.25rem;
}
/* 优化代码块 */
.analysis-result :deep(pre) {
font-size: 0.8rem;
padding: 0.75rem 0.5rem;
border-radius: 0.4rem;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
margin: 0.7rem 0;
background-color: rgba(0, 0, 0, 0.04);
border-left: 3px solid rgba(32, 128, 240, 0.5);
white-space: pre-wrap;
}
.analysis-result :deep(code) {
font-size: 0.8rem;
padding: 0.15rem 0.3rem;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 0.2rem;
}
/* 优化表格显示 */
.analysis-result :deep(table) {
display: block;
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
width: 100%;
border-radius: 0.375rem;
border-radius: 0.4rem;
margin: 0.7rem 0;
font-size: 0.8rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.07);
}
.indicator-item {
border-radius: 0.5rem;
padding: 0.625rem;
.analysis-result :deep(th),
.analysis-result :deep(td) {
padding: 0.5rem 0.4rem;
}
/* 优化强调文本 */
.analysis-result :deep(strong) {
font-weight: 600;
}
/* 优化专业术语显示 */
.analysis-result :deep(.buy),
.analysis-result :deep(.sell),
.analysis-result :deep(.hold) {
padding: 0.1rem 0.3rem;
border-radius: 0.2rem;
}
.analysis-result :deep(.indicator) {
padding: 0.1rem 0.3rem;
border-radius: 0.2rem;
}
/* 优化图片显示 */
.analysis-result :deep(img) {
max-width: 100%;
height: auto;
border-radius: 0.4rem;
margin: 0.7rem auto;
}
/* 优化滚动条样式 */
.analysis-result::-webkit-scrollbar {
width: 4px;
height: 4px;
}
.analysis-result::-webkit-scrollbar-thumb {
background-color: rgba(32, 128, 240, 0.3);
border-radius: 2px;
}
/* 滚动提示 */
.analysis-result::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 20px;
background: linear-gradient(to top, rgba(255, 255, 255, 0.7), transparent);
pointer-events: none;
opacity: 0.8;
border-radius: 0 0 0.5rem 0.5rem;
z-index: 2;
}
}
@@ -1106,28 +1525,78 @@ const getStatusText = computed(() => {
}
.stock-info {
flex-direction: column;
align-items: flex-start;
flex-direction: row;
align-items: center;
gap: 6px;
}
.stock-code {
font-size: 1rem;
}
.stock-name {
margin-left: 0;
margin-top: 0.25rem;
font-size: 0.875rem;
margin-top: 0;
font-size: 0.75rem;
max-width: 80px;
}
.stock-price-info {
gap: 12px;
padding-top: 6px;
margin-top: 6px;
flex-wrap: nowrap;
}
.stock-price, .stock-change {
white-space: nowrap;
}
.stock-price .label,
.stock-change .label {
font-size: 0.7rem;
}
.stock-price .value {
font-size: 0.85rem;
}
.stock-change .value {
font-size: 0.8rem;
padding: 1px 4px;
}
.technical-indicators {
margin: 0.5rem 0.25rem;
border-radius: 0.45rem;
padding: 0.4rem 0.3rem;
}
.indicators-grid {
grid-template-columns: repeat(1, 1fr);
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
padding: 0.2rem;
}
.indicator-item {
padding: 0.5rem;
border-radius: 0.45rem;
padding: 0.5rem 0.25rem;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
background-color: rgba(255, 255, 255, 0.7);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
}
.analysis-result {
font-size: 0.75rem;
padding: 0.5rem;
margin: 0.375rem;
.indicator-value {
font-size: 0.9rem;
margin-bottom: 0.15rem;
}
.indicator-label {
font-size: 0.7rem;
margin-top: 0;
}
.card-header {
@@ -1138,5 +1607,208 @@ const getStatusText = computed(() => {
.stock-card, .indicator-item, .analysis-result {
border: 1px solid rgba(0, 0, 0, 0.08) !important;
}
/* 为不同类型的指标设置不同的边框颜色 */
.indicator-item .rsi-overbought {
border-bottom: 2px solid #d03050;
}
.indicator-item .rsi-oversold {
border-bottom: 2px solid #18a058;
}
.indicator-item .trend-up {
border-bottom: 2px solid #d03050;
}
.indicator-item .trend-down {
border-bottom: 2px solid #18a058;
}
.indicator-item .signal-buy {
border-bottom: 2px solid #d03050;
}
.indicator-item .signal-sell {
border-bottom: 2px solid #18a058;
}
/* 分析结果小屏幕样式 */
.analysis-result {
font-size: 0.825rem;
line-height: 1.6;
padding: 0.5rem 0.4rem;
margin: 0.2rem 0;
max-height: 300px;
max-width: none; /* 移除宽度限制 */
width: 100%; /* 占据全部可用宽度 */
box-sizing: border-box;
}
.card-content {
padding: 0.3rem 0.1rem;
}
.analysis-result :deep(h1) {
font-size: 1.2rem;
margin-top: 0.85rem;
}
.analysis-result :deep(h2) {
font-size: 1.1rem;
}
.analysis-result :deep(h3) {
font-size: 0.95rem;
}
.analysis-result :deep(ul),
.analysis-result :deep(ol) {
padding-left: 1rem;
}
.analysis-result :deep(blockquote) {
padding: 0.5rem 0.625rem;
}
.analysis-result :deep(pre) {
font-size: 0.75rem;
padding: 0.6rem 0.4rem;
}
.analysis-result :deep(code) {
font-size: 0.75rem;
}
.analysis-result :deep(th),
.analysis-result :deep(td) {
padding: 0.4rem 0.3rem;
}
}
/* 超小屏幕适配 */
@media (max-width: 375px) {
.indicators-grid {
grid-template-columns: repeat(2, 1fr);
gap: 0.4rem;
}
.indicator-item {
padding: 0.4rem 0.2rem;
}
.indicator-value {
font-size: 0.85rem;
margin-bottom: 0.1rem;
}
.indicator-label {
font-size: 0.65rem;
}
/* 分析结果超小屏幕样式 */
.analysis-result {
font-size: 0.8rem;
padding: 0.4rem 0.3rem;
margin: 0.1rem 0;
width: 100%; /* 占据全部可用宽度 */
box-sizing: border-box;
}
.analysis-result :deep(h1) {
font-size: 1.15rem;
}
.analysis-result :deep(h2) {
font-size: 1.05rem;
}
.analysis-result :deep(h3) {
font-size: 0.9rem;
}
.card-content {
padding: 0.2rem 0.05rem;
}
}
/* 添加PC端特定样式确保纵向布局 */
@media (min-width: 769px) {
.stock-card {
max-width: 100%;
display: flex;
flex-direction: column;
}
.card-header {
flex-direction: column;
}
.header-main {
flex-direction: row;
flex-wrap: nowrap;
}
.header-left {
flex-direction: row;
flex-wrap: nowrap;
}
.stock-price-info {
flex-direction: column;
flex-wrap: nowrap;
}
.stock-summary {
flex-direction: row;
flex-wrap: nowrap;
}
.card-content {
width: 100%;
overflow-x: hidden;
}
.analysis-result {
width: 100%;
max-width: 100%;
overflow-x: hidden;
}
/* 优化技术指标在PC端的显示 */
.indicators-grid {
grid-template-columns: repeat(5, 1fr);
gap: 1rem;
}
}
/* 确保所有嵌套元素不会超出容器 */
.analysis-result :deep(*) {
max-width: 100%;
box-sizing: border-box;
}
/* 对于图片特别控制 */
.analysis-result :deep(img) {
max-width: 100%;
height: auto;
display: block;
margin: 0.75rem auto;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
object-fit: contain; /* 保持图片比例 */
}
/* 修复长链接可能导致的溢出 */
.analysis-result :deep(a) {
word-break: break-word;
overflow-wrap: break-word;
display: inline-block;
max-width: 100%;
}
/* 删除滚动控制面板样式 */
.scroll-controls {
display: none;
}
</style>

View File

@@ -13,7 +13,7 @@
</template>
</n-input>
<div class="search-results" v-show="showResults">
<div class="search-results mobile-search-results" v-show="showResults">
<div v-if="loading" class="loading-results">
<n-spin size="small" />
<span>搜索中...</span>
@@ -28,12 +28,12 @@
<div
v-for="item in results"
:key="item.symbol"
class="search-result-item"
class="search-result-item mobile-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>
<span class="result-name mobile-result-name">{{ item.name }}</span>
</div>
<div class="result-meta">
<span class="result-market">{{ item.market }}</span>
@@ -106,7 +106,10 @@ function handleSearchInput() {
}
function selectStock(item: SearchResult) {
emit('select', item.symbol);
// 处理symbol确保不包含序号
// 假设symbol格式可能是"1. AAPL"这样的格式,我们只需要"AAPL"部分
const cleanSymbol = item.symbol.replace(/^\d+\.\s*/, '');
emit('select', cleanSymbol);
searchKeyword.value = '';
showResults.value = false;
}
@@ -224,39 +227,14 @@ onBeforeUnmount(() => {
/* 移动端适配 */
@media (max-width: 768px) {
.search-results {
max-width: 100%;
width: 100%;
border-radius: 0.75rem;
border: 1px solid var(--n-border-color, rgba(0, 0, 0, 0.1));
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.search-result-item {
padding: 0.625rem 0.875rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.search-result-item:last-child {
border-bottom: none;
}
.result-name {
max-width: 170px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 确保输入框在移动端正确显示 */
:deep(.n-input) {
width: 100% !important;
}
/* 增大触摸区域 */
.search-result-item {
min-height: 44px;
}
}
@media (max-width: 480px) {
@@ -264,30 +242,14 @@ onBeforeUnmount(() => {
font-size: 0.875rem;
}
.result-name, .result-market, .result-market-value {
.result-market, .result-market-value {
font-size: 0.75rem;
}
.result-name {
max-width: 120px;
}
.search-result-item {
padding: 0.5rem 0.75rem;
}
.search-results {
border-radius: 0.625rem;
}
.loading-results, .no-results {
padding: 0.75rem;
font-size: 0.75rem;
}
/* 确保边框在小屏幕上清晰可见 */
.search-results {
border: 1px solid rgba(0, 0, 0, 0.08);
}
}
</style>